fix memory leak issue related to AddDbContext not working as expected

This commit is contained in:
RaidMax 2020-11-29 16:01:52 -06:00
parent b2d282d412
commit bd3f0caf60
18 changed files with 499 additions and 505 deletions

View File

@ -48,10 +48,12 @@ namespace IW4MAdmin.Application.Extensions
return services;
}
public static IServiceCollection AddDatabaseContext(this IServiceCollection services,
public static IServiceCollection AddDatabaseContextOptions(this IServiceCollection services,
ApplicationConfiguration appConfig)
{
if (string.IsNullOrEmpty(appConfig.ConnectionString) || appConfig.DatabaseProvider == "sqlite")
var activeProvider = appConfig.DatabaseProvider?.ToLower();
if (string.IsNullOrEmpty(appConfig.ConnectionString) || activeProvider == "sqlite")
{
var currentPath = Utilities.OperatingDirectory;
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
@ -62,31 +64,34 @@ namespace IW4MAdmin.Application.Extensions
{DataSource = Path.Join(currentPath, "Database", "Database.db")};
var connectionString = connectionStringBuilder.ToString();
services.AddDbContext<DatabaseContext, SqliteDatabaseContext>(options =>
options.UseSqlite(connectionString), ServiceLifetime.Transient);
var builder = new DbContextOptionsBuilder<SqliteDatabaseContext>()
.UseSqlite(connectionString);
services.AddSingleton((DbContextOptions) builder.Options);
return services;
}
switch (appConfig.DatabaseProvider)
switch (activeProvider)
{
case "mysql":
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
StringComparison.InvariantCultureIgnoreCase);
services.AddDbContext<DatabaseContext, MySqlDatabaseContext>(options =>
options.UseMySql(
appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
mysqlOptions => mysqlOptions.EnableRetryOnFailure()), ServiceLifetime.Transient);
break;
var mysqlBuilder = new DbContextOptionsBuilder<MySqlDatabaseContext>()
.UseMySql(appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
mysqlOptions => mysqlOptions.EnableRetryOnFailure());
services.AddSingleton((DbContextOptions) mysqlBuilder.Options);
return services;
case "postgresql":
appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout",
StringComparison.InvariantCultureIgnoreCase);
services.AddDbContext<DatabaseContext, PostgresqlDatabaseContext>(options =>
options.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
postgresqlOptions => postgresqlOptions.EnableRetryOnFailure()), ServiceLifetime.Transient);
break;
}
var postgresqlBuilder = new DbContextOptionsBuilder<PostgresqlDatabaseContext>()
.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
postgresqlOptions => postgresqlOptions.EnableRetryOnFailure());
services.AddSingleton((DbContextOptions) postgresqlBuilder.Options);
return services;
default:
throw new ArgumentException($"No context available for {appConfig.DatabaseProvider}");
}
}
}
}

View File

@ -1,7 +1,8 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.MigrationContext;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories
@ -11,11 +12,13 @@ namespace IW4MAdmin.Application.Factories
/// </summary>
public class DatabaseContextFactory : IDatabaseContextFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly DbContextOptions _contextOptions;
private readonly string _activeProvider;
public DatabaseContextFactory(IServiceProvider serviceProvider)
public DatabaseContextFactory(ApplicationConfiguration appConfig, DbContextOptions contextOptions)
{
_serviceProvider = serviceProvider;
_contextOptions = contextOptions;
_activeProvider = appConfig.DatabaseProvider?.ToLower();
}
/// <summary>
@ -25,7 +28,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns>
public DatabaseContext CreateContext(bool? enableTracking = true)
{
var context = _serviceProvider.GetRequiredService<DatabaseContext>();
var context = BuildContext();
enableTracking ??= true;
@ -44,5 +47,16 @@ namespace IW4MAdmin.Application.Factories
return context;
}
private DatabaseContext BuildContext()
{
return _activeProvider switch
{
"sqlite" => new SqliteDatabaseContext(_contextOptions),
"mysql" => new MySqlDatabaseContext(_contextOptions),
"postgresql" => new PostgresqlDatabaseContext(_contextOptions),
_ => throw new ArgumentException($"No context found for {_activeProvider}")
};
}
}
}

View File

@ -361,7 +361,7 @@ namespace IW4MAdmin.Application
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.AddSingleton(translationLookup)
.AddDatabaseContext(appConfig);
.AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents"))
{

View File

@ -28,7 +28,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId)

