update caching to use automatic timer instead of request based to prevent task cancellation

This commit is contained in:
RaidMax 2021-11-15 10:25:55 -06:00
parent 08bcd23cbc
commit a88b30562c
13 changed files with 65 additions and 35 deletions

View File

@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache; private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly TimeSpan? _cacheTimeSpan = private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(1) : (TimeSpan?) TimeSpan.FromMinutes(1); Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache, public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache, IDataValueCache<EFClient, (int, int)> serverStatsCache,
@ -36,7 +36,8 @@ namespace IW4MAdmin.Application.Misc
_clientHistoryCache = clientHistoryCache; _clientHistoryCache = clientHistoryCache;
} }
public async Task<(int?, DateTime?)> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null, public async Task<(int?, DateTime?)>
MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
CancellationToken token = default) CancellationToken token = default)
{ {
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) => _snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
@ -83,7 +84,7 @@ namespace IW4MAdmin.Application.Misc
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients); _logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
return (maxClients, maxClientsTime); return (maxClients, maxClientsTime);
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan); }, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true);
try try
{ {
@ -107,7 +108,7 @@ namespace IW4MAdmin.Application.Misc
cancellationToken); cancellationToken);
return (count, recentCount); return (count, recentCount);
}, nameof(_serverStatsCache), _cacheTimeSpan); }, nameof(_serverStatsCache), _cacheTimeSpan, true);
try try
{ {

View File

@ -5,9 +5,10 @@ using Microsoft.EntityFrameworkCore;
namespace Data.Abstractions namespace Data.Abstractions
{ {
public interface IDataValueCache<T, V> where T : class public interface IDataValueCache<TEntityType, TReturnType> where TEntityType : class
{ {
void SetCacheItem(Func<DbSet<T>, CancellationToken, Task<V>> itemGetter, string keyName, TimeSpan? expirationTime = null); void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
Task<V> GetCacheItem(string keyName, CancellationToken token = default); TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
} }
} }

View File

@ -8,7 +8,7 @@
<PackageId>RaidMax.IW4MAdmin.Data</PackageId> <PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title> <Title>RaidMax.IW4MAdmin.Data</Title>
<Authors /> <Authors />
<PackageVersion>1.0.7</PackageVersion> <PackageVersion>1.0.8</PackageVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,58 +1,83 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Concurrent;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Timer = System.Timers.Timer;
namespace Data.Helpers namespace Data.Helpers
{ {
public class DataValueCache<T, V> : IDataValueCache<T, V> where T : class public class DataValueCache<TEntityType, TReturnType> : IDataValueCache<TEntityType, TReturnType>
where TEntityType : class
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly Dictionary<string, CacheState> _cacheStates = new Dictionary<string, CacheState>();
private readonly ConcurrentDictionary<string, CacheState> _cacheStates =
new ConcurrentDictionary<string, CacheState>();
private bool _autoRefresh;
private const int DefaultExpireMinutes = 15; private const int DefaultExpireMinutes = 15;
private Timer _timer;
private class CacheState private class CacheState
{ {
public string Key { get; set; } public string Key { get; set; }
public DateTime LastRetrieval { get; set; } public DateTime LastRetrieval { get; set; }
public TimeSpan ExpirationTime { get; set; } public TimeSpan ExpirationTime { get; set; }
public Func<DbSet<T>, CancellationToken, Task<V>> Getter { get; set; } public Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> Getter { get; set; }
public V Value { get; set; } public TReturnType Value { get; set; }
public bool IsExpired => ExpirationTime != TimeSpan.MaxValue && public bool IsExpired => ExpirationTime != TimeSpan.MaxValue &&
(DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0; (DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0;
} }
public DataValueCache(ILogger<DataValueCache<T, V>> logger, IDatabaseContextFactory contextFactory) public DataValueCache(ILogger<DataValueCache<TEntityType, TReturnType>> logger,
IDatabaseContextFactory contextFactory)
{ {
_logger = logger; _logger = logger;
_contextFactory = contextFactory; _contextFactory = contextFactory;
} }
public void SetCacheItem(Func<DbSet<T>, CancellationToken, Task<V>> getter, string key, ~DataValueCache()
TimeSpan? expirationTime = null) {
_timer?.Stop();
_timer?.Dispose();
}
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false)
{ {
if (_cacheStates.ContainsKey(key)) if (_cacheStates.ContainsKey(key))
{ {
_logger.LogDebug("Cache key {key} is already added", key); _logger.LogDebug("Cache key {Key} is already added", key);
return; return;
} }
var state = new CacheState() var state = new CacheState
{ {
Key = key, Key = key,
Getter = getter, Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes) ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
}; };
_cacheStates.Add(key, state); _autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
} }
public async Task<V> GetCacheItem(string keyName, CancellationToken cancellationToken = default) public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
{ {
if (!_cacheStates.ContainsKey(keyName)) if (!_cacheStates.ContainsKey(keyName))
{ {
@ -61,7 +86,9 @@ namespace Data.Helpers
var state = _cacheStates[keyName]; var state = _cacheStates[keyName];
if (state.IsExpired || state.Value == null) // 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.Value == null) && !_autoRefresh || _autoRefresh && state.Value == null)
{ {
await RunCacheUpdate(state, cancellationToken); await RunCacheUpdate(state, cancellationToken);
} }
@ -73,15 +100,16 @@ namespace Data.Helpers
{ {
try try
{ {
_logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state);
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
var set = context.Set<T>(); var set = context.Set<TEntityType>();
var value = await state.Getter(set, token); var value = await state.Getter(set, token);
state.Value = value; state.Value = value;
state.LastRetrieval = DateTime.Now; state.LastRetrieval = DateTime.Now;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Could not get cached value for {key}", state.Key); _logger.LogError(ex, "Could not get cached value for {Key}", state.Key);
} }
} }
} }

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.31.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.11.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.31.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.11.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.31.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.11.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.31.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.11.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.31.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.11.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.31.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.11.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.8.31.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.11.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -123,9 +123,9 @@ namespace SharedLibraryCore.Configuration
if (passwords.Length > 1) if (passwords.Length > 1)
{ {
var (index, value) = var (index, value) =
loc["SETUP_RCON_PASSWORD_PROMPT"].PromptSelection("Other", null, loc["SETUP_RCON_PASSWORD_PROMPT"].PromptSelection(loc["SETUP_RCON_PASSWORD_MANUAL"], null,
passwords.Select(pw => passwords.Select(pw =>
$"{pw.Item1}{(string.IsNullOrEmpty(pw.Item2) ? "" : " " + pw.Item2)}").ToArray()); $"{pw.Item1}{(string.IsNullOrEmpty(pw.Item2) ? "" : " " + pw.Item2)}").ToArray());
if (index > 0) if (index > 0)
{ {

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2021.8.31.1</Version> <Version>2021.11.15.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2021.8.31.1</PackageVersion> <PackageVersion>2021.11.15.1</PackageVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -44,7 +44,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.10" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.10" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.10" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.10" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.7" /> <PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.8" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" /> <PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup> </ItemGroup>