From 1f2b3530ebeafa162fe4df41e691c33cb2ff0009 Mon Sep 17 00:00:00 2001 From: vman Date: Thu, 15 Dec 2022 01:45:03 -0500 Subject: JWK sigs, session cleanup v1 --- .../Endpoints/AccessTokenEndpoint.cs | 12 +- .../O2SessionProviderEntry.cs | 11 +- .../OAuth2SessionProvider.cs | 22 +++- .../VNLib.Plugins.Essentials.Sessions.OAuth.csproj | 2 +- ...NLib.Plugins.Essentials.Sessions.Runtime.csproj | 4 +- .../VnCacheClient.cs | 35 +++--- ...NLib.Plugins.Essentials.Sessions.VNCache.csproj | 2 +- .../WebSessionProvider.cs | 22 +--- .../WebSessionProviderEntry.cs | 6 +- .../MemorySession.cs | 30 +++-- .../MemorySessionEntrypoint.cs | 3 + .../MemorySessionStore.cs | 34 +++++- ...VNLib.Plugins.Essentials.Sessions.Memory.csproj | 4 +- .../Exceptions/SessionStatusException.cs | 3 + .../RemoteSession.cs | 17 +-- .../SessionCacheClient.cs | 123 ++++++++++++++++----- .../VNLib.Plugins.Sessions.Cache.Client.csproj | 3 +- 17 files changed, 223 insertions(+), 110 deletions(-) (limited to 'Libs') diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs index a159456..d968398 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs @@ -30,12 +30,12 @@ using VNLib.Utils.Memory; using VNLib.Hashing.IdentityUtility; using VNLib.Plugins.Essentials.Oauth; using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Oauth.Tokens; using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Extensions; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Sql; using VNLib.Plugins.Extensions.Validation; -using VNLib.Plugins.Essentials.Oauth.Tokens; namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints { @@ -56,9 +56,9 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints //override protection settings to allow most connections to authenticate protected override ProtectionSettings EndpointProtectionSettings { get; } = new() { - BrowsersOnly = false, - SessionsRequired = false, - VerifySessionCors = false + DisableBrowsersOnly = true, + DisableSessionsRequired = true, + DisableVerifySessionCors = true }; public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore, Task verificationKey) @@ -117,7 +117,7 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints secret = secret.ToLower(); //Convert secret to private string that is unreferrenced - PrivateString secretPv = new(secret, false); + using PrivateString secretPv = new(secret, false); //Get the application from apps store UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv); @@ -171,7 +171,7 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints if (result == null) { - entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application"); + entity.CloseResponseError(HttpStatusCode.TooManyRequests, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application"); return VfReturnType.VirtualSkip; } diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs index 07b6530..f4462a4 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs @@ -27,16 +27,17 @@ using System.Text.Json; using VNLib.Net.Http; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; +using VNLib.Data.Caching.Extensions; using VNLib.Plugins.Essentials.Oauth.Tokens; using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Sessions.OAuth; +using VNLib.Plugins.Essentials.Sessions.Runtime; using VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Routing; using VNLib.Plugins.Extensions.Loading.Sql; using VNLib.Plugins.Extensions.Loading.Events; -using VNLib.Plugins.Essentials.Sessions.Runtime; -using VNLib.Data.Caching.Extensions; + namespace VNLib.Plugins.Essentials.Sessions.Oauth { @@ -69,7 +70,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth //Optional application jwt token Task jwtTokenSecret = plugin.TryGetSecretAsync("application_token_key") - .ContinueWith(static t => t.Result == null ? null : JsonDocument.Parse(t.Result), TaskScheduler.Default); + .ContinueWith(static t => t.Result == null ? null : t.Result.GetJsonDocument(), TaskScheduler.Default); //Access token endpoint is optional if (oauth2Config.TryGetValue("token_path", out JsonElement el)) @@ -107,7 +108,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth IReadOnlyDictionary oauth2Config) { //Init cache client - using VnCacheClient cache = new(plugin.IsDebug() ? plugin.Log : null, Utils.Memory.Memory.Shared); + using VnCacheClient cache = new(plugin.IsDebug() ? localized : null, Utils.Memory.Memory.Shared); try { @@ -125,7 +126,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth await cache.LoadConfigAsync(plugin, cacheConfig); //Init session provider now that client is loaded - _sessions = new(cache.Resource!, cacheLimit, idProv, plugin.GetContextOptions()); + _sessions = new(cache.Resource!, cacheLimit, 100, idProv, plugin.GetContextOptions()); //Schedule cleanup interval with the plugin scheduler plugin.ScheduleInterval(_sessions, cleanupInterval); diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs index d698c81..106029f 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs @@ -55,18 +55,20 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth private readonly IOauthSessionIdFactory factory; private readonly TokenStore TokenStore; - - public OAuth2SessionProvider(FBMClient client, int maxCacheItems, IOauthSessionIdFactory idFactory, DbContextOptions dbCtx) + private readonly uint MaxConnections; + + public OAuth2SessionProvider(FBMClient client, int maxCacheItems, uint maxConnections, IOauthSessionIdFactory idFactory, DbContextOptions dbCtx) : base(client, maxCacheItems) { factory = idFactory; TokenStore = new(dbCtx); + MaxConnections = maxConnections; } /// - protected override RemoteSession SessionCtor(string sessionId) => new OAuth2Session(sessionId, Client, BackgroundTimeout, InvlidatateCache); + protected override RemoteSession SessionCtor(string sessionId) => new OAuth2Session(sessionId, Client, BackgroundTimeout, InvalidatateCache); - private void InvlidatateCache(OAuth2Session session) + private void InvalidatateCache(OAuth2Session session) { lock (CacheLock) { @@ -91,6 +93,14 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth return SessionHandle.Empty; } + //Limit max number of waiting clients + if (WaitingConnections > MaxConnections) + { + //Set 503 for temporary unavail + entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable); + return new SessionHandle(null, FileProcessArgs.VirtualSkip, null); + } + //Recover the session RemoteSession session = await base.GetSessionAsync(entity, sessionId, cancellationToken); @@ -174,14 +184,14 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth /* - * Interval for remving expired tokens + * Interval for removing expired tokens */ /// async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) { //Calculate valid token time - DateTimeOffset validAfter = DateTimeOffset.UtcNow.Subtract(factory.SessionValidFor); + DateTime validAfter = DateTime.UtcNow.Subtract(factory.SessionValidFor); //Remove tokens from db store IReadOnlyCollection revoked = await TokenStore.CleanupExpiredTokensAsync(validAfter, cancellationToken); //exception list diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj index d75a1c0..e9927b5 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj @@ -31,7 +31,7 @@ - + diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj index d72d6b9..cf6d0c9 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj +++ b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VNLib.Plugins.Essentials.Sessions.Runtime.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs index 3b348d2..2f7bdf2 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.Runtime/VnCacheClient.cs @@ -28,13 +28,14 @@ using System.Net.Sockets; using System.Net.WebSockets; using System.Security.Cryptography; -using VNLib.Utils; using VNLib.Utils.Memory; using VNLib.Utils.Logging; +using VNLib.Utils.Resources; using VNLib.Utils.Extensions; using VNLib.Data.Caching.Extensions; using VNLib.Net.Messaging.FBM.Client; using VNLib.Plugins.Extensions.Loading; +using VNLib.Hashing.IdentityUtility; namespace VNLib.Plugins.Essentials.Sessions.Runtime { @@ -70,7 +71,8 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime ClientHeap = heap; } - + + /// protected override void Free() { _client?.Dispose(); @@ -90,14 +92,20 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime string? brokerAddress = config["broker_address"].GetString() ?? throw new KeyNotFoundException("Missing required configuration variable broker_address"); //Get keys async - Task clientPrivTask = pbase.TryGetSecretAsync("client_private_key"); - Task brokerPubTask = pbase.TryGetSecretAsync("broker_public_key"); + Task clientPrivTask = pbase.TryGetSecretAsync("client_private_key"); + Task brokerPubTask = pbase.TryGetSecretAsync("broker_public_key"); + Task cachePubTask = pbase.TryGetSecretAsync("cache_public_key"); //Wait for all tasks to complete - string?[] keys = await Task.WhenAll(clientPrivTask, brokerPubTask); + _ = await Task.WhenAll(clientPrivTask, brokerPubTask, cachePubTask); + + using SecretResult clientPriv = await clientPrivTask ?? throw new KeyNotFoundException("Missing required secret client_private_key"); + using SecretResult brokerPub = await brokerPubTask ?? throw new KeyNotFoundException("Missing required secret broker_public_key"); + using SecretResult cachePub = await cachePubTask ?? throw new KeyNotFoundException("Missing required secret cache_public_key"); - byte[] privKey = Convert.FromBase64String(keys[0] ?? throw new KeyNotFoundException("Missing required secret client_private_key")); - byte[] brokerPub = Convert.FromBase64String(keys[1] ?? throw new KeyNotFoundException("Missing required secret broker_public_key")); + ReadOnlyJsonWebKey clientCert = clientPriv.GetJsonWebKey(); + ReadOnlyJsonWebKey brokerPubKey = brokerPub.GetJsonWebKey(); + ReadOnlyJsonWebKey cachePubKey = cachePub.GetJsonWebKey(); RetryInterval = config["retry_interval_sec"].GetTimeSpan(TimeParseType.Seconds); @@ -107,16 +115,14 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime FBMClientConfig conf = FBMDataCacheExtensions.GetDefaultConfig(ClientHeap ?? Memory.Shared, maxMessageSize, DebugLog); _client = new(conf); - //Add the configuration + + //Add the configuration to the client _client.GetCacheConfiguration() .WithBroker(brokerUri) - .ImportVerificationKey(brokerPub) - .ImportSigningKey(privKey) + .WithVerificationKey(cachePubKey) + .WithSigningCertificate(clientCert) + .WithBrokerVerificationKey(brokerPubKey) .WithTls(brokerUri.Scheme == Uri.UriSchemeHttps); - - //Zero the key memory - Memory.InitializeBlock(privKey.AsSpan()); - Memory.InitializeBlock(brokerPub.AsSpan()); } /// @@ -157,6 +163,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Runtime int randomMsDelay = RandomNumberGenerator.GetInt32(1000, 2000); await Task.Delay(randomMsDelay, cancellationToken); } + if (servers?.Length == 0) { Log.Warn("No cluster nodes found, retrying"); diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj index 8c3e23a..dc73ed8 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj +++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/VNLib.Plugins.Essentials.Sessions.VNCache.csproj @@ -21,7 +21,7 @@ - + diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs index 057e308..5747175 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs @@ -71,9 +71,6 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache protected override RemoteSession SessionCtor(string sessionId) => new WebSession(sessionId, Client, BackgroundUpdateTimeout, UpdateSessionId); - - private uint _waitingCount; - public async ValueTask GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) { //Callback to close the session when the handle is closeed @@ -92,27 +89,15 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache } //Limit max number of waiting clients - if (_waitingCount > MaxConnections) + if (WaitingConnections > MaxConnections) { //Set 503 for temporary unavail entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable); return new SessionHandle(null, FileProcessArgs.VirtualSkip, null); } - RemoteSession session; - - //Inc waiting count - Interlocked.Increment(ref _waitingCount); - try - { - //Recover the session - session = await GetSessionAsync(entity, sessionId, cancellationToken); - } - finally - { - //Dec on exit - Interlocked.Decrement(ref _waitingCount); - } + //Get session + RemoteSession session = await GetSessionAsync(entity, sessionId, cancellationToken); //If the session is new (not in cache), then overwrite the session id with a new one as user may have specified their own if (session.IsNew) @@ -131,6 +116,7 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache session.Privilages = 0; session.SetLoginToken(null); } + return new SessionHandle(session, HandleClosedAsync); } catch (OperationCanceledException) diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs index 382717b..8e25416 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProviderEntry.cs @@ -84,7 +84,7 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache IReadOnlyDictionary webSessionConfig) { //Init cache client - using VnCacheClient cache = new(plugin.IsDebug() ? plugin.Log : null, Memory.Shared); + using VnCacheClient cache = new(plugin.IsDebug() ? localized : null, Memory.Shared); try { @@ -97,9 +97,11 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache //Init provider _sessions = new(cache.Resource!, cacheLimit, maxConnections, idFactory); - localized.Information("Session provider loaded"); + //Listen for cache table events + _ = plugin.DeferTask(() => _sessions.CleanupExpiredSessionsAsync(localized, plugin.UnloadToken)); + //Run and wait for exit await cache.RunAsync(localized, plugin.UnloadToken); diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs index a806438..d365cd2 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs @@ -28,23 +28,27 @@ using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Plugins.Essentials.Extensions; +using VNLib.Utils.Async; using VNLib.Net.Http; +using VNLib.Utils.Memory.Caching; using static VNLib.Plugins.Essentials.Sessions.ISessionExtensions; #nullable enable namespace VNLib.Plugins.Essentials.Sessions.Memory { - internal class MemorySession : SessionBase + internal class MemorySession : SessionBase, ICacheable { private readonly Dictionary DataStorage; private readonly Func OnSessionUpdate; + private readonly AsyncQueue ExpiredTable; - public MemorySession(string sessionId, IPAddress ipAddress, Func onSessionUpdate) + public MemorySession(string sessionId, IPAddress ipAddress, Func onSessionUpdate, AsyncQueue expired) { //Set the initial is-new flag DataStorage = new Dictionary(10); + ExpiredTable = expired; OnSessionUpdate = onSessionUpdate; //Get new session id @@ -90,14 +94,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory //Memory session always completes return ValueTask.FromResult(null); } - - protected override Task OnEvictedAsync() - { - //Clear all session data - DataStorage.Clear(); - return Task.CompletedTask; - } - + protected override string IndexerGet(string key) { return DataStorage.GetValueOrDefault(key, string.Empty); @@ -116,5 +113,18 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory } DataStorage[key] = value; } + + + DateTime ICacheable.Expires { get; set; } + + void ICacheable.Evicted() + { + DataStorage.Clear(); + //Enque cleanup + _ = ExpiredTable.TryEnque(this); + } + + bool IEquatable.Equals(ICacheable? other) => other is ISession ses && SessionID.Equals(ses.SessionID, StringComparison.Ordinal); + } } diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs index 91a3a0e..f58129a 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs @@ -73,6 +73,9 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory _sessions = new(config); + //Begin listening for expired records + _ = plugin.DeferTask(() => _sessions.CleanupExiredAsync(localized, plugin.UnloadToken)); + //Schedule garbage collector _ = plugin.ScheduleInterval(this, TimeSpan.FromMinutes(1)); diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs index e76d7a4..1af885e 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs @@ -28,11 +28,12 @@ using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Net.Http; +using VNLib.Net.Http.Core; using VNLib.Utils; using VNLib.Utils.Async; +using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Extensions; -using VNLib.Net.Http.Core; #nullable enable @@ -47,12 +48,14 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory internal readonly MemorySessionConfig Config; internal readonly SessionIdFactory IdFactory; + internal readonly AsyncQueue ExpiredSessions; public MemorySessionStore(MemorySessionConfig config) { Config = config; SessionsStore = new(config.MaxAllowedSessions, StringComparer.Ordinal); IdFactory = new(config.SessionIdSizeBytes, config.SessionCookieID, config.SessionTimeout); + ExpiredSessions = new(false, true); } /// @@ -68,7 +71,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory if (IdFactory.TryGetSessionId(entity, out string? sessionId)) { //Try to get the old record or evict it - ERRNO result = SessionsStore.TryGetOrEvictRecord(sessionId, out MemorySession session); + ERRNO result = SessionsStore.TryGetOrEvictRecord(sessionId, out MemorySession? session); if(result > 0) { //Valid, now wait for exclusive access @@ -89,7 +92,7 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory return new(null, FileProcessArgs.VirtualSkip, null); } //Initialze a new session - session = new(sessionId, entity.Server.GetTrustedIp(), UpdateSessionId); + session = new(sessionId, entity.Server.GetTrustedIp(), UpdateSessionId, ExpiredSessions); //Increment the semaphore (session as IWaitHandle).WaitOne(); //store the session in cache while holding semaphore, and set its expiration @@ -104,6 +107,31 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory } } + public async Task CleanupExiredAsync(ILogProvider log, CancellationToken token) + { + while (true) + { + try + { + //Wait for expired session and dispose it + using MemorySession session = await ExpiredSessions.DequeueAsync(token); + + //Obtain lock on session + await session.WaitOneAsync(CancellationToken.None); + + log.Verbose("Removed expired session {id}", session.SessionID); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + log.Error(ex); + } + } + } + private string UpdateSessionId(IHttpEvent entity, string oldId) { //Generate and set a new sessionid diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj b/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj index 0d3cb40..c4a65f5 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj +++ b/Libs/VNLib.Plugins.Essentials.Sessions/VNLib.Plugins.Essentials.Sessions.Memory.csproj @@ -36,8 +36,8 @@ - - + + diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs index b857638..bc6b217 100644 --- a/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs @@ -29,6 +29,9 @@ using VNLib.Plugins.Essentials.Sessions; namespace VNLib.Plugins.Sessions.Cache.Client.Exceptions { + /// + /// Raised when the status of the session is invalid and cannot be used + /// public class SessionStatusException : SessionException { public SessionStatusException() diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs index d2d4200..3b61f68 100644 --- a/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs @@ -37,8 +37,6 @@ using VNLib.Net.Messaging.FBM.Client; using VNLib.Plugins.Essentials.Sessions; using VNLib.Plugins.Essentials.Extensions; -#nullable enable - namespace VNLib.Plugins.Sessions.Cache.Client { /// @@ -48,9 +46,9 @@ namespace VNLib.Plugins.Sessions.Cache.Client public abstract class RemoteSession : SessionBase { protected const string CREATED_TIME_ENTRY = "__.i.ctime"; - - protected readonly FBMClient Client; - protected readonly TimeSpan UpdateTimeout; + + protected FBMClient Client { get; } + protected TimeSpan UpdateTimeout { get; } private readonly AsyncLazyInitializer Initializer; @@ -119,6 +117,8 @@ namespace VNLib.Plugins.Sessions.Cache.Client protected set => this.SetValueType(CREATED_TIME_ENTRY, value.ToUnixTimeMilliseconds()); } + + /// protected override string IndexerGet(string key) { @@ -186,12 +186,5 @@ namespace VNLib.Plugins.Sessions.Cache.Client throw; } } - /// - protected override Task OnEvictedAsync() - { - //empty the dict to help the GC - DataStore!.Clear(); - return Task.CompletedTask; - } } } diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs index 8eed404..2e72391 100644 --- a/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs @@ -29,37 +29,64 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using VNLib.Net.Http; -using VNLib.Utils; +using VNLib.Utils.Async; +using VNLib.Utils.Logging; using VNLib.Utils.Memory.Caching; using VNLib.Net.Messaging.FBM.Client; using VNLib.Plugins.Essentials.Sessions; -#nullable enable - namespace VNLib.Plugins.Sessions.Cache.Client { /// /// A client that allows access to sessions located on external servers /// - public abstract class SessionCacheClient : VnDisposeable, ICacheHolder + public abstract class SessionCacheClient : ICacheHolder { - public class LRUSessionStore : LRUCache where T : ISession, ICacheable + public class LRUSessionStore : LRUCache, ICacheHolder where T : ISession { + internal AsyncQueue ExpiredSessions { get; } + + /// public override bool IsReadOnly => false; + /// protected override int MaxCapacity { get; } - public LRUSessionStore(int maxCapacity) : base(StringComparer.Ordinal) => MaxCapacity = maxCapacity; - + + public LRUSessionStore(int maxCapacity) : base(StringComparer.Ordinal) + { + MaxCapacity = maxCapacity; + ExpiredSessions = new (true, true); + } + + /// protected override bool CacheMiss(string key, [NotNullWhen(true)] out T? value) { value = default; return false; } + + /// protected override void Evicted(KeyValuePair evicted) { - //Evice record - evicted.Value.Evicted(); + //add to queue, the list lock should be held during this operatio + _ = ExpiredSessions.TryEnque(evicted.Value); + } + + /// + public void CacheClear() + { + foreach (KeyValuePair value in List) + { + Evicted(value); + } + Clear(); + } + + /// + public void CacheHardClear() + { + CacheClear(); } } @@ -67,6 +94,9 @@ namespace VNLib.Plugins.Sessions.Cache.Client protected readonly object CacheLock; protected readonly int MaxLoadedEntires; + /// + /// The client used to communicate with the cache server + /// protected FBMClient Client { get; } /// @@ -80,11 +110,14 @@ namespace VNLib.Plugins.Sessions.Cache.Client CacheLock = new(); CacheTable = new(maxCacheItems); Client = client; - //Listen for close events - Client.ConnectionClosed += Client_ConnectionClosed; } - private void Client_ConnectionClosed(object? sender, EventArgs e) => CacheHardClear(); + private ulong _waitingCount; + + /// + /// The number of pending connections waiting for results from the cache server + /// + public ulong WaitingConnections => _waitingCount; /// /// Attempts to get a session from the cache identified by its sessionId asynchronously @@ -96,7 +129,6 @@ namespace VNLib.Plugins.Sessions.Cache.Client /// public virtual async ValueTask GetSessionAsync(IHttpEvent entity, string sessionId, CancellationToken cancellationToken) { - Check(); try { RemoteSession? session; @@ -113,6 +145,10 @@ namespace VNLib.Plugins.Sessions.Cache.Client } //Valid entry found in cache } + + //Inc waiting count + Interlocked.Increment(ref _waitingCount); + try { //Load session-data @@ -128,6 +164,11 @@ namespace VNLib.Plugins.Sessions.Cache.Client } throw; } + finally + { + //Dec waiting count + Interlocked.Decrement(ref _waitingCount); + } } catch (SessionException) { @@ -151,6 +192,47 @@ namespace VNLib.Plugins.Sessions.Cache.Client /// The session identifier /// The new session for the given ID protected abstract RemoteSession SessionCtor(string sessionId); + + /// + /// Begins waiting for expired sessions to be evicted from the cache table that + /// may have pending synchronization operations + /// + /// + /// + /// + public async Task CleanupExpiredSessionsAsync(ILogProvider log, CancellationToken token) + { + //Close handler + void OnConnectionClosed(object? sender, EventArgs e) => CacheHardClear(); + + //Attach event + Client.ConnectionClosed += OnConnectionClosed; + + while (true) + { + try + { + //Wait for expired session and dispose it + using RemoteSession session = await CacheTable.ExpiredSessions.DequeueAsync(token); + + //Obtain lock on session + await session.WaitOneAsync(CancellationToken.None); + + log.Verbose("Removed expired session {id}", session.SessionID); + } + catch (OperationCanceledException) + { + break; + } + catch(Exception ex) + { + log.Error(ex); + } + } + + //remove handler + Client.ConnectionClosed -= OnConnectionClosed; + } /// public void CacheClear() @@ -163,21 +245,8 @@ namespace VNLib.Plugins.Sessions.Cache.Client //Cleanup cache when disconnected lock (CacheLock) { - CacheTable.Clear(); - foreach (RemoteSession session in (IEnumerable)CacheTable) - { - session.Evicted(); - } - CacheTable.Clear(); + CacheTable.CacheHardClear(); } } - - protected override void Free() - { - //Unsub from events - Client.ConnectionClosed -= Client_ConnectionClosed; - //Clear all cached sessions - CacheHardClear(); - } } } diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj index fc99bbf..a75ebe3 100644 --- a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj @@ -12,6 +12,7 @@ True https://www.vaughnnugent.com/resources latest-all + enable @@ -36,7 +37,7 @@ - + -- cgit