View File

@ -31,7 +31,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
using var ctx = _contextFactory.CreateContext(enableTracking: false);
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId)

View File

@ -28,7 +28,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
var iqAliasUpdates = ctx.Aliases

View File

@ -37,7 +37,7 @@ namespace IW4MAdmin.Application.Misc
return;
}
using var ctx = _contextFactory.CreateContext();
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
@ -66,7 +66,7 @@ namespace IW4MAdmin.Application.Misc
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)

View File

@ -2,7 +2,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore;
using IW4MAdmin.Plugins.Stats.Models;
using System.Collections.Generic;
@ -19,7 +18,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
private readonly IDatabaseContextFactory _contextFactory;
private readonly CommandConfiguration _config;
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) : base(config, translationLookup)
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{
Name = "mostkills";
Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_DESC"];
@ -32,7 +32,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
public override async Task ExecuteAsync(GameEvent E)
{
var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(), _contextFactory, _translationLookup);
var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(),
_contextFactory, _translationLookup);
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{
foreach (var stat in mostKills)
@ -50,10 +51,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}
}
public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config, IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup)
{
using (var ctx = contextFactory.CreateContext(enableTracking: false))
public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config,
IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup)
{
await using var ctx = contextFactory.CreateContext(enableTracking: false);
var dayInPast = DateTime.UtcNow.AddDays(-config.MostKillsMaxInactivityDays);
var iqStats = (from stats in ctx.Set<EFClientStatistics>()
@ -74,9 +75,9 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var iqList = await iqStats.ToListAsync();
return iqList.Select((stats, index) => translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_FORMAT"].FormatExt(index + 1, stats.Name, stats.Kills))
return iqList.Select((stats, index) => translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_FORMAT"]
.FormatExt(index + 1, stats.Name, stats.Kills))
.Prepend(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTKILLS_HEADER"]);
}
}
}
}

View File

@ -68,8 +68,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <returns></returns>
public async Task<int> GetClientOverallRanking(int clientId)
{
using (var context = _contextFactory.CreateContext(enableTracking: false))
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
var clientPerformance = await context.Set<EFRating>()
.Where(r => r.RatingHistory.ClientId == clientId)
.Where(r => r.ServerId == null)
@ -89,12 +89,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 0;
}
}
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
{
using (var context = _contextFactory.CreateContext(enableTracking: false))
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
// setup the query for the clients within the given rating range
var iqClientRatings = (from rating in context.Set<EFRating>()
.Where(GetRankingFunc(serverId))
@ -183,7 +181,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return finished;
}
}
/// <summary>
/// Add a server to the StatManager server pool
@ -202,8 +199,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
long serverId = GetIdForServer(sv);
EFServer server;
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
var serverSet = ctx.Set<EFServer>();
// get the server from the database if it exists, otherwise create and insert a new one
server = serverSet.FirstOrDefault(s => s.ServerId == serverId);
@ -257,7 +253,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
ctx.SaveChanges();
}
// check to see if the stats have ever been initialized
var serverStats = InitializeServerStats(server.ServerId);
@ -304,8 +299,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
EFClientStatistics clientStats;
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
{
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var clientStatsSet = ctx.Set<EFClientStatistics>();
clientStats = clientStatsSet
.Include(cl => cl.HitLocations)
@ -375,7 +369,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats));
_log.LogDebug("Added {client} to stats", pl.ToString());
}
return clientStats;
}
@ -434,12 +427,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private async Task SaveClientStats(EFClientStatistics clientStats)
{
using (var ctx = _contextFactory.CreateContext())
{
await using var ctx = _contextFactory.CreateContext();
ctx.Update(clientStats);
await ctx.SaveChangesAsync();
}
}
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
{
@ -628,14 +619,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task SaveHitCache(long serverId)
{
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
{
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var server = _servers[serverId];
ctx.AddRange(server.HitCache.ToList());
await ctx.SaveChangesAsync();
server.HitCache.Clear();
}
}
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{
@ -714,15 +703,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
EFACSnapshot change;
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
{
await using var ctx = _contextFactory.CreateContext();
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
{
ctx.Add(change);
}
await ctx.SaveChangesAsync();
}
}
public async Task AddStandardKill(EFClient attacker, EFClient victim)
{
@ -826,8 +813,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
using (var ctx = _contextFactory.CreateContext(enableTracking: true))
{
await using var ctx = _contextFactory.CreateContext(enableTracking: true);
// select the rating history for client
var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>()
.Include(h => h.Ratings)
@ -980,7 +966,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await ctx.SaveChangesAsync();
}
}
/// <summary>
/// Performs the incrementation of kills and deaths for client statistics
@ -1137,8 +1122,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
EFServerStatistics serverStats;
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
var serverStatsSet = ctx.Set<EFServerStatistics>();
serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId);
@ -1156,7 +1140,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
serverStats = serverStatsSet.Add(serverStats).Entity;
ctx.SaveChanges();
}
}
return serverStats;
}
@ -1216,12 +1199,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
await waiter.WaitAsync();
using (var ctx = _contextFactory.CreateContext())
{
await using var ctx = _contextFactory.CreateContext();
var serverStatsSet = ctx.Set<EFServerStatistics>();
serverStatsSet.Update(_servers[serverId].ServerStatistics);
await ctx.SaveChangesAsync();
}
foreach (var stats in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))

View File

@ -26,7 +26,7 @@ namespace Stats.Helpers
public async Task<ResourceQueryHelperResult<StatsInfoResult>> QueryResource(StatsInfoRequest query)
{
var result = new ResourceQueryHelperResult<StatsInfoResult>();
using var context = _contextFactory.CreateContext(enableTracking: false);
await using var context = _contextFactory.CreateContext(enableTracking: false);
// we need to get the ratings separately because there's not explicit FK
var ratings = await context.Set<EFClientRatingHistory>()

View File

@ -173,11 +173,9 @@ namespace IW4MAdmin.Plugins.Stats
{
IList<EFClientStatistics> clientStats;
int messageCount = 0;
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
{
await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
}
int kills = clientStats.Sum(c => c.Kills);
int deaths = clientStats.Sum(c => c.Deaths);
@ -252,13 +250,11 @@ namespace IW4MAdmin.Plugins.Stats
{
IList<EFClientStatistics> clientStats;
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
{
await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
clientStats = await ctx.Set<EFClientStatistics>()
.Include(c => c.HitLocations)
.Where(c => c.ClientId == request.ClientId)
.ToListAsync();
}
double headRatio = 0;
double chestRatio = 0;

View File

@ -41,7 +41,7 @@ namespace StatsWeb
}
var result = new ResourceQueryHelperResult<MessageResponse>();
using var context = _contextFactory.CreateContext(enableTracking: false);
await using var context = _contextFactory.CreateContext(enableTracking: false);
if (serverCache == null)
{

View File

@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database
{
public static async Task Seed(IDatabaseContextFactory contextFactory, CancellationToken token)
{
var context = contextFactory.CreateContext();
await using var context = contextFactory.CreateContext();
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{

View File

@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database.MigrationContext
}
}
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> options) : base(options)
public MySqlDatabaseContext(DbContextOptions options) : base(options)
{
}

View File

@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database.MigrationContext
}
}
public PostgresqlDatabaseContext(DbContextOptions<PostgresqlDatabaseContext> options) : base(options)
public PostgresqlDatabaseContext(DbContextOptions options) : base(options)
{
}

View File

@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database.MigrationContext
}
}
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> options) : base(options)
public SqliteDatabaseContext(DbContextOptions options) : base(options)
{
}

View File

@ -91,12 +91,11 @@ namespace SharedLibraryCore.Database.Models
SetAdditionalProperty("_reportCount", 0);
ReceivedPenalties = new List<EFPenalty>();
_processingEvent = new SemaphoreSlim(1, 1);
}
~EFClient()
{
_processingEvent.Dispose();
_processingEvent?.Dispose();
}
public override string ToString()

View File

@ -22,8 +22,7 @@ namespace SharedLibraryCore.Repositories
/// <inheritdoc/>
public async Task<IList<AuditInfo>> ListAuditInformation(PaginationRequest paginationInfo)
{
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
{
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqItems = (from change in ctx.EFChangeHistory
where change.TypeOfChange != Database.Models.EFChangeHistory.ChangeType.Ban
orderby change.TimeChanged descending
@ -52,4 +51,3 @@ namespace SharedLibraryCore.Repositories
}
}
}
}