update top level client count stats to support filtering per game
This commit is contained in:
parent
c53e0de7d0
commit
92992dfb13
@ -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,11 +41,20 @@ 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);
|
||||
@ -52,9 +62,10 @@ namespace IW4MAdmin.Application.Misc
|
||||
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,11 +82,12 @@ 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);
|
||||
@ -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<int> 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)
|
||||
{
|
||||
|
@ -11,10 +11,11 @@ namespace Data.Abstractions
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||
|
||||
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
|
||||
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
|
||||
|
||||
Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null, CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace Data.Helpers
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _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<DbSet<TEntityType>, CancellationToken, Task<TCacheType>> Getter { get; set; }
|
||||
public Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TCacheType>> Getter { get; set; }
|
||||
public TCacheType Value { get; set; }
|
||||
public bool IsSet { get; set; }
|
||||
|
||||
@ -53,10 +53,10 @@ namespace Data.Helpers
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> 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<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
ids ??= new[] { _defaultKey };
|
||||
@ -66,15 +66,14 @@ namespace Data.Helpers
|
||||
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
|
||||
}
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var cacheInstance = _cacheStates[key];
|
||||
var id = GenerateKeyFromIds(ids);
|
||||
|
||||
lock (_cacheStates)
|
||||
{
|
||||
if (cacheInstance.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,15 +97,14 @@ namespace Data.Helpers
|
||||
}
|
||||
|
||||
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
|
||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, ids, CancellationToken.None);
|
||||
_timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
|
||||
GetCacheItem(keyName, null, cancellationToken);
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> 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<TReturnType> state, CancellationToken token)
|
||||
private async Task RunCacheUpdate(CacheState<TReturnType> state, IEnumerable<object> 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<TEntityType>();
|
||||
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<object> ids) =>
|
||||
string.Join("_", ids.Select(id => id?.ToString() ?? "null"));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using static SharedLibraryCore.Server;
|
||||
using Data.Models;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
@ -15,11 +15,11 @@ namespace SharedLibraryCore.Dtos
|
||||
/// <summary>
|
||||
/// specifies the game name filter
|
||||
/// </summary>
|
||||
public Game? Game { get; set; }
|
||||
public Reference.Game? Game { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// collection of unique game names being monitored
|
||||
/// </summary>
|
||||
public Game[] ActiveServerGames { get; set; }
|
||||
public Reference.Game[] ActiveServerGames { get; set; }
|
||||
}
|
||||
}
|
@ -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
|
||||
/// </summary>
|
||||
/// <param name="serverId">ServerId to query on</param>
|
||||
/// <param name="gameCode"><see cref="Reference.Game"/></param>
|
||||
/// <param name="overPeriod">how far in the past to search</param>
|
||||
/// <param name="token">CancellationToken</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of clients connected and total clients connected in the given time frame
|
||||
/// </summary>
|
||||
/// <param name="overPeriod">how far in the past to search</param>
|
||||
/// <param name="gameCode"><see cref="Reference.Game"/></param>
|
||||
/// <param name="token">CancellationToken</param>
|
||||
/// <returns></returns>
|
||||
Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default);
|
||||
Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the client count and history over the given period
|
||||
|
@ -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<IActionResult> Get(int period = 24, CancellationToken token = default)
|
||||
public async Task<IActionResult> 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
|
||||
{
|
||||
|
@ -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<IActionResult> Index(Game? game = null, CancellationToken cancellationToken = default)
|
||||
public async Task<IActionResult> 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);
|
||||
|
@ -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<Map>();
|
||||
ViewBag.Maps = _defaultSettings.Maps?.FirstOrDefault(map => map.Game == (Server.Game)game)?.Maps
|
||||
?.ToList() ?? new List<Map>();
|
||||
}
|
||||
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<ServerInfo>();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user