Merge pull request #1049 from tgstation/1041-ContextPooling

Adds database context pooling
This commit is contained in:
Jordan Brown
2020-06-13 21:13:35 -04:00
committed by GitHub
20 changed files with 283 additions and 313 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<TgsCoreVersion>4.3.2</TgsCoreVersion>
<TgsCoreVersion>4.4.0</TgsCoreVersion>
<TgsApiVersion>7.0.0</TgsApiVersion>
<TgsClientVersion>7.3.0</TgsClientVersion>
<TgsDmapiVersion>5.2.2</TgsDmapiVersion>
@@ -177,7 +177,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
public override async Task Disconnect(CancellationToken cancellationToken)
protected override async Task DisconnectImpl(CancellationToken cancellationToken)
{
Logger.LogTrace("Disconnecting...");
if (!Connected)
@@ -337,7 +337,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Title = "Code Deployment",
Footer = new EmbedFooterBuilder
{
Text = "In progress... ETA"
Text = $"In progress...{(estimatedCompletionTime.HasValue ? " ETA" : String.Empty)}"
},
Timestamp = estimatedCompletionTime
};
@@ -350,7 +350,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
var message = await channel.SendMessageAsync(
String.Empty,
"DM: Deployment in Progress...",
false,
builder.Build(),
new RequestOptions
@@ -361,7 +361,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return async (errorMessage, dreamMakerOutput) =>
{
builder.Footer.Text = errorMessage == null ? "Succeeded" : "Failed";
var completionString = errorMessage == null ? "Succeeded" : "Failed";
builder.Footer.Text = completionString;
builder.Color = errorMessage == null ? Color.Green : Color.Red;
builder.Timestamp = DateTimeOffset.Now;
builder.Description = errorMessage == null
@@ -394,7 +395,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
try
{
await channel.SendMessageAsync(
String.Empty,
$"DM: Deployment {completionString}!",
false,
builder.Build())
.ConfigureAwait(false);
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Task<bool> Connect(CancellationToken cancellationToken);
/// <summary>
/// Gracefully disconnects the provider. Implies a call to <see cref="IDisposable.Dispose"/>
/// Gracefully disconnects the provider. Permanently stops the reconnection timer.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
@@ -346,7 +346,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public override async Task Disconnect(CancellationToken cancellationToken)
protected override async Task DisconnectImpl(CancellationToken cancellationToken)
{
if (!Connected)
return;
@@ -87,8 +87,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public abstract Task<bool> Connect(CancellationToken cancellationToken);
/// <summary>
/// Gracefully disconnects the provider.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected abstract Task DisconnectImpl(CancellationToken cancellationToken);
/// <inheritdoc />
public abstract Task Disconnect(CancellationToken cancellationToken);
public async Task Disconnect(CancellationToken cancellationToken)
{
await StopReconnectionTimer().ConfigureAwait(false);
await DisconnectImpl(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public abstract Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
@@ -67,6 +67,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IDatabaseSeeder"/> for the <see cref="InstanceManager"/>
/// </summary>
readonly IDatabaseSeeder databaseSeeder;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="InstanceManager"/>
/// </summary>
@@ -113,6 +118,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="serverControl">The value of <see cref="serverControl"/></param>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="databaseSeeder">The value of <see cref="databaseSeeder"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="logger">The value of <see cref="logger"/></param>
public InstanceManager(
@@ -124,6 +130,7 @@ namespace Tgstation.Server.Host.Components
IServerControl serverControl,
ISystemIdentityFactory systemIdentityFactory,
IAsyncDelayer asyncDelayer,
IDatabaseSeeder databaseSeeder,
IOptions<GeneralConfiguration> generalConfigurationOptions,
ILogger<InstanceManager> logger)
{
@@ -135,6 +142,7 @@ namespace Tgstation.Server.Host.Components
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -274,7 +282,7 @@ namespace Tgstation.Server.Host.Components
{
CheckSystemCompatibility();
var factoryStartup = instanceFactory.StartAsync(cancellationToken);
await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
await databaseSeeder.Initialize(databaseContext, cancellationToken).ConfigureAwait(false);
await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
var dbInstances = databaseContext
.Instances
@@ -323,7 +331,7 @@ namespace Tgstation.Server.Host.Components
// downgrade the db if necessary
if (downgradeVersion != null)
await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
await databaseContextFactory.UseContext(db => databaseSeeder.Downgrade(db, downgradeVersion, cancellationToken)).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -197,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers
{
var query = DatabaseContext.ChatBots
.AsQueryable()
.Where(x => x.Id == id)
.Where(x => x.Id == id && x.InstanceId == Instance.Id)
.Include(x => x.Channels);
var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+15 -1
View File
@@ -18,6 +18,7 @@ using Serilog.Formatting.Display;
using System;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
@@ -207,10 +208,23 @@ namespace Tgstation.Server.Host.Core
void AddTypedContext<TContext>() where TContext : DatabaseContext
{
services.AddDbContext<TContext>(builder =>
// HACK HACK HACK HACK HACK
const string ConfigureMethodName = nameof(SqlServerDatabaseContext.ConfigureWith);
var configureFunction = typeof(TContext).GetMethod(
nameof(SqlServerDatabaseContext.ConfigureWith),
BindingFlags.Public | BindingFlags.Static);
if (configureFunction == null)
throw new InvalidOperationException($"Context type {typeof(TContext).FullName} missing static {ConfigureMethodName} function!");
services.AddDbContextPool<TContext>((serviceProvider, builder) =>
{
if (hostingEnvironment.IsDevelopment())
builder.EnableSensitiveDataLogging();
var databaseConfigOptions = serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>();
var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!");
configureFunction.Invoke(null, new object[] { builder, databaseConfig });
});
services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
}
@@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Globalization;
using System.Linq;
@@ -19,11 +18,8 @@ namespace Tgstation.Server.Host.Database
/// Backend abstract implementation of <see cref="IDatabaseContext"/>
/// </summary>
#pragma warning disable CA1506 // TODO: Decomplexify
abstract class DatabaseContext : DbContext, IDatabaseContext
public abstract class DatabaseContext : DbContext, IDatabaseContext
{
/// <inheritdoc />
public DatabaseType DatabaseType => DatabaseConfiguration.DatabaseType;
/// <summary>
/// The <see cref="User"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
@@ -99,16 +95,6 @@ namespace Tgstation.Server.Host.Database
/// </summary>
public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="DatabaseContext"/>
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// The <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/>
/// </summary>
protected DatabaseConfiguration DatabaseConfiguration { get; }
/// <summary>
/// The <see cref="DeleteBehavior"/> for the <see cref="CompileJob"/>/<see cref="RevisionInformation"/> foreign key.
/// </summary>
@@ -153,11 +139,6 @@ namespace Tgstation.Server.Host.Database
/// <inheritdoc />
IDatabaseCollection<DualReattachInformation> IDatabaseContext.WatchdogReattachInformations => watchdogReattachInformationsCollection;
/// <summary>
/// The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/>
/// </summary>
readonly IDatabaseSeeder databaseSeeder;
/// <summary>
/// Backing field for <see cref="IDatabaseContext.Users"/>.
/// </summary>
@@ -227,15 +208,8 @@ namespace Tgstation.Server.Host.Database
/// Construct a <see cref="DatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions"/> for the <see cref="DatabaseContext"/>.</param>
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="DatabaseConfiguration"/></param>
/// <param name="databaseSeeder">The value of <see cref="databaseSeeder"/></param>
/// <param name="logger">The value of <see cref="Logger"/></param>
public DatabaseContext(DbContextOptions dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
public DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
{
DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
usersCollection = new DatabaseCollection<User>(Users);
instancesCollection = new DatabaseCollection<Instance>(Instances);
instanceUsersCollection = new DatabaseCollection<InstanceUser>(InstanceUsers);
@@ -254,8 +228,9 @@ namespace Tgstation.Server.Host.Database
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Setup our more complex database relations
Logger.LogTrace("Building entity framework context...");
if (modelBuilder == null)
throw new ArgumentNullException(nameof(modelBuilder));
base.OnModelCreating(modelBuilder);
var userModel = modelBuilder.Entity<User>();
@@ -306,52 +281,41 @@ namespace Tgstation.Server.Host.Database
}
/// <inheritdoc />
public async Task Initialize(CancellationToken cancellationToken)
public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
/// <inheritdoc />
public Task Drop(CancellationToken cancellationToken) => Database.EnsureDeletedAsync(cancellationToken);
/// <inheritdoc />
public async Task<bool> Migrate(ILogger<DatabaseContext> logger, CancellationToken cancellationToken)
{
ValidateDatabaseType();
if (DatabaseConfiguration.DropDatabase)
{
Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
}
if (logger == null)
throw new ArgumentNullException(nameof(logger));
var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
var wasEmpty = !migrations.Any();
if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
{
Logger.LogInformation("Migrating database...");
logger.LogInformation("Migrating database...");
await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
}
else
Logger.LogDebug("No migrations to apply.");
logger.LogDebug("No migrations to apply");
wasEmpty |= (await Users.AsQueryable().CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
if (wasEmpty)
{
Logger.LogInformation("Seeding database...");
await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
}
else
{
if (DatabaseConfiguration.ResetAdminPassword)
{
Logger.LogWarning("Enabling and resetting admin password due to configuration!");
await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
}
await databaseSeeder.SanitizeDatabase(this, cancellationToken).ConfigureAwait(false);
}
return wasEmpty;
}
/// <inheritdoc />
public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
/// <inheritdoc />
public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
public async Task SchemaDowngradeForServerVersion(
ILogger<DatabaseContext> logger,
Version version,
DatabaseType currentDatabaseType,
CancellationToken cancellationToken)
{
if(logger == null)
throw new ArgumentNullException(nameof(logger));
if (version == null)
throw new ArgumentNullException(nameof(version));
if (version < new Version(4, 0))
@@ -360,23 +324,23 @@ namespace Tgstation.Server.Host.Database
// Update this with new migrations as they are made
string targetMigration = null;
if (DatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0))
if (currentDatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0))
throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!");
if (version < new Version(4, 1, 0))
throw new NotSupportedException("Cannot migrate below version 4.1.0!");
if (version < new Version(4, 2, 0))
targetMigration = DatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete);
targetMigration = currentDatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete);
if (targetMigration == null)
{
Logger.LogDebug("No down migration required.");
logger.LogDebug("No down migration required.");
return;
}
string migrationSubstitution;
switch (DatabaseType)
switch (currentDatabaseType)
{
case DatabaseType.SqlServer:
// already setup
@@ -393,7 +357,7 @@ namespace Tgstation.Server.Host.Database
migrationSubstitution = "PG{0}";
break;
default:
throw new InvalidOperationException($"Invalid DatabaseType: {DatabaseType}");
throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}");
}
if (migrationSubstitution != null)
@@ -403,20 +367,15 @@ namespace Tgstation.Server.Host.Database
var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
try
{
await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
Logger.LogCritical("Failed to migrate! Exception: {0}", e);
logger.LogCritical("Failed to migrate! Exception: {0}", e);
}
}
/// <summary>
/// Ensure the <see cref="DatabaseType"/> is correct for the <see cref="DatabaseContext"/>.
/// </summary>
protected abstract void ValidateDatabaseType();
}
}
@@ -1,9 +1,12 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -23,15 +26,41 @@ namespace Tgstation.Server.Host.Database
/// </summary>
readonly IPlatformIdentifier platformIdentifier;
/// <summary>
/// The <see cref="ILogger"/> used for <see cref="IDatabaseContext"/>s.
/// </summary>
readonly ILogger<DatabaseContext> databaseLogger;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="DatabaseSeeder"/>.
/// </summary>
readonly ILogger<DatabaseSeeder> logger;
/// <summary>
/// The <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseSeeder"/>.
/// </summary>
readonly DatabaseConfiguration databaseConfiguration;
/// <summary>
/// Construct a <see cref="DatabaseSeeder"/>
/// </summary>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
public DatabaseSeeder(ICryptographySuite cryptographySuite, IPlatformIdentifier platformIdentifier)
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="databaseConfiguration"/>.</param>
/// <param name="databaseLogger">The value of <see cref="databaseLogger"/></param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public DatabaseSeeder(
ICryptographySuite cryptographySuite,
IPlatformIdentifier platformIdentifier,
IOptions<DatabaseConfiguration> databaseConfigurationOptions,
ILogger<DatabaseContext> databaseLogger,
ILogger<DatabaseSeeder> logger)
{
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
this.databaseLogger = databaseLogger ?? throw new ArgumentNullException(nameof(databaseLogger));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
@@ -53,15 +82,25 @@ namespace Tgstation.Server.Host.Database
databaseContext.Users.Add(admin);
}
/// <inheritdoc />
public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
/// <summary>
/// Initially seed a given <paramref name="databaseContext"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to seed</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
{
SeedAdminUser(databaseContext);
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
/// <summary>
/// Correct invalid database data caused by previous versions.
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to sanitize.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
{
var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false);
if (admin != null)
@@ -88,8 +127,13 @@ namespace Tgstation.Server.Host.Database
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
/// <summary>
/// Changes the admin password in <see cref="IDatabaseContext"/> back to it's default and enables the account
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to reset the admin password for</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
{
var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false);
if (admin != null)
@@ -120,5 +164,46 @@ namespace Tgstation.Server.Host.Database
return admin;
}
/// <inheritdoc />
public async Task Initialize(IDatabaseContext databaseContext, CancellationToken cancellationToken)
{
if (databaseContext == null)
throw new ArgumentNullException(nameof(databaseContext));
if (databaseConfiguration.DropDatabase)
{
logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
await databaseContext.Drop(cancellationToken).ConfigureAwait(false);
}
var wasEmpty = await databaseContext.Migrate(databaseLogger, cancellationToken).ConfigureAwait(false);
if (wasEmpty)
{
logger.LogInformation("Seeding database...");
await SeedDatabase(databaseContext, cancellationToken).ConfigureAwait(false);
}
else
{
if (databaseConfiguration.ResetAdminPassword)
{
logger.LogWarning("Enabling and resetting admin password due to configuration!");
await ResetAdminPassword(databaseContext, cancellationToken).ConfigureAwait(false);
}
await SanitizeDatabase(databaseContext, cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public Task Downgrade(IDatabaseContext databaseContext, Version downgradeVersion, CancellationToken cancellationToken)
{
if (databaseContext == null)
throw new ArgumentNullException(nameof(databaseContext));
if (downgradeVersion == null)
throw new ArgumentNullException(nameof(downgradeVersion));
return databaseContext.SchemaDowngradeForServerVersion(databaseLogger, downgradeVersion, databaseConfiguration.DatabaseType, cancellationToken);
}
}
}
@@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Database.Design
{
/// <summary>
/// <see cref="IDesignTimeDbContextFactory{TContext}"/> for creating <see cref="MySqlDatabaseContext"/>s.
/// </summary>
sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MySqlDatabaseContext>
{
/// <inheritdoc />
public MySqlDatabaseContext CreateDbContext(string[] args)
{
using var loggerFactory = new LoggerFactory();
return new MySqlDatabaseContext(
new DbContextOptions<MySqlDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration(
DatabaseType.MariaDB,
"Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"),
new DatabaseSeeder(
new CryptographySuite(
new PasswordHasher<User>()),
new PlatformIdentifier()),
loggerFactory.CreateLogger<MySqlDatabaseContext>());
}
}
}
@@ -1,31 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Database.Design
{
/// <inheritdoc />
sealed class PostgresSqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<PostgresSqlDatabaseContext>
{
/// <inheritdoc />
public PostgresSqlDatabaseContext CreateDbContext(string[] args)
{
using var loggerFactory = new LoggerFactory();
return new PostgresSqlDatabaseContext(
new DbContextOptions<PostgresSqlDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration(
DatabaseType.PostgresSql,
"Application Name=tgstation-server;Host=127.0.0.1;Password=qCkWimNgLfWwpr7TnUHs;Username=postgres;Database=TGS_Design"),
new DatabaseSeeder(
new CryptographySuite(
new PasswordHasher<User>()),
new PlatformIdentifier()),
loggerFactory.CreateLogger<PostgresSqlDatabaseContext>());
}
}
}
@@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Database.Design
{
/// <summary>
/// <see cref="IDesignTimeDbContextFactory{TContext}"/> for creating <see cref="SqlServerDatabaseContext"/>s.
/// </summary>
sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqlServerDatabaseContext>
{
/// <inheritdoc />
public SqlServerDatabaseContext CreateDbContext(string[] args)
{
using var loggerFactory = new LoggerFactory();
return new SqlServerDatabaseContext(
new DbContextOptions<SqlServerDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration(
DatabaseType.SqlServer,
"Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"),
new DatabaseSeeder(
new CryptographySuite(
new PasswordHasher<User>()),
new PlatformIdentifier()),
loggerFactory.CreateLogger<SqlServerDatabaseContext>());
}
}
}
@@ -1,11 +1,7 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Database.Design
{
@@ -18,16 +14,13 @@ namespace Tgstation.Server.Host.Database.Design
public SqliteDatabaseContext CreateDbContext(string[] args)
{
using var loggerFactory = new LoggerFactory();
return new SqliteDatabaseContext(
new DbContextOptions<SqliteDatabaseContext>(),
var config =
DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration(
DatabaseType.Sqlite,
"Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"),
new DatabaseSeeder(
new CryptographySuite(
new PasswordHasher<User>()),
new PlatformIdentifier()),
loggerFactory.CreateLogger<SqliteDatabaseContext>());
"Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate");
SqliteDatabaseContext.DesignTime = config.Value.DesignTime;
return new SqliteDatabaseContext(
new DbContextOptions<SqliteDatabaseContext>());
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -12,11 +13,6 @@ namespace Tgstation.Server.Host.Database
/// </summary>
public interface IDatabaseContext
{
/// <summary>
/// The <see cref="DatabaseType"/>.
/// </summary>
DatabaseType DatabaseType { get; }
/// <summary>
/// The <see cref="User"/>s in the <see cref="IDatabaseContext"/>
/// </summary>
@@ -89,19 +85,33 @@ namespace Tgstation.Server.Host.Database
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Save(CancellationToken cancellationToken);
/// <summary>
/// Attempts to delete all tables and drop the database in use.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task Drop(CancellationToken cancellationToken);
/// <summary>
/// Creates and migrates the <see cref="IDatabaseContext"/>
/// </summary>
/// <param name="logger">The <see cref="DatabaseContext"/> <see cref="ILogger"/> to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Initialize(CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the database should be seeded, <see langword="false"/> otherwise.</returns>
Task<bool> Migrate(ILogger<DatabaseContext> logger, CancellationToken cancellationToken);
/// <summary>
/// Attempt to downgrade the schema to the migration used for a given server <paramref name="version"/>
/// </summary>
/// <param name="logger">The <see cref="DatabaseContext"/> <see cref="ILogger"/> to use.</param>
/// <param name="version">The tgstation-server <see cref="Version"/> that the schema should downgrade for</param>
/// <param name="currentDatabaseType">The <see cref="DatabaseType"/> in use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken);
Task SchemaDowngradeForServerVersion(
ILogger<DatabaseContext> logger,
Version version,
DatabaseType currentDatabaseType,
CancellationToken cancellationToken);
}
}
@@ -1,35 +1,29 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Database
{
/// <summary>
/// For initially seeding a database
/// For initially setting up a database.
/// </summary>
interface IDatabaseSeeder
{
/// <summary>
/// Initially seed a given <paramref name="databaseContext"/>
/// Setup up a given <paramref name="databaseContext"/>.
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to seed</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
/// <summary>
/// Correct invalid database data caused by previous versions.
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to sanitize.</param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to setup.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
Task Initialize(IDatabaseContext databaseContext, CancellationToken cancellationToken);
/// <summary>
/// Changes the admin password in <see cref="IDatabaseContext"/> back to it's default and enables the account
/// Migrate a given <paramref name="databaseContext"/> down.
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to reset the admin password for</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken);
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to downgrade.</param>
/// <param name="downgradeVersion">The migration <see cref="Version"/> to downgrade the <paramref name="databaseContext"/> to.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task Downgrade(IDatabaseContext databaseContext, Version downgradeVersion, CancellationToken cancellationToken);
}
}
@@ -1,7 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MySql.Data.MySqlClient;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using System;
using Tgstation.Server.Host.Configuration;
@@ -20,42 +17,37 @@ namespace Tgstation.Server.Host.Database
/// Construct a <see cref="MySqlDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<MySqlDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions) : base(dbContextOptions)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
/// <summary>
/// Configure the <see cref="MySqlDatabaseContext"/>.
/// </summary>
/// <param name="options">The <see cref="DbContextOptionsBuilder"/> to configure.</param>
/// <param name="databaseConfiguration">The <see cref="DatabaseConfiguration"/>.</param>
public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration)
{
base.OnConfiguring(options);
var stringDeconstructor = new MySqlConnectionStringBuilder
{
ConnectionString = DatabaseConfiguration.ConnectionString
};
if (stringDeconstructor.Server == "localhost")
Logger.LogWarning("MariaDB/MySQL server address is set to 'localhost'! If there are connection issues, try setting it to '127.0.0.1'!");
if (options == null)
throw new ArgumentNullException(nameof(options));
if (databaseConfiguration == null)
throw new ArgumentNullException(nameof(databaseConfiguration));
if (databaseConfiguration.DatabaseType != DatabaseType.MariaDB && databaseConfiguration.DatabaseType != DatabaseType.MySql)
throw new InvalidOperationException($"Invalid DatabaseType for {nameof(MySqlDatabaseContext)}!");
options.UseMySql(
DatabaseConfiguration.ConnectionString,
databaseConfiguration.ConnectionString,
mySqlOptions =>
{
mySqlOptions.EnableRetryOnFailure();
if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion))
if (!String.IsNullOrEmpty(databaseConfiguration.ServerVersion))
mySqlOptions.ServerVersion(
Version.Parse(DatabaseConfiguration.ServerVersion),
DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB
Version.Parse(databaseConfiguration.ServerVersion),
databaseConfiguration.DatabaseType == DatabaseType.MariaDB
? ServerType.MariaDb
: ServerType.MySql);
});
}
/// <inheritdoc />
protected override void ValidateDatabaseType()
{
if (DatabaseType != DatabaseType.MariaDB && DatabaseType != DatabaseType.MySql)
throw new InvalidOperationException("Invalid DatabaseType for MySqlDatabaseContext!");
}
}
}
@@ -1,6 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using Tgstation.Server.Host.Configuration;
@@ -18,36 +16,34 @@ namespace Tgstation.Server.Host.Database
/// Construct a <see cref="SqlServerDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public PostgresSqlDatabaseContext(
DbContextOptions<PostgresSqlDatabaseContext> dbContextOptions,
IOptions<DatabaseConfiguration> databaseConfiguration,
IDatabaseSeeder databaseSeeder,
ILogger<PostgresSqlDatabaseContext> logger)
: base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
DbContextOptions<PostgresSqlDatabaseContext> dbContextOptions)
: base(dbContextOptions)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
/// <summary>
/// Configure the <see cref="PostgresSqlDatabaseContext"/>.
/// </summary>
/// <param name="options">The <see cref="DbContextOptionsBuilder"/> to configure.</param>
/// <param name="databaseConfiguration">The <see cref="DatabaseConfiguration"/>.</param>
public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration)
{
base.OnConfiguring(options);
options.UseNpgsql(DatabaseConfiguration.ConnectionString, options =>
if (options == null)
throw new ArgumentNullException(nameof(options));
if (databaseConfiguration == null)
throw new ArgumentNullException(nameof(databaseConfiguration));
if (databaseConfiguration.DatabaseType != DatabaseType.PostgresSql)
throw new InvalidOperationException($"Invalid DatabaseType for {nameof(PostgresSqlDatabaseContext)}!");
options.UseNpgsql(databaseConfiguration.ConnectionString, options =>
{
options.EnableRetryOnFailure();
if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion))
if (!String.IsNullOrEmpty(databaseConfiguration.ServerVersion))
options.SetPostgresVersion(
Version.Parse(DatabaseConfiguration.ServerVersion));
Version.Parse(databaseConfiguration.ServerVersion));
});
}
/// <inheritdoc />
protected override void ValidateDatabaseType()
{
if (DatabaseType != DatabaseType.PostgresSql)
throw new InvalidOperationException("Invalid DatabaseType for PostgresSqlDatabaseContext!");
}
}
}
@@ -1,6 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using Tgstation.Server.Host.Configuration;
@@ -15,24 +13,25 @@ namespace Tgstation.Server.Host.Database
/// Construct a <see cref="SqlServerDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqlServerDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions) : base(dbContextOptions)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
/// <summary>
/// Configure the <see cref="SqlServerDatabaseContext"/>.
/// </summary>
/// <param name="options">The <see cref="DbContextOptionsBuilder"/> to configure.</param>
/// <param name="databaseConfiguration">The <see cref="DatabaseConfiguration"/>.</param>
public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration)
{
base.OnConfiguring(options);
options.UseSqlServer(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure());
}
if (options == null)
throw new ArgumentNullException(nameof(options));
if (databaseConfiguration == null)
throw new ArgumentNullException(nameof(databaseConfiguration));
/// <inheritdoc />
protected override void ValidateDatabaseType()
{
if (DatabaseType != DatabaseType.SqlServer)
throw new InvalidOperationException("Invalid DatabaseType for SqlServerDatabaseContext!");
if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqlServerDatabaseContext)}!");
options.UseSqlServer(databaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure());
}
}
}
@@ -1,7 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Linq;
using Tgstation.Server.Host.Configuration;
@@ -9,25 +7,39 @@ using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Database
{
/// <summary>
/// <see cref="DatabaseContext"/> for Sqlite.
/// <see cref="DatabaseContext"/> for SQLite.
/// </summary>
sealed class SqliteDatabaseContext : DatabaseContext
{
/// <summary>
/// Static property to receive the configured value of <see cref="DatabaseConfiguration.DesignTime"/>.
/// </summary>
public static bool DesignTime { get; set; }
/// <summary>
/// Construct a <see cref="MySqlDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqliteDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions) : base(dbContextOptions)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
/// <summary>
/// Configure the <see cref="SqliteDatabaseContext"/>.
/// </summary>
/// <param name="options">The <see cref="DbContextOptionsBuilder"/> to configure.</param>
/// <param name="databaseConfiguration">The <see cref="DatabaseConfiguration"/>.</param>
public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration)
{
base.OnConfiguring(options);
options.UseSqlite(DatabaseConfiguration.ConnectionString);
if (options == null)
throw new ArgumentNullException(nameof(options));
if (databaseConfiguration == null)
throw new ArgumentNullException(nameof(databaseConfiguration));
if (databaseConfiguration.DatabaseType != DatabaseType.Sqlite)
throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqliteDatabaseContext)}!");
DesignTime = databaseConfiguration.DesignTime;
options.UseSqlite(databaseConfiguration.ConnectionString);
}
/// <inheritdoc />
@@ -43,7 +55,7 @@ namespace Tgstation.Server.Host.Database
// use the DateTimeOffsetToBinaryConverter
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
// This only supports millisecond precision, but should be sufficient for most use cases.
if (!DatabaseConfiguration.DesignTime)
if (!DesignTime)
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var properties = entityType
@@ -57,12 +69,5 @@ namespace Tgstation.Server.Host.Database
.HasConversion(new DateTimeOffsetToBinaryConverter());
}
}
/// <inheritdoc />
protected override void ValidateDatabaseType()
{
if (DatabaseType != DatabaseType.Sqlite)
throw new InvalidOperationException("Invalid DatabaseType for SqliteDatabaseContext!");
}
}
}