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}");
}
return services;
}
}
}

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,13 +12,15 @@ namespace IW4MAdmin.Application.Factories
/// </summary>
public class DatabaseContextFactory : IDatabaseContextFactory
{
private readonly IServiceProvider _serviceProvider;
public DatabaseContextFactory(IServiceProvider serviceProvider)
private readonly DbContextOptions _contextOptions;
private readonly string _activeProvider;
public DatabaseContextFactory(ApplicationConfiguration appConfig, DbContextOptions contextOptions)
{
_serviceProvider = serviceProvider;
_contextOptions = contextOptions;
_activeProvider = appConfig.DatabaseProvider?.ToLower();
}
/// <summary>
/// creates a new database context
/// </summary>
@ -25,10 +28,10 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns>
public DatabaseContext CreateContext(bool? enableTracking = true)
{
var context = _serviceProvider.GetRequiredService<DatabaseContext>();
var context = BuildContext();
enableTracking ??= true;
if (enableTracking.Value)
{
context.ChangeTracker.AutoDetectChangesEnabled = 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,33 +51,33 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}
}
public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config, IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup)
public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config,
IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup)
{
using (var ctx = contextFactory.CreateContext(enableTracking: false))
{
var dayInPast = DateTime.UtcNow.AddDays(-config.MostKillsMaxInactivityDays);
await using var ctx = contextFactory.CreateContext(enableTracking: false);
var dayInPast = DateTime.UtcNow.AddDays(-config.MostKillsMaxInactivityDays);
var iqStats = (from stats in ctx.Set<EFClientStatistics>()
join client in ctx.Clients
on stats.ClientId equals client.ClientId
join alias in ctx.Aliases
on client.CurrentAliasId equals alias.AliasId
where stats.ServerId == serverId
where client.Level != EFClient.Permission.Banned
where client.LastConnection >= dayInPast
orderby stats.Kills descending
select new
{
alias.Name,
stats.Kills
})
.Take(config.MostKillsClientLimit);
var iqStats = (from stats in ctx.Set<EFClientStatistics>()
join client in ctx.Clients
on stats.ClientId equals client.ClientId
join alias in ctx.Aliases
on client.CurrentAliasId equals alias.AliasId
where stats.ServerId == serverId
where client.Level != EFClient.Permission.Banned
where client.LastConnection >= dayInPast
orderby stats.Kills descending
select new
{
alias.Name,
stats.Kills
})
.Take(config.MostKillsClientLimit);
var iqList = await iqStats.ToListAsync();
var iqList = await iqStats.ToListAsync();
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"]);
}
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,121 +68,118 @@ 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)
.Where(r => r.Newest)
.Select(r => r.Performance)
.FirstOrDefaultAsync();
if (clientPerformance != 0)
{
var clientPerformance = await context.Set<EFRating>()
.Where(r => r.RatingHistory.ClientId == clientId)
.Where(r => r.ServerId == null)
.Where(r => r.Newest)
.Select(r => r.Performance)
.FirstOrDefaultAsync();
var iqClientRanking = context.Set<EFRating>()
.Where(r => r.RatingHistory.ClientId != clientId)
.Where(r => r.Performance > clientPerformance)
.Where(GetRankingFunc());
if (clientPerformance != 0)
{
var iqClientRanking = context.Set<EFRating>()
.Where(r => r.RatingHistory.ClientId != clientId)
.Where(r => r.Performance > clientPerformance)
.Where(GetRankingFunc());
return await iqClientRanking.CountAsync() + 1;
}
return 0;
return await iqClientRanking.CountAsync() + 1;
}
return 0;
}
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
{
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))
select new
{
rating.RatingHistory.ClientId,
rating.RatingHistory.Client.CurrentAlias.Name,
rating.RatingHistory.Client.LastConnection,
rating.Performance,
})
.OrderByDescending(c => c.Performance)
.Skip(start)
.Take(count);
// materialized list
var clientRatings = await iqClientRatings.ToListAsync();
// get all the unique client ids that are in the top stats
var clientIds = clientRatings
.GroupBy(r => r.ClientId)
.Select(r => r.First().ClientId)
.ToList();
var iqRatingInfo = from rating in context.Set<EFRating>()
where clientIds.Contains(rating.RatingHistory.ClientId)
where rating.ServerId == serverId
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))
select new
{
rating.Ranking,
rating.Performance,
rating.RatingHistory.ClientId,
rating.When
};
rating.RatingHistory.Client.CurrentAlias.Name,
rating.RatingHistory.Client.LastConnection,
rating.Performance,
})
.OrderByDescending(c => c.Performance)
.Skip(start)
.Take(count);
var ratingInfo = (await iqRatingInfo.ToListAsync())
.GroupBy(r => r.ClientId)
.Select(grp => new
{
grp.Key,
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
});
// materialized list
var clientRatings = await iqClientRatings.ToListAsync();
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
where clientIds.Contains(stat.ClientId)
where stat.Kills > 0 || stat.Deaths > 0
where serverId == null ? true : stat.ServerId == serverId
group stat by stat.ClientId into s
select new
{
ClientId = s.Key,
Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths),
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed),
});
var topPlayers = await iqStatsInfo.ToListAsync();
var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId);
var finished = topPlayers.Select(s => new TopStatsInfo()
{
ClientId = s.ClientId,
Id = (int?)serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
Name = clientRatingsDict[s.ClientId].Name,
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ?
ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When).Select(r => r.Performance).ToList() :
new List<double>() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance },
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
})
.OrderByDescending(r => r.Performance)
// get all the unique client ids that are in the top stats
var clientIds = clientRatings
.GroupBy(r => r.ClientId)
.Select(r => r.First().ClientId)
.ToList();
// set the ranking numerically
int i = start + 1;
foreach (var stat in finished)
{
stat.Ranking = i;
i++;
}
var iqRatingInfo = from rating in context.Set<EFRating>()
where clientIds.Contains(rating.RatingHistory.ClientId)
where rating.ServerId == serverId
select new
{
rating.Ranking,
rating.Performance,
rating.RatingHistory.ClientId,
rating.When
};
return finished;
var ratingInfo = (await iqRatingInfo.ToListAsync())
.GroupBy(r => r.ClientId)
.Select(grp => new
{
grp.Key,
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
});
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
where clientIds.Contains(stat.ClientId)
where stat.Kills > 0 || stat.Deaths > 0
where serverId == null ? true : stat.ServerId == serverId
group stat by stat.ClientId into s
select new
{
ClientId = s.Key,
Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths),
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed),
});
var topPlayers = await iqStatsInfo.ToListAsync();
var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId);
var finished = topPlayers.Select(s => new TopStatsInfo()
{
ClientId = s.ClientId,
Id = (int?)serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
Name = clientRatingsDict[s.ClientId].Name,
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ?
ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When).Select(r => r.Performance).ToList() :
new List<double>() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance },
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
})
.OrderByDescending(r => r.Performance)
.ToList();
// set the ranking numerically
int i = start + 1;
foreach (var stat in finished)
{
stat.Ranking = i;
i++;
}
return finished;
}
/// <summary>
@ -202,63 +199,61 @@ 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);
// the server might be using legacy server id
if (server == null)
{
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);
server = serverSet.FirstOrDefault(s => s.EndPoint == sv.ToString());
// the server might be using legacy server id
if (server == null)
if (server != null)
{
server = serverSet.FirstOrDefault(s => s.EndPoint == sv.ToString());
if (server != null)
{
// this provides a way to identify legacy server entries
server.EndPoint = sv.ToString();
ctx.Update(server);
ctx.SaveChanges();
}
}
// server has never been added before
if (server == null)
{
server = new EFServer()
{
Port = sv.Port,
EndPoint = sv.ToString(),
ServerId = serverId,
GameName = sv.GameName,
HostName = sv.Hostname
};
server = serverSet.Add(server).Entity;
// this doesn't need to be async as it's during initialization
// this provides a way to identify legacy server entries
server.EndPoint = sv.ToString();
ctx.Update(server);
ctx.SaveChanges();
}
}
// we want to set the gamename up if it's never been set, or it changed
else if (!server.GameName.HasValue || server.GameName.HasValue && server.GameName.Value != sv.GameName)
// server has never been added before
if (server == null)
{
server = new EFServer()
{
server.GameName = sv.GameName;
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
ctx.SaveChanges();
}
Port = sv.Port,
EndPoint = sv.ToString(),
ServerId = serverId,
GameName = sv.GameName,
HostName = sv.Hostname
};
if (server.HostName == null || server.HostName != sv.Hostname)
{
server.HostName = sv.Hostname;
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
ctx.SaveChanges();
}
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
server = serverSet.Add(server).Entity;
// this doesn't need to be async as it's during initialization
ctx.SaveChanges();
}
// we want to set the gamename up if it's never been set, or it changed
else if (!server.GameName.HasValue || server.GameName.HasValue && server.GameName.Value != sv.GameName)
{
server.GameName = sv.GameName;
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
ctx.SaveChanges();
}
if (server.HostName == null || server.HostName != sv.Hostname)
{
server.HostName = sv.Hostname;
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
ctx.SaveChanges();
}
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,79 +299,77 @@ 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)
.FirstOrDefault(c => c.ClientId == pl.ClientId && c.ServerId == serverId);
if (clientStats == null)
{
var clientStatsSet = ctx.Set<EFClientStatistics>();
clientStats = clientStatsSet
.Include(cl => cl.HitLocations)
.FirstOrDefault(c => c.ClientId == pl.ClientId && c.ServerId == serverId);
if (clientStats == null)
clientStats = new EFClientStatistics()
{
clientStats = new EFClientStatistics()
{
Active = true,
ClientId = pl.ClientId,
Deaths = 0,
Kills = 0,
ServerId = serverId,
Skill = 0.0,
SPM = 0.0,
EloRating = 200.0,
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>()
.Select(hl => new EFHitLocationCount()
{
Active = true,
HitCount = 0,
Location = hl
}).ToList()
};
// insert if they've not been added
clientStats = clientStatsSet.Add(clientStats).Entity;
await ctx.SaveChangesAsync();
}
pl.SetAdditionalProperty(CLIENT_STATS_KEY, clientStats);
// migration for previous existing stats
if (clientStats.HitLocations.Count == 0)
{
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation))
.OfType<IW4Info.HitLocation>()
Active = true,
ClientId = pl.ClientId,
Deaths = 0,
Kills = 0,
ServerId = serverId,
Skill = 0.0,
SPM = 0.0,
EloRating = 200.0,
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>()
.Select(hl => new EFHitLocationCount()
{
Active = true,
HitCount = 0,
Location = hl
})
.ToList();
}).ToList()
};
ctx.Update(clientStats);
await ctx.SaveChangesAsync();
}
// for stats before rating
if (clientStats.EloRating == 0.0)
{
clientStats.EloRating = clientStats.Skill;
}
if (clientStats.RollingWeightedKDR == 0)
{
clientStats.RollingWeightedKDR = clientStats.KDR;
}
// set these on connecting
clientStats.LastActive = DateTime.UtcNow;
clientStats.LastStatCalculation = DateTime.UtcNow;
clientStats.SessionScore = pl.Score;
clientStats.LastScore = pl.Score;
pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats));
_log.LogDebug("Added {client} to stats", pl.ToString());
// insert if they've not been added
clientStats = clientStatsSet.Add(clientStats).Entity;
await ctx.SaveChangesAsync();
}
pl.SetAdditionalProperty(CLIENT_STATS_KEY, clientStats);
// migration for previous existing stats
if (clientStats.HitLocations.Count == 0)
{
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation))
.OfType<IW4Info.HitLocation>()
.Select(hl => new EFHitLocationCount()
{
Active = true,
HitCount = 0,
Location = hl
})
.ToList();
ctx.Update(clientStats);
await ctx.SaveChangesAsync();
}
// for stats before rating
if (clientStats.EloRating == 0.0)
{
clientStats.EloRating = clientStats.Skill;
}
if (clientStats.RollingWeightedKDR == 0)
{
clientStats.RollingWeightedKDR = clientStats.KDR;
}
// set these on connecting
clientStats.LastActive = DateTime.UtcNow;
clientStats.LastStatCalculation = DateTime.UtcNow;
clientStats.SessionScore = pl.Score;
clientStats.LastScore = pl.Score;
pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats));
_log.LogDebug("Added {client} to stats", pl.ToString());
return clientStats;
}
@ -434,11 +427,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private async Task SaveClientStats(EFClientStatistics clientStats)
{
using (var ctx = _contextFactory.CreateContext())
{
ctx.Update(clientStats);
await ctx.SaveChangesAsync();
}
await using var ctx = _contextFactory.CreateContext();
ctx.Update(clientStats);
await ctx.SaveChangesAsync();
}
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
@ -628,13 +619,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task SaveHitCache(long serverId)
{
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
{
var server = _servers[serverId];
ctx.AddRange(server.HitCache.ToList());
await ctx.SaveChangesAsync();
server.HitCache.Clear();
}
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,14 +703,12 @@ 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))
{
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
{
ctx.Add(change);
}
await ctx.SaveChangesAsync();
ctx.Add(change);
}
await ctx.SaveChangesAsync();
}
public async Task AddStandardKill(EFClient attacker, EFClient victim)
@ -826,160 +813,158 @@ 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)
where history.ClientId == client.ClientId
select history;
// get the client ratings
var clientHistory = await iqHistoryLink
.FirstOrDefaultAsync() ?? new EFClientRatingHistory()
{
Active = true,
ClientId = client.ClientId,
Ratings = new List<EFRating>()
};
// it's the first time they've played
if (clientHistory.RatingHistoryId == 0)
{
// select the rating history for client
var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>()
.Include(h => h.Ratings)
where history.ClientId == client.ClientId
select history;
// get the client ratings
var clientHistory = await iqHistoryLink
.FirstOrDefaultAsync() ?? new EFClientRatingHistory()
{
Active = true,
ClientId = client.ClientId,
Ratings = new List<EFRating>()
};
// it's the first time they've played
if (clientHistory.RatingHistoryId == 0)
{
ctx.Add(clientHistory);
}
#region INDIVIDUAL_SERVER_PERFORMANCE
// get the client ranking for the current server
int individualClientRanking = await ctx.Set<EFRating>()
.Where(GetRankingFunc(clientStats.ServerId))
// ignore themselves in the query
.Where(c => c.RatingHistory.ClientId != client.ClientId)
.Where(c => c.Performance > clientStats.Performance)
.CountAsync() + 1;
// limit max history per server to 40
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
{
// select the oldest one
var ratingToRemove = clientHistory.Ratings
.Where(r => r.ServerId == clientStats.ServerId)
.OrderBy(r => r.When)
.First();
ctx.Remove(ratingToRemove);
}
// set the previous newest to false
var ratingToUnsetNewest = clientHistory.Ratings
.Where(r => r.ServerId == clientStats.ServerId)
.OrderByDescending(r => r.When)
.FirstOrDefault();
if (ratingToUnsetNewest != null)
{
if (ratingToUnsetNewest.Newest)
{
ctx.Update(ratingToUnsetNewest);
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
ratingToUnsetNewest.Newest = false;
}
}
var newServerRating = new EFRating()
{
Performance = clientStats.Performance,
Ranking = individualClientRanking,
Active = true,
Newest = true,
ServerId = clientStats.ServerId,
RatingHistory = clientHistory,
ActivityAmount = currentServerTotalPlaytime,
};
// add new rating for current server
ctx.Add(newServerRating);
#endregion
#region OVERALL_RATING
// select all performance & time played for current client
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
where stats.ClientId == client.ClientId
where stats.ServerId != clientStats.ServerId
select new
{
stats.Performance,
stats.TimePlayed
};
var clientStatsList = await iqClientStats.ToListAsync();
// add the current server's so we don't have to pull it from the database
clientStatsList.Add(new
{
clientStats.Performance,
TimePlayed = currentServerTotalPlaytime
});
// weight the overall performance based on play time
double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
// shouldn't happen but just in case the sum of time played is 0
if (double.IsNaN(performanceAverage))
{
performanceAverage = clientStatsList.Average(p => p.Performance);
}
int overallClientRanking = await ctx.Set<EFRating>()
.Where(GetRankingFunc())
.Where(r => r.RatingHistory.ClientId != client.ClientId)
.Where(r => r.Performance > performanceAverage)
.CountAsync() + 1;
// limit max average history to 40
if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40)
{
var ratingToRemove = clientHistory.Ratings
.Where(r => r.ServerId == null)
.OrderBy(r => r.When)
.First();
ctx.Remove(ratingToRemove);
}
// set the previous average newest to false
ratingToUnsetNewest = clientHistory.Ratings
.Where(r => r.ServerId == null)
.OrderByDescending(r => r.When)
.FirstOrDefault();
if (ratingToUnsetNewest != null)
{
if (ratingToUnsetNewest.Newest)
{
ctx.Update(ratingToUnsetNewest);
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
ratingToUnsetNewest.Newest = false;
}
}
// add new average rating
var averageRating = new EFRating()
{
Active = true,
Newest = true,
Performance = performanceAverage,
Ranking = overallClientRanking,
ServerId = null,
RatingHistory = clientHistory,
ActivityAmount = clientStatsList.Sum(s => s.TimePlayed)
};
ctx.Add(averageRating);
#endregion
await ctx.SaveChangesAsync();
ctx.Add(clientHistory);
}
#region INDIVIDUAL_SERVER_PERFORMANCE
// get the client ranking for the current server
int individualClientRanking = await ctx.Set<EFRating>()
.Where(GetRankingFunc(clientStats.ServerId))
// ignore themselves in the query
.Where(c => c.RatingHistory.ClientId != client.ClientId)
.Where(c => c.Performance > clientStats.Performance)
.CountAsync() + 1;
// limit max history per server to 40
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
{
// select the oldest one
var ratingToRemove = clientHistory.Ratings
.Where(r => r.ServerId == clientStats.ServerId)
.OrderBy(r => r.When)
.First();
ctx.Remove(ratingToRemove);
}
// set the previous newest to false
var ratingToUnsetNewest = clientHistory.Ratings
.Where(r => r.ServerId == clientStats.ServerId)
.OrderByDescending(r => r.When)
.FirstOrDefault();
if (ratingToUnsetNewest != null)
{
if (ratingToUnsetNewest.Newest)
{
ctx.Update(ratingToUnsetNewest);
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
ratingToUnsetNewest.Newest = false;
}
}
var newServerRating = new EFRating()
{
Performance = clientStats.Performance,
Ranking = individualClientRanking,
Active = true,
Newest = true,
ServerId = clientStats.ServerId,
RatingHistory = clientHistory,
ActivityAmount = currentServerTotalPlaytime,
};
// add new rating for current server
ctx.Add(newServerRating);
#endregion
#region OVERALL_RATING
// select all performance & time played for current client
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
where stats.ClientId == client.ClientId
where stats.ServerId != clientStats.ServerId
select new
{
stats.Performance,
stats.TimePlayed
};
var clientStatsList = await iqClientStats.ToListAsync();
// add the current server's so we don't have to pull it from the database
clientStatsList.Add(new
{
clientStats.Performance,
TimePlayed = currentServerTotalPlaytime
});
// weight the overall performance based on play time
double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
// shouldn't happen but just in case the sum of time played is 0
if (double.IsNaN(performanceAverage))
{
performanceAverage = clientStatsList.Average(p => p.Performance);
}
int overallClientRanking = await ctx.Set<EFRating>()
.Where(GetRankingFunc())
.Where(r => r.RatingHistory.ClientId != client.ClientId)
.Where(r => r.Performance > performanceAverage)
.CountAsync() + 1;
// limit max average history to 40
if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40)
{
var ratingToRemove = clientHistory.Ratings
.Where(r => r.ServerId == null)
.OrderBy(r => r.When)
.First();
ctx.Remove(ratingToRemove);
}
// set the previous average newest to false
ratingToUnsetNewest = clientHistory.Ratings
.Where(r => r.ServerId == null)
.OrderByDescending(r => r.When)
.FirstOrDefault();
if (ratingToUnsetNewest != null)
{
if (ratingToUnsetNewest.Newest)
{
ctx.Update(ratingToUnsetNewest);
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
ratingToUnsetNewest.Newest = false;
}
}
// add new average rating
var averageRating = new EFRating()
{
Active = true,
Newest = true,
Performance = performanceAverage,
Ranking = overallClientRanking,
ServerId = null,
RatingHistory = clientHistory,
ActivityAmount = clientStatsList.Sum(s => s.TimePlayed)
};
ctx.Add(averageRating);
#endregion
await ctx.SaveChangesAsync();
}
/// <summary>
@ -1137,25 +1122,23 @@ 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);
if (serverStats == null)
{
var serverStatsSet = ctx.Set<EFServerStatistics>();
serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId);
if (serverStats == null)
_log.LogDebug("Initializing server stats for {serverId}", serverId);
// server stats have never been generated before
serverStats = new EFServerStatistics()
{
_log.LogDebug("Initializing server stats for {serverId}", serverId);
// server stats have never been generated before
serverStats = new EFServerStatistics()
{
ServerId = serverId,
TotalKills = 0,
TotalPlayTime = 0,
};
ServerId = serverId,
TotalKills = 0,
TotalPlayTime = 0,
};
serverStats = serverStatsSet.Add(serverStats).Entity;
ctx.SaveChanges();
}
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())
{
var serverStatsSet = ctx.Set<EFServerStatistics>();
serverStatsSet.Update(_servers[serverId].ServerStatistics);
await ctx.SaveChangesAsync();
}
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))
{
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
}
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))
{
clientStats = await ctx.Set<EFClientStatistics>()
.Include(c => c.HitLocations)
.Where(c => c.ClientId == request.ClientId)
.ToListAsync();
}
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,34 +22,32 @@ namespace SharedLibraryCore.Repositories
/// <inheritdoc/>
public async Task<IList<AuditInfo>> ListAuditInformation(PaginationRequest paginationInfo)
{
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
join originClient in ctx.Clients
on (change.ImpersonationEntityId ?? change.OriginEntityId) equals originClient.ClientId
join targetClient in ctx.Clients
on change.TargetEntityId equals targetClient.ClientId
into targetChange
from targetClient in targetChange.DefaultIfEmpty()
select new AuditInfo()
{
Action = change.TypeOfChange.ToString(),
OriginName = originClient.CurrentAlias.Name,
OriginId = originClient.ClientId,
TargetName = targetClient == null ? "" : targetClient.CurrentAlias.Name,
TargetId = targetClient == null ? new int?() : targetClient.ClientId,
When = change.TimeChanged,
Data = change.Comment,
OldValue = change.PreviousValue,
NewValue = change.CurrentValue
})
.Skip(paginationInfo.Offset)
.Take(paginationInfo.Count);
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
join originClient in ctx.Clients
on (change.ImpersonationEntityId ?? change.OriginEntityId) equals originClient.ClientId
join targetClient in ctx.Clients
on change.TargetEntityId equals targetClient.ClientId
into targetChange
from targetClient in targetChange.DefaultIfEmpty()
select new AuditInfo()
{
Action = change.TypeOfChange.ToString(),
OriginName = originClient.CurrentAlias.Name,
OriginId = originClient.ClientId,
TargetName = targetClient == null ? "" : targetClient.CurrentAlias.Name,
TargetId = targetClient == null ? new int?() : targetClient.ClientId,
When = change.TimeChanged,
Data = change.Comment,
OldValue = change.PreviousValue,
NewValue = change.CurrentValue
})
.Skip(paginationInfo.Offset)
.Take(paginationInfo.Count);
return await iqItems.ToListAsync();
}
return await iqItems.ToListAsync();
}
}
}