From 92992dfb1325ae2d34d97b6c1690796f052bf0c6 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Wed, 19 Apr 2023 19:55:33 -0500 Subject: [PATCH] update top level client count stats to support filtering per game --- Application/Misc/ServerDataViewer.cs | 70 +++++++++++----- Data/Abstractions/IDataValueCache.cs | 5 +- Data/Helpers/DataValueCache.cs | 83 ++++++++++--------- SharedLibraryCore/Dtos/IW4MAdminInfo.cs | 8 +- .../Interfaces/IServerDataViewer.cs | 7 +- WebfrontCore/Controllers/API/Info.cs | 5 +- WebfrontCore/Controllers/HomeController.cs | 23 +++-- .../ViewComponents/ServerListViewComponent.cs | 12 +-- 8 files changed, 126 insertions(+), 87 deletions(-) diff --git a/Application/Misc/ServerDataViewer.cs b/Application/Misc/ServerDataViewer.cs index e174f9ccf..b36222a7c 100644 --- a/Application/Misc/ServerDataViewer.cs +++ b/Application/Misc/ServerDataViewer.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Data.Abstractions; +using Data.Models; using Data.Models.Client; using Data.Models.Client.Stats; using Data.Models.Server; @@ -40,21 +41,31 @@ namespace IW4MAdmin.Application.Misc } public async Task<(int?, DateTime?)> - MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, + MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null, CancellationToken token = default) { - _snapshotCache.SetCacheItem(async (snapshots, cancellationToken) => + _snapshotCache.SetCacheItem(async (snapshots, ids, cancellationToken) => { + Reference.Game? game = null; + long? id = null; + + if (ids.Any()) + { + game = (Reference.Game?)ids.First(); + id = (long?)ids.Last(); + } + var oldestEntry = overPeriod.HasValue ? DateTime.UtcNow - overPeriod.Value : DateTime.UtcNow.AddDays(-1); - + int? maxClients; DateTime? maxClientsTime; - if (serverId != null) + if (id != null) { - var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId) + var clients = await snapshots.Where(snapshot => snapshot.ServerId == id) + .Where(snapshot => game == null || snapshot.Server.GameName == game) .Where(snapshot => snapshot.CapturedAt >= oldestEntry) .OrderByDescending(snapshot => snapshot.ClientCount) .Select(snapshot => new @@ -71,15 +82,16 @@ namespace IW4MAdmin.Application.Misc else { var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry) + .Where(snapshot => game == null || snapshot.Server.GameName == game) .GroupBy(snapshot => snapshot.PeriodBlock) .Select(grp => new { - ClientCount = grp.Sum(snapshot => (int?) snapshot.ClientCount), - Time = grp.Max(snapshot => (DateTime?) snapshot.CapturedAt) + ClientCount = grp.Sum(snapshot => (int?)snapshot.ClientCount), + Time = grp.Max(snapshot => (DateTime?)snapshot.CapturedAt) }) .OrderByDescending(snapshot => snapshot.ClientCount) .FirstOrDefaultAsync(cancellationToken); - + maxClients = clients?.ClientCount; maxClientsTime = clients?.Time; } @@ -87,11 +99,12 @@ namespace IW4MAdmin.Application.Misc _logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients); return (maxClients, maxClientsTime); - }, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true); + }, nameof(MaxConcurrentClientsAsync), new object[] { gameCode, serverId }, _cacheTimeSpan, true); try { - return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token); + return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), + new object[] { gameCode, serverId }, token); } catch (Exception ex) { @@ -100,22 +113,30 @@ namespace IW4MAdmin.Application.Misc } } - public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default) + public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default) { - _serverStatsCache.SetCacheItem(async (set, cancellationToken) => + _serverStatsCache.SetCacheItem(async (set, ids, cancellationToken) => { - var count = await set.CountAsync(cancellationToken); + Reference.Game? game = null; + + if (ids.Any()) + { + game = (Reference.Game?)ids.First(); + } + + var count = await set.CountAsync(item => game == null || item.GameName == game, + cancellationToken); var startOfPeriod = DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24); - var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod, + var recentCount = await set.CountAsync(client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod, cancellationToken); return (count, recentCount); - }, nameof(_serverStatsCache), _cacheTimeSpan, true); + }, nameof(_serverStatsCache), new object[] { gameCode }, _cacheTimeSpan, true); try { - return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token); + return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), new object[] { gameCode }, token); } catch (Exception ex) { @@ -166,21 +187,28 @@ namespace IW4MAdmin.Application.Misc public async Task RankedClientsCountAsync(long? serverId = null, CancellationToken token = default) { - _rankedClientsCache.SetCacheItem(async (set, cancellationToken) => + _rankedClientsCache.SetCacheItem((set, ids, cancellationToken) => { + long? id = null; + + if (ids.Any()) + { + id = (long?)ids.First(); + } + var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); - return await set + return set .Where(rating => rating.Newest) - .Where(rating => rating.ServerId == serverId) + .Where(rating => rating.ServerId == id) .Where(rating => rating.CreatedDateTime >= fifteenDaysAgo) .Where(rating => rating.Client.Level != EFClient.Permission.Banned) .Where(rating => rating.Ranking != null) .CountAsync(cancellationToken); - }, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan); + }, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan); try { - return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token); + return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token); } catch (Exception ex) { diff --git a/Data/Abstractions/IDataValueCache.cs b/Data/Abstractions/IDataValueCache.cs index 12c626b9e..327852fd3 100644 --- a/Data/Abstractions/IDataValueCache.cs +++ b/Data/Abstractions/IDataValueCache.cs @@ -11,10 +11,11 @@ namespace Data.Abstractions void SetCacheItem(Func, CancellationToken, Task> itemGetter, string keyName, TimeSpan? expirationTime = null, bool autoRefresh = false); - void SetCacheItem(Func, CancellationToken, Task> itemGetter, string keyName, + void SetCacheItem(Func, IEnumerable, CancellationToken, Task> itemGetter, string keyName, IEnumerable ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false); Task GetCacheItem(string keyName, CancellationToken token = default); - Task GetCacheItem(string keyName, object id = null, CancellationToken token = default); + + Task GetCacheItem(string keyName, IEnumerable ids = null, CancellationToken token = default); } } diff --git a/Data/Helpers/DataValueCache.cs b/Data/Helpers/DataValueCache.cs index ec4979acb..182720819 100644 --- a/Data/Helpers/DataValueCache.cs +++ b/Data/Helpers/DataValueCache.cs @@ -18,7 +18,7 @@ namespace Data.Helpers private readonly IDatabaseContextFactory _contextFactory; private readonly ConcurrentDictionary>> _cacheStates = new(); - private readonly object _defaultKey = new(); + private readonly string _defaultKey = null; private bool _autoRefresh; private const int DefaultExpireMinutes = 15; @@ -29,7 +29,7 @@ namespace Data.Helpers public string Key { get; set; } public DateTime LastRetrieval { get; set; } public TimeSpan ExpirationTime { get; set; } - public Func, CancellationToken, Task> Getter { get; set; } + public Func, IEnumerable, CancellationToken, Task> Getter { get; set; } public TCacheType Value { get; set; } public bool IsSet { get; set; } @@ -53,60 +53,58 @@ namespace Data.Helpers public void SetCacheItem(Func, CancellationToken, Task> getter, string key, TimeSpan? expirationTime = null, bool autoRefresh = false) { - SetCacheItem(getter, key, null, expirationTime, autoRefresh); + SetCacheItem((set, _, token) => getter(set, token), key, null, expirationTime, autoRefresh); } - public void SetCacheItem(Func, CancellationToken, Task> getter, string key, + public void SetCacheItem(Func, IEnumerable, CancellationToken, Task> getter, string key, IEnumerable ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false) { ids ??= new[] { _defaultKey }; - + if (!_cacheStates.ContainsKey(key)) { _cacheStates.TryAdd(key, new Dictionary>()); } - foreach (var id in ids) + var cacheInstance = _cacheStates[key]; + var id = GenerateKeyFromIds(ids); + + lock (_cacheStates) { - var cacheInstance = _cacheStates[key]; - - lock (_cacheStates) - { - if (cacheInstance.ContainsKey(id)) - { - continue; - } - } - - var state = new CacheState - { - Key = key, - Getter = getter, - ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes) - }; - - lock (_cacheStates) - { - cacheInstance.Add(id, state); - } - - _autoRefresh = autoRefresh; - - if (!_autoRefresh || expirationTime == TimeSpan.MaxValue) + if (cacheInstance.ContainsKey(id)) { return; } - - _timer = new Timer(state.ExpirationTime.TotalMilliseconds); - _timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None); - _timer.Start(); } + + var state = new CacheState + { + Key = key, + Getter = getter, + ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes) + }; + + lock (_cacheStates) + { + cacheInstance.Add(id, state); + } + + _autoRefresh = autoRefresh; + + if (!_autoRefresh || expirationTime == TimeSpan.MaxValue) + { + return; + } + + _timer = new Timer(state.ExpirationTime.TotalMilliseconds); + _timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, ids, CancellationToken.None); + _timer.Start(); } public Task GetCacheItem(string keyName, CancellationToken cancellationToken = default) => GetCacheItem(keyName, null, cancellationToken); - public async Task GetCacheItem(string keyName, object id = null, + public async Task GetCacheItem(string keyName, IEnumerable ids = null, CancellationToken cancellationToken = default) { if (!_cacheStates.ContainsKey(keyName)) @@ -120,27 +118,27 @@ namespace Data.Helpers lock (_cacheStates) { - state = id is null ? cacheInstance.Values.First() : _cacheStates[keyName][id]; + state = ids is null ? cacheInstance.Values.First() : _cacheStates[keyName][GenerateKeyFromIds(ids)]; } // when auto refresh is off we want to check the expiration and value // when auto refresh is on, we want to only check the value, because it'll be refreshed automatically if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet) { - await RunCacheUpdate(state, cancellationToken); + await RunCacheUpdate(state, ids, cancellationToken); } return state.Value; } - - private async Task RunCacheUpdate(CacheState state, CancellationToken token) + + private async Task RunCacheUpdate(CacheState state, IEnumerable ids, CancellationToken token) { try { _logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state); await using var context = _contextFactory.CreateContext(false); var set = context.Set(); - var value = await state.Getter(set, token); + var value = await state.Getter(set, ids, token); state.Value = value; state.IsSet = true; state.LastRetrieval = DateTime.Now; @@ -150,5 +148,8 @@ namespace Data.Helpers _logger.LogError(ex, "Could not get cached value for {Key}", state.Key); } } + + private static string GenerateKeyFromIds(IEnumerable ids) => + string.Join("_", ids.Select(id => id?.ToString() ?? "null")); } } diff --git a/SharedLibraryCore/Dtos/IW4MAdminInfo.cs b/SharedLibraryCore/Dtos/IW4MAdminInfo.cs index bcb481a6b..03ce4e2b3 100644 --- a/SharedLibraryCore/Dtos/IW4MAdminInfo.cs +++ b/SharedLibraryCore/Dtos/IW4MAdminInfo.cs @@ -1,5 +1,5 @@ using System; -using static SharedLibraryCore.Server; +using Data.Models; namespace SharedLibraryCore.Dtos { @@ -15,11 +15,11 @@ namespace SharedLibraryCore.Dtos /// /// specifies the game name filter /// - public Game? Game { get; set; } + public Reference.Game? Game { get; set; } /// /// collection of unique game names being monitored /// - public Game[] ActiveServerGames { get; set; } + public Reference.Game[] ActiveServerGames { get; set; } } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/Interfaces/IServerDataViewer.cs b/SharedLibraryCore/Interfaces/IServerDataViewer.cs index 8ecfd3c48..bd8b1be47 100644 --- a/SharedLibraryCore/Interfaces/IServerDataViewer.cs +++ b/SharedLibraryCore/Interfaces/IServerDataViewer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Data.Models; using SharedLibraryCore.Dtos; namespace SharedLibraryCore.Interfaces @@ -15,19 +16,21 @@ namespace SharedLibraryCore.Interfaces /// Retrieves the max concurrent clients over a give time period for all servers or given server id /// /// ServerId to query on + /// /// how far in the past to search /// CancellationToken /// - Task<(int?, DateTime?)> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, + Task<(int?, DateTime?)> MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null, CancellationToken token = default); /// /// Gets the total number of clients connected and total clients connected in the given time frame /// /// how far in the past to search + /// /// CancellationToken /// - Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default); + Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default); /// /// Retrieves the client count and history over the given period diff --git a/WebfrontCore/Controllers/API/Info.cs b/WebfrontCore/Controllers/API/Info.cs index 6f655504d..385f511c1 100644 --- a/WebfrontCore/Controllers/API/Info.cs +++ b/WebfrontCore/Controllers/API/Info.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Data.Models; using Microsoft.AspNetCore.Mvc; using SharedLibraryCore; using SharedLibraryCore.Interfaces; @@ -21,13 +22,13 @@ public class Info : BaseController } [HttpGet] - public async Task Get(int period = 24, CancellationToken token = default) + public async Task Get(int period = 24, Reference.Game? game = null, CancellationToken token = default) { // todo: this is hardcoded currently because the cache doesn't take into consideration the duration, so // we could impact the webfront usage too var duration = TimeSpan.FromHours(24); var (totalClients, totalRecentClients) = - await _serverDataViewer.ClientCountsAsync(duration, token); + await _serverDataViewer.ClientCountsAsync(duration, game, token); var (maxConcurrent, maxConcurrentTime) = await _serverDataViewer.MaxConcurrentClientsAsync(overPeriod: duration, token: token); var response = new InfoResponse { diff --git a/WebfrontCore/Controllers/HomeController.cs b/WebfrontCore/Controllers/HomeController.cs index d572f54b0..dc5187a93 100644 --- a/WebfrontCore/Controllers/HomeController.cs +++ b/WebfrontCore/Controllers/HomeController.cs @@ -7,8 +7,8 @@ using SharedLibraryCore.Interfaces; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Data.Models; using Microsoft.Extensions.Logging; -using static SharedLibraryCore.Server; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace WebfrontCore.Controllers @@ -27,26 +27,31 @@ namespace WebfrontCore.Controllers _serverDataViewer = serverDataViewer; } - public async Task Index(Game? game = null, CancellationToken cancellationToken = default) + public async Task Index(Reference.Game? game = null, + CancellationToken cancellationToken = default) { ViewBag.Description = Localization["WEBFRONT_DESCRIPTION_HOME"]; ViewBag.Title = Localization["WEBFRONT_HOME_TITLE"]; ViewBag.Keywords = Localization["WEBFRONT_KEWORDS_HOME"]; - var servers = Manager.GetServers().Where(_server => !game.HasValue || _server.GameName == game); - var (clientCount, time) = await _serverDataViewer.MaxConcurrentClientsAsync(token: cancellationToken); - var (count, recentCount) = await _serverDataViewer.ClientCountsAsync(token: cancellationToken); + var servers = Manager.GetServers().Where(server => game is null || server.GameName == (Server.Game?)game) + .ToList(); + var (clientCount, time) = + await _serverDataViewer.MaxConcurrentClientsAsync(gameCode: game, token: cancellationToken); + var (count, recentCount) = + await _serverDataViewer.ClientCountsAsync(gameCode: game, token: cancellationToken); - var model = new IW4MAdminInfo() + var model = new IW4MAdminInfo { - TotalAvailableClientSlots = servers.Sum(_server => _server.MaxClients), - TotalOccupiedClientSlots = servers.SelectMany(_server => _server.GetClientsAsList()).Count(), + TotalAvailableClientSlots = servers.Sum(server => server.MaxClients), + TotalOccupiedClientSlots = servers.SelectMany(server => server.GetClientsAsList()).Count(), TotalClientCount = count, RecentClientCount = recentCount, MaxConcurrentClients = clientCount ?? 0, MaxConcurrentClientsTime = time ?? DateTime.UtcNow, Game = game, - ActiveServerGames = Manager.GetServers().Select(_server => _server.GameName).Distinct().ToArray() + ActiveServerGames = Manager.GetServers().Select(server => (Reference.Game)server.GameName).Distinct() + .ToArray() }; return View(model); diff --git a/WebfrontCore/ViewComponents/ServerListViewComponent.cs b/WebfrontCore/ViewComponents/ServerListViewComponent.cs index 8877c49da..aac16b78e 100644 --- a/WebfrontCore/ViewComponents/ServerListViewComponent.cs +++ b/WebfrontCore/ViewComponents/ServerListViewComponent.cs @@ -10,7 +10,6 @@ using Data.Models.Client.Stats; using IW4MAdmin.Plugins.Stats.Helpers; using SharedLibraryCore.Configuration; using SharedLibraryCore.Interfaces; -using static SharedLibraryCore.Server; namespace WebfrontCore.ViewComponents { @@ -28,19 +27,20 @@ namespace WebfrontCore.ViewComponents _defaultSettings = defaultSettings; } - public IViewComponentResult Invoke(Game? game) + public IViewComponentResult Invoke(Reference.Game? game) { if (game.HasValue) { - ViewBag.Maps = _defaultSettings.Maps.FirstOrDefault(map => map.Game == game)?.Maps.ToList() ?? - new List(); + ViewBag.Maps = _defaultSettings.Maps?.FirstOrDefault(map => map.Game == (Server.Game)game)?.Maps + ?.ToList() ?? new List(); } else { - ViewBag.Maps = _defaultSettings.Maps.SelectMany(maps => maps.Maps).ToList(); + ViewBag.Maps = _defaultSettings.Maps?.SelectMany(maps => maps.Maps).ToList(); } - var servers = Program.Manager.GetServers().Where(server => !game.HasValue || server.GameName == game); + var servers = Program.Manager.GetServers() + .Where(server => game is null || server.GameName == (Server.Game)game); var serverInfo = new List();