diff options
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.Cache.Client/src')
13 files changed, 1250 insertions, 360 deletions
diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs index 55f5016..bb7acd6 100644 --- a/libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Sessions.Cache.Client @@ -30,6 +30,7 @@ using VNLib.Data.Caching; namespace VNLib.Plugins.Sessions.Cache.Client { + /// <summary> /// A wrapper class to provide a <see cref="IRemoteCacheStore"/> from /// a <see cref="IGlobalCacheProvider"/> client instance @@ -37,10 +38,19 @@ namespace VNLib.Plugins.Sessions.Cache.Client public sealed class GlobalCacheStore : IRemoteCacheStore { private readonly IGlobalCacheProvider _cache; + + private readonly SessionDataSerialzer _serialzer; + /// <summary> + /// Initiailzes a new <see cref="GlobalCacheStore"/> with the backing <see cref="IGlobalCacheProvider"/> + /// global cache + /// </summary> + /// <param name="globalCache"></param> + /// <exception cref="ArgumentNullException"></exception> public GlobalCacheStore(IGlobalCacheProvider globalCache) { _cache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); + _serialzer = new(); } ///<inheritdoc/> @@ -49,9 +59,9 @@ namespace VNLib.Plugins.Sessions.Cache.Client ///<inheritdoc/> public Task AddOrUpdateObjectAsync<T>(string objectId, string? newId, T obj, CancellationToken cancellationToken = default) { - return _cache.AddOrUpdateAsync(objectId, newId, obj, cancellationToken); + return _cache.AddOrUpdateAsync(objectId, newId, obj, _serialzer, cancellationToken); } - + ///<inheritdoc/> public Task DeleteObjectAsync(string objectId, CancellationToken cancellationToken = default) { @@ -61,7 +71,7 @@ namespace VNLib.Plugins.Sessions.Cache.Client ///<inheritdoc/> public Task<T?> GetObjectAsync<T>(string objectId, CancellationToken cancellationToken = default) { - return _cache.GetAsync<T>(objectId, cancellationToken); + return _cache.GetAsync<T>(objectId, _serialzer, cancellationToken); } } } diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs index ce95a6b..aa2e3a7 100644 --- a/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Sessions.Cache.Client @@ -37,7 +37,7 @@ namespace VNLib.Plugins.Sessions.Cache.Client /// </summary> /// <typeparam name="T">The data type</typeparam> /// <param name="objectId">The key/id of the object to recover</param> - /// <param name="cancellationToken">A token to cancel the operation</param> + /// <param name="cancellationToken">An optional token to cancel the operation</param> /// <returns>A task that resolves the found object or null otherwise</returns> Task<T?> GetObjectAsync<T>(string objectId, CancellationToken cancellationToken = default); diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteSession.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteSession.cs new file mode 100644 index 0000000..d88fee2 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteSession.cs @@ -0,0 +1,73 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: IRemoteSession.cs +* +* IRemoteSession.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Collections.Generic; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// A session that is expected to be stored in a remote caching storage system + /// </summary> + public interface IRemoteSession + { + /// <summary> + /// Gets the ID of the session + /// </summary> + string SessionID { get; } + + /// <summary> + /// Gets the status of the session during a release operation + /// </summary> + /// <returns>The <see cref="SessionStatus"/> flags that represent the status of the session</returns> + SessionStatus GetStatus(); + + /// <summary> + /// Gets the internal session data to update when requested + /// </summary> + /// <returns>The internal session data</returns> + IDictionary<string, string> GetSessionData(); + + /// <summary> + /// Destroys the internal state of the session so it cannot be + /// reused + /// </summary> + /// <param name="cause">A optional exception that caused the error condition</param> + void Destroy(Exception? cause); + + /// <summary> + /// Determines if the state of the session is valid for reuse by a waiting + /// connection. Optionally returns an exception that caused + /// </summary> + /// <param name="cause">The exception that caused the session state to transition to invalid</param> + /// <returns>True of the session is valid, false if it cannot be reused</returns> + bool IsValid(out Exception? cause); + + /// <summary> + /// Called by the store to notify the session that a pending update has completed + /// successfully. + /// </summary> + void SessionUpdateComplete(); + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionFactory.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionFactory.cs new file mode 100644 index 0000000..ea1646e --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionFactory.cs @@ -0,0 +1,47 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: ISessionFactory.cs +* +* ISessionFactory.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Collections.Generic; + +using VNLib.Net.Http; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// A session factory that generates new sessions on demand by a <see cref="ISessionStore{TSession}"/> + /// </summary> + /// <typeparam name="TSession">The session type to generate</typeparam> + public interface ISessionFactory<TSession> + { + /// <summary> + /// Constructs a new session of the given type from the session Id + /// and its initial object data + /// </summary> + /// <param name="sessionId">The is of the session to create</param> + /// <param name="sessionData">The initial session data to create the session from</param> + /// <param name="entity">The connection to get the session for</param> + /// <returns>The new session</returns> + TSession? GetNewSession(IHttpEvent entity, string sessionId, IDictionary<string, string>? sessionData); + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionIdFactory.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionIdFactory.cs new file mode 100644 index 0000000..d423ae2 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionIdFactory.cs @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: ISessionIdFactory.cs +* +* ISessionIdFactory.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using VNLib.Net.Http; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// A factory that gets sessionIds from connections and regenerates + /// session ids on connections if supported + /// </summary> + public interface ISessionIdFactory + { + /// <summary> + /// A value that indicates if session id regeneration + /// will be supported by this provider + /// </summary> + bool RegenerationSupported { get; } + + /// <summary> + /// A value that indicates if a session id should be + /// regenerated when the remote cache does not have a + /// result for the recovered id + /// </summary> + /// <remarks> + /// This is considered a security feature to dissalow clients + /// from injecting thier own sessions + /// </remarks> + bool RegenIdOnEmptyEntry { get; } + + /// <summary> + /// Indicates if the request can be serviced + /// </summary> + /// <param name="entity">The entity to service</param> + /// <returns>True if a session id can be provided for this connection</returns> + bool CanService(IHttpEvent entity); + + /// <summary> + /// Regenerates the session id for the given connection + /// </summary> + /// <param name="entity">The connection to regenreate the id for</param> + /// <returns>The new session id for the connection, or null if regeneration fails</returns> + string RegenerateId(IHttpEvent entity); + + /// <summary> + /// Attempts to recover a session id from the connection. If null is + /// returned it is consisdered a failure. + /// </summary> + /// <param name="entity">The connection to retrieve the sessionId for</param> + /// <returns>The session id if successfull, null otherwise</returns> + string? TryGetSessionId(IHttpEvent entity); + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionSerialzer.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionSerialzer.cs new file mode 100644 index 0000000..40bfa7b --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionSerialzer.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: ISessionSerialzer.cs +* +* ISessionSerialzer.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.Async; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// A specialized <see cref="IAsyncAccessSerializer{TMoniker}"/> + /// that allows for re-using an instance that may be awaited + /// </summary> + /// <typeparam name="TSession">The session type</typeparam> + public interface ISessionSerialzer<TSession> : IAsyncAccessSerializer<TSession> + { + /// <summary> + /// Attempts to get an active session in the wait table as an atomic operation + /// </summary> + /// <param name="sessionId">The id of the session to retreive from the store</param> + /// <param name="session">The stored session</param> + /// <returns>A value that inidcates if the session was found</returns> + bool TryGetSession(string sessionId, [NotNullWhen(true)] out TSession? session); + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionStore.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionStore.cs new file mode 100644 index 0000000..8e595dc --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionStore.cs @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: ISessionStore.cs +* +* ISessionStore.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Net.Http; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// A store for session types that attaches sessions to incomming http connections + /// </summary> + /// <typeparam name="TSession">The session type</typeparam> + public interface ISessionStore<TSession> + { + /// <summary> + /// Gets a session for the given connection or returns null if no + /// session could be attached to the connection + /// </summary> + /// <param name="entity">The connection to attach a session to</param> + /// <param name="cancellationToken">A token to cancel that async operation</param> + /// <returns>The session for the incomming connection, or null if no session was found</returns> + ValueTask<TSession?> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken); + + /// <summary> + /// Releases the session from the connection, as its no longer required. Cleanup tasks + /// should be performed while the connection is still alive + /// </summary> + /// <param name="session">The session to detach</param> + /// <param name="entity">The connection the session is attached to</param> + /// <returns>A task that completes when the session has been detached</returns> + /// <remarks> + /// The connection results/request should not be modified. Cookies/headers + /// may still be valid + /// </remarks> + ValueTask ReleaseSessionAsync(TSession session, IHttpEvent entity); + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs index af2c969..421d07d 100644 --- a/libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Sessions.Cache.Client @@ -23,85 +23,49 @@ */ using System; -using System.Threading; -using System.Threading.Tasks; using System.Collections.Generic; -using Microsoft.VisualStudio.Threading; - -using VNLib.Net.Http; -using VNLib.Data.Caching.Exceptions; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Sessions; using VNLib.Plugins.Essentials.Extensions; + namespace VNLib.Plugins.Sessions.Cache.Client { /// <summary> /// Base class for cacheable lazy initialized session entires /// that exist in a remote caching server /// </summary> - public abstract class RemoteSession : SessionBase + public abstract class RemoteSession : SessionBase, IRemoteSession { protected const string CREATED_TIME_ENTRY = "__.i.ctime"; - - protected IRemoteCacheStore Client { get; } - protected TimeSpan UpdateTimeout { get; } - private readonly AsyncLazyInitializer Initializer; - /// <summary> - /// The lazy loaded data-store + /// The session data store /// </summary> - protected Dictionary<string, string>? DataStore; - - protected RemoteSession(string sessionId, IRemoteCacheStore client, TimeSpan backgroundTimeOut) - { - SessionID = sessionId; - UpdateTimeout = backgroundTimeOut; - Client = client; - Initializer = new(InitializeAsync, null); - } + protected readonly IDictionary<string, string> DataStore; /// <summary> - /// The data initializer, loads the data store from the connected cache server + /// The reason that the session was destroyed if an error occured /// </summary> - /// <returns>A task that completes when the get operation completes</returns> - protected virtual async Task InitializeAsync() - { - //Setup timeout cancellation for the get, to cancel it - using CancellationTokenSource cts = new(UpdateTimeout); - //get or create a new session - DataStore = await Client.GetObjectAsync<Dictionary<string, string>>(SessionID, cancellationToken: cts.Token); - } - /// <summary> - /// Updates the current sessin agaisnt the cache store - /// </summary> - /// <returns>A task that complets when the update has completed</returns> - protected virtual async Task ProcessUpdateAsync() - { - //Setup timeout cancellation for the update, to cancel it - using CancellationTokenSource cts = new(UpdateTimeout); - await Client.AddOrUpdateObjectAsync(SessionID, null, DataStore, cts.Token); - } + protected Exception? ErrorCause { get; set; } + /// <summary> - /// Delets the current session in the remote store + /// Initialzies a new session /// </summary> - /// <returns>A task that completes when instance has been deleted</returns> - protected virtual async Task ProcessDeleteAsync() + /// <param name="sessionId">The id of the current session</param> + /// <param name="initialData">The initial data</param> + /// <param name="isNew">A flag that determines if the session is considered new</param> + protected RemoteSession(string sessionId, IDictionary<string, string> initialData, bool isNew) { - //Setup timeout cancellation for the update, to cancel it - using CancellationTokenSource cts = new(UpdateTimeout); - try - { - await Client.DeleteObjectAsync(SessionID, cts.Token); - } - catch (ObjectNotFoundException) - { - //This is fine, if the object does not exist, nothing to invalidate - } + SessionID = sessionId; + DataStore = initialData; + IsNew = isNew; } - + + ///<inheritdoc/> + public override string SessionID { get; } + ///<inheritdoc/> public override DateTimeOffset Created { @@ -113,15 +77,14 @@ namespace VNLib.Plugins.Sessions.Cache.Client return DateTimeOffset.FromUnixTimeMilliseconds(unixMs); } - protected set => this.SetValueType(CREATED_TIME_ENTRY, value.ToUnixTimeMilliseconds()); - } - + set => this.SetValueType(CREATED_TIME_ENTRY, value.ToUnixTimeMilliseconds()); + } ///<inheritdoc/> protected override string IndexerGet(string key) { //Get the value at the key or an empty string as a default - return DataStore!.GetValueOrDefault(key, string.Empty); + return DataStore.GetValueOrDefault(key) ?? string.Empty; } ///<inheritdoc/> protected override void IndexerSet(string key, string value) @@ -130,59 +93,60 @@ namespace VNLib.Plugins.Sessions.Cache.Client if (value == null) { //Set modified flag - IsModified |= DataStore!.Remove(key); + IsModified |= DataStore.Remove(key); } else { //Store the value at the specified key - DataStore![key] = value; + DataStore[key] = value; IsModified = true; } } + - - /* - * If the data-store is not found it means the session does not - * exist in cache, so its technically not dangerous to reuse, - * so the new mask needs to be set, but the old ID is going - * to be reused - */ + ///<inheritdoc/> + public virtual SessionStatus GetStatus() + { + SessionStatus status = SessionStatus.None; - /// <summary> - /// Waits for exclusive access to the session, and initializes - /// session data (loads it from the remote store) - /// </summary> - /// <param name="entity">The event to attach a session to</param> - /// <param name="cancellationToken">A token to cancel the operaion</param> - /// <returns></returns> - public virtual async Task WaitAndLoadAsync(IHttpEvent entity, CancellationToken cancellationToken) + status |= Flags.IsSet(INVALID_MSK) ? SessionStatus.Delete : SessionStatus.None; + status |= Flags.IsSet(REGEN_ID_MSK) ? SessionStatus.RegenId : SessionStatus.None; + status |= Flags.IsSet(MODIFIED_MSK) ? SessionStatus.UpdateOnly: SessionStatus.None; + + return status; + } + + ///<inheritdoc/> + public virtual IDictionary<string, string> GetSessionData() => DataStore; + + ///<inheritdoc/> + public virtual void Destroy(Exception? cause) { - //Wait for exclusive access - await base.WaitOneAsync(cancellationToken); - try - { - //Lazily initalize the current instance - await Initializer.InitializeAsync(cancellationToken); - //See if data-store is null (new session was created - if (DataStore == null) - { - //New session was created - DataStore = new(10); - //Set is-new flag - Flags.Set(IS_NEW_MSK); - //Set created time - Created = DateTimeOffset.UtcNow; - //Init ipaddress - UserIP = entity.Server.GetTrustedIp(); - //Set modified flag so session will be updated - IsModified = true; - } - } - catch - { - MainLock.Release(); - throw; - } + //Set invalid status + ErrorCause = cause; + Flags.Set(INVALID_MSK); + + //Clear all session data + DataStore.Clear(); + } + + ///<inheritdoc/> + public virtual bool IsValid(out Exception? cause) + { + /* + * Were reusing the invalid mask assuming that when a session is invalidated + * it will be deleted and destroyed + */ + + cause = ErrorCause; + return !Flags.IsSet(INVALID_MSK); + } + + ///<inheritdoc/> + public virtual void SessionUpdateComplete() + { + //Reset flags + Flags.ClearAll(); } } } diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs deleted file mode 100644 index 20ea947..0000000 --- a/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs +++ /dev/null @@ -1,251 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Sessions.Cache.Client -* File: SessionCacheClient.cs -* -* SessionCacheClient.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -using VNLib.Net.Http; -using VNLib.Utils.Async; -using VNLib.Utils.Logging; -using VNLib.Utils.Memory.Caching; -using VNLib.Plugins.Essentials.Sessions; - -namespace VNLib.Plugins.Sessions.Cache.Client -{ - - /// <summary> - /// A client that allows access to sessions located on external servers - /// </summary> - public abstract class SessionCacheClient : ICacheHolder - { - public class LRUSessionStore<T> : LRUCache<string, T>, ICacheHolder where T : ISession - { - internal AsyncQueue<T> ExpiredSessions { get; } - - ///<inheritdoc/> - public override bool IsReadOnly => false; - ///<inheritdoc/> - protected override int MaxCapacity { get; } - - - public LRUSessionStore(int maxCapacity) : base(StringComparer.Ordinal) - { - MaxCapacity = maxCapacity; - ExpiredSessions = new (true, true); - } - - ///<inheritdoc/> - protected override bool CacheMiss(string key, [NotNullWhen(true)] out T? value) - { - value = default; - return false; - } - - ///<inheritdoc/> - protected override void Evicted(ref KeyValuePair<string, T> evicted) - { - //add to queue, the list lock should be held during this operatio - _ = ExpiredSessions.TryEnque(evicted.Value); - } - - ///<inheritdoc/> - public void CacheClear() - { - foreach (KeyValuePair<string, T> value in List) - { - KeyValuePair<string, T> onStack = value; - - Evicted(ref onStack); - } - - Clear(); - } - - ///<inheritdoc/> - public void CacheHardClear() - { - CacheClear(); - } - } - - protected readonly LRUSessionStore<RemoteSession> CacheTable; - protected readonly object CacheLock; - protected readonly int MaxLoadedEntires; - - /// <summary> - /// The client used to communicate with the cache server - /// </summary> - protected IRemoteCacheStore Store { get; } - - /// <summary> - /// Gets a value that determines if the backing <see cref="IRemoteCacheStore"/> is connected - /// to a server - /// </summary> - public bool IsConnected => Store.IsConnected; - - /// <summary> - /// Initializes a new <see cref="SessionCacheClient"/> - /// </summary> - /// <param name="client"></param> - /// <param name="maxCacheItems">The maximum number of sessions to keep in memory</param> - protected SessionCacheClient(IRemoteCacheStore client, int maxCacheItems) - { - MaxLoadedEntires = maxCacheItems; - CacheLock = new(); - CacheTable = new(maxCacheItems); - Store = client; - } - - private ulong _waitingCount; - - /// <summary> - /// The number of pending connections waiting for results from the cache server - /// </summary> - public ulong WaitingConnections => _waitingCount; - - /// <summary> - /// Attempts to get a session from the cache identified by its sessionId asynchronously - /// </summary> - /// <param name="entity">The connection/request to attach the session to</param> - /// <param name="sessionId">The ID of the session to retrieve</param> - /// <param name="cancellationToken">A token to cancel the operation</param> - /// <returns>A <see cref="ValueTask"/> that resolves the remote session</returns> - /// <exception cref="SessionException"></exception> - public virtual async ValueTask<RemoteSession> GetSessionAsync(IHttpEvent entity, string sessionId, CancellationToken cancellationToken) - { - try - { - RemoteSession? session; - //Aquire lock on cache - lock (CacheLock) - { - //See if session is loaded into cache - if (!CacheTable.TryGetValue(sessionId, out session)) - { - //Init new record - session = SessionCtor(sessionId); - //Add to cache - CacheTable.Add(session.SessionID, session); - } - //Valid entry found in cache - } - - //Inc waiting count - Interlocked.Increment(ref _waitingCount); - - try - { - //Load session-data - await session.WaitAndLoadAsync(entity, cancellationToken); - return session; - } - catch - { - //Remove the invalid cached session - lock (CacheLock) - { - _ = CacheTable.Remove(sessionId); - } - throw; - } - finally - { - //Dec waiting count - Interlocked.Decrement(ref _waitingCount); - } - } - catch (SessionException) - { - throw; - } - catch (OperationCanceledException) - { - throw; - } - //Wrap exceptions - catch (Exception ex) - { - throw new SessionException("An unhandled exception was raised", ex); - } - } - - /// <summary> - /// Gets a new <see cref="RemoteSession"/> instances for the given sessionId, - /// and places it a the head of internal cache - /// </summary> - /// <param name="sessionId">The session identifier</param> - /// <returns>The new session for the given ID</returns> - protected abstract RemoteSession SessionCtor(string sessionId); - - /// <summary> - /// Begins waiting for expired sessions to be evicted from the cache table that - /// may have pending synchronization operations - /// </summary> - /// <param name="log"></param> - /// <param name="token"></param> - /// <returns></returns> - public async Task CleanupExpiredSessionsAsync(ILogProvider log, CancellationToken token) - { - 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); - } - } - } - - ///<inheritdoc/> - public void CacheClear() - { - - } - ///<inheritdoc/> - public void CacheHardClear() - { - //Cleanup cache when disconnected - lock (CacheLock) - { - CacheTable.CacheHardClear(); - } - } - } -} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionDataSerialzer.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionDataSerialzer.cs new file mode 100644 index 0000000..3af8641 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionDataSerialzer.cs @@ -0,0 +1,150 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: SessionDataSerialzer.cs +* +* SessionDataSerialzer.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Text; +using System.Buffers; +using System.Collections.Generic; + +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using VNLib.Data.Caching; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + + /// <summary> + /// Very basic session data serializer memory optimized for key-value + /// string pairs + /// </summary> + internal sealed class SessionDataSerialzer : ICacheObjectSerialzer, ICacheObjectDeserialzer + { + const string KV_DELIMITER = "\0\0"; + + object? ICacheObjectDeserialzer.Deserialze(Type type, ReadOnlySpan<byte> buffer) + { + if (!type.IsAssignableTo(typeof(IDictionary<string, string>))) + { + throw new NotSupportedException("This deserialzer only supports IDictionary<string,string>"); + } + + //Get char count from bin buffer + int charCount = Encoding.UTF8.GetCharCount(buffer); + + //Alloc decode buffer + using UnsafeMemoryHandle<char> charBuffer = MemoryUtil.UnsafeAllocNearestPage<char>(charCount, true); + + //decode chars + Encoding.UTF8.GetChars(buffer, charBuffer.Span); + + //Alloc new dict to write strings to + Dictionary<string, string> output = new(); + + //Reader to track position of char buffer + ForwardOnlyReader<char> reader = new(charBuffer.Span[0..charCount]); + + //Read data from the object data buffer + while (reader.WindowSize > 0) + { + //get index of next separator + int sep = GetNextToken(ref reader); + + //No more separators are found, skip + if (sep == -1) + { + break; + } + + //Get pointer to key before reading value + ReadOnlySpan<char> key = reader.Window[0..sep]; + + //Advance reader to next sequence + reader.Advance(sep + 2); + + //Find next sepearator to recover the value + sep = GetNextToken(ref reader); + + if (sep == -1) + { + break; + } + + //Store value + ReadOnlySpan<char> value = reader.Window[0..sep]; + + //Set the kvp in the dict + output[key.ToString()] = value.ToString(); + + //Advance reader again + reader.Advance(sep + 2); + } + + return output; + } + + private static int GetNextToken(ref ForwardOnlyReader<char> reader) => reader.Window.IndexOf(KV_DELIMITER); + + void ICacheObjectSerialzer.Serialize<T>(T obj, IBufferWriter<byte> finiteWriter) + { + if(obj is not Dictionary<string, string> dict) + { + throw new NotSupportedException("Data type is not supported by this serializer"); + } + + //Alloc char buffer, sessions should be under 16k + using UnsafeMemoryHandle<char> charBuffer = MemoryUtil.UnsafeAllocNearestPage<char>(16 * 1024); + + using Dictionary<string, string>.Enumerator e = dict.GetEnumerator(); + + ForwardOnlyWriter<char> writer = new(charBuffer.Span); + + while (e.MoveNext()) + { + KeyValuePair<string, string> element = e.Current; + + /* + * confim there is enough room in the writer, if there is not + * flush to the buffer writer + */ + if(element.Key.Length + element.Value.Length + 4 > writer.RemainingSize) + { + //Flush to the output + Encoding.UTF8.GetBytes(writer.AsSpan(), finiteWriter); + + //Reset the writer + writer.Reset(); + } + + //Add key/value elements + writer.Append(element.Key); + writer.Append(KV_DELIMITER); + writer.Append(element.Value); + writer.Append(KV_DELIMITER); + } + + //encode remaining data + Encoding.UTF8.GetBytes(writer.AsSpan(), finiteWriter); + } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionSerializer.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionSerializer.cs new file mode 100644 index 0000000..8634c39 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionSerializer.cs @@ -0,0 +1,352 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: SessionSerializer.cs +* +* SessionSerializer.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils.Memory.Caching; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// Concrete <see cref="ISessionSerialzer{TSession}"/> that provides + /// access serialization for session types + /// </summary> + /// <typeparam name="TSession">The session type</typeparam> + public class SessionSerializer<TSession> : ISessionSerialzer<TSession>, ICacheHolder where TSession : IRemoteSession + { + + private readonly object StoreLock; + private readonly Stack<WaitEntry> _entryPool; + private readonly Dictionary<string, WaitEntry> _waitStore; + private readonly int MaxPoolSize; + + /// <summary> + /// Initializes a new <see cref="SessionSerializer{TSession}"/> + /// </summary> + /// <param name="poolCapacity">The maximum number of <see cref="WaitEntry"/> elements to pool</param> + public SessionSerializer(int poolCapacity) + { + StoreLock = new(); + _entryPool = new Stack<WaitEntry>(poolCapacity); + + //Session-ids are security senstive, we must use ordinal(binary) string comparison + _waitStore = new Dictionary<string, WaitEntry>(poolCapacity, StringComparer.Ordinal); + + MaxPoolSize = poolCapacity; + } + + ///<inheritdoc/> + public virtual bool TryGetSession(string sessionId, [NotNullWhen(true)] out TSession? session) + { + lock (StoreLock) + { + //Try to see if an entry is loaded, and get the session + bool result = _waitStore.TryGetValue(sessionId, out WaitEntry? entry); + session = result ? entry!.Session : default; + return result; + } + } + + ///<inheritdoc/> + public virtual Task WaitAsync(TSession moniker, CancellationToken cancellation = default) + { + //Token must not be cancelled + cancellation.ThrowIfCancellationRequested(); + + WaitEnterToken token; + + lock (StoreLock) + { + //See if the entry already exists, otherwise get a new wait entry + if (!_waitStore.TryGetValue(moniker.SessionID, out WaitEntry? wait)) + { + GetWaitEntry(ref wait, moniker); + + //Add entry to store + _waitStore[moniker.SessionID] = wait; + } + + //Get waiter before leaving lock + token = wait.GetWaiter(); + } + + return token.EnterWaitAsync(cancellation); + } + + ///<inheritdoc/> + public virtual void Release(TSession moniker) + { + /* + * When releasing a lock on a moniker, we store entires in an internal table. Wait entires also require mutual + * exclustion to properly track waiters. This happens inside a single lock for lower entry times/complexity. + * The wait's internal semaphore may also cause longer waits within the lock, so wait entires are "prepared" + * by using tokens to access the wait/release mechanisms with proper tracking. + * + * Tokens can be used to control the wait because the call to release may cause thread yielding (if internal + * WaitHandle is being used), so we don't want to block other callers. + * + * When there are no more waiters for a moniker at the time the lock was entered, the WaitEntry is released + * back to the pool. + */ + + WaitReleaseToken releaser; + + lock (StoreLock) + { + WaitEntry entry = _waitStore[moniker.SessionID]; + + //Call release while holding store lock + if(entry.Release(out releaser) == 0) + { + //No more waiters + _waitStore.Remove(moniker.SessionID); + + /* + * We must release the semaphore before returning to pool, + * its safe because there are no more waiters + */ + releaser.Release(); + + ReturnEntry(entry); + + //already released + releaser = default; + } + } + //Release sem outside of lock + releaser.Release(); + } + + private void GetWaitEntry([NotNull] ref WaitEntry? wait, TSession session) + { + //Try to get wait from pool + if(!_entryPool.TryPop(out wait)) + { + wait = new(); + } + + //Init wait with session + wait.Prepare(session); + } + + private void ReturnEntry(WaitEntry entry) + { + //Remove session ref + entry.Prepare(default); + + if(_entryPool.Count < MaxPoolSize) + { + _entryPool.Push(entry); + } + else + { + //Dispose entry since were not storing it + entry.Dispose(); + } + } + + /// <summary> + /// NOOP + /// </summary> + public void CacheClear() + { } + + ///<inheritdoc/> + public void CacheHardClear() + { + //Take lock to remove the stored wait entires to dispose of them + WaitEntry[] pooled; + + lock (StoreLock) + { + pooled = _entryPool.ToArray(); + _entryPool.Clear(); + + //Cleanup the wait store + _waitStore.TrimExcess(MaxPoolSize); + } + + //Dispose entires + Array.ForEach(pooled, static pooled => pooled.Dispose()); + } + + /// <summary> + /// An entry within the lock table that + /// </summary> + protected sealed class WaitEntry : IDisposable + { + private uint _waitCount; + private readonly SemaphoreSlim _waitHandle; + + /// <summary> + /// The session this entry is providing mutual exclusion to + /// </summary> + public TSession? Session { get; private set; } + + /// <summary> + /// Initializes a new <see cref="WaitEntry"/> + /// </summary> + public WaitEntry() + { + _waitHandle = new(1, 1); + Session = default!; + } + + /// <summary> + /// Gets a token used to enter the lock which may block, or yield async + /// outside of a nested lock + /// </summary> + /// <returns>The waiter used to enter a wait on the moniker</returns> + public WaitEnterToken GetWaiter() + { + /* + * Increment wait count before entering the lock + * A cancellation is the only way out, so cover that + * during the async, only if the token is cancelable + */ + _ = Interlocked.Increment(ref _waitCount); + return new(this); + } + + /// <summary> + /// Prepares a release + /// </summary> + /// <param name="releaser"> + /// The token that should be used to release the exclusive lock held on + /// a moniker + /// </param> + /// <returns>The number of remaining waiters</returns> + public uint Release(out WaitReleaseToken releaser) + { + releaser = new(_waitHandle); + + //Decrement release count before leaving + return Interlocked.Decrement(ref _waitCount); + } + + /// <summary> + /// Prepres a new <see cref="WaitEntry"/> for + /// its new session. + /// </summary> + /// <param name="session">The session to hold a referrnce to</param> + public void Prepare(TSession? session) + { + Session = session; + _waitCount = 0; + } + + /* + * Called by WaitEnterToken to enter the lock + * outside a nested lock + */ + + internal Task WaitAsync(CancellationToken cancellation) + { + + //See if lock can be entered synchronously + if (_waitHandle.Wait(0, CancellationToken.None)) + { + //Lock was entered successfully without async yield + return Task.CompletedTask; + } + + //Lock must be entered async + + //Check to confirm cancellation may happen + if (cancellation.CanBeCanceled) + { + //Task may be cancelled, so we need to monitor the results to properly set waiting count + Task wait = _waitHandle.WaitAsync(cancellation); + return WaitForLockEntryWithCancellationAsync(wait); + } + else + { + //Task cannot be canceled, so we dont need to monitor the results + return _waitHandle.WaitAsync(CancellationToken.None); + } + } + + private async Task WaitForLockEntryWithCancellationAsync(Task wait) + { + try + { + await wait.ConfigureAwait(false); + } + catch + { + //Decrement wait count on error entering lock async + _ = Interlocked.Decrement(ref _waitCount); + throw; + } + } + + ///<inheritdoc/> + public void Dispose() + { + _waitHandle.Dispose(); + GC.SuppressFinalize(this); + } + } + + /// <summary> + /// A token used to safely release an exclusive lock inside the + /// <see cref="WaitEntry"/> + /// </summary> + protected readonly ref struct WaitReleaseToken + { + private readonly SemaphoreSlim? _sem; + + internal WaitReleaseToken(SemaphoreSlim sem) => _sem = sem; + + /// <summary> + /// Releases the exclusive lock held by the token. NOTE: + /// this method may only be called ONCE after a wait has been + /// released + /// </summary> + public readonly void Release() => _sem?.Release(); + } + + /// <summary> + /// A token used to safely enter a wait for exclusive access to a <see cref="WaitEntry"/> + /// </summary> + protected readonly ref struct WaitEnterToken + { + private readonly WaitEntry _entry; + + internal WaitEnterToken(WaitEntry entry) => _entry = entry; + + /// <summary> + /// Enters the wait for the WaitEntry. This method may not block + /// or yield (IE Return <see cref="Task.CompletedTask"/>) + /// </summary> + /// <param name="cancellation">A token to cancel the wait for the resource</param> + /// <returns></returns> + public Task EnterWaitAsync(CancellationToken cancellation) => _entry.WaitAsync(cancellation); + } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStatus.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStatus.cs new file mode 100644 index 0000000..1ab880c --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStatus.cs @@ -0,0 +1,53 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: SessionStatus.cs +* +* SessionStatus.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// Flags for determining the status of an HTTP session + /// before it is releases/post processed + /// </summary> + [Flags] + public enum SessionStatus + { + /// <summary> + /// The session has not been modified and does not need attention + /// </summary> + None = 0, + /// <summary> + /// The session is no longer valid and should be deleted + /// </summary> + Delete = 1, + /// <summary> + /// The session has been modified and requires its data be published + /// </summary> + UpdateOnly = 2, + /// <summary> + /// The session has been modified and requires an ID change + /// </summary> + RegenId = 4 + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStore.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStore.cs new file mode 100644 index 0000000..62de219 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStore.cs @@ -0,0 +1,312 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: SessionStore.cs +* +* SessionStore.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Data.Caching.Exceptions; +using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Sessions.Cache.Client.Exceptions; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + + /// <summary> + /// Provides an abstract <see cref="ISessionStore{TSession}"/> for managing sessions for HTTP connections + /// remote cache backed sessions + /// </summary> + /// <typeparam name="TSession"></typeparam> + public abstract class SessionStore<TSession> : ISessionStore<TSession> where TSession: IRemoteSession + { +#nullable disable + + /* + * Default imple for serializer + */ + protected virtual ISessionSerialzer<TSession> Serializer { get; } = new SessionSerializer<TSession>(100); + + /// <summary> + /// The <see cref="ISessionIdFactory"/> that provides session ids for connections + /// </summary> + protected abstract ISessionIdFactory IdFactory { get; } + /// <summary> + /// The backing cache store + /// </summary> + protected abstract IRemoteCacheStore Cache { get; } + /// <summary> + /// The session factory, produces sessions from their initial data and session-id + /// </summary> + protected abstract ISessionFactory<TSession> SessionFactory { get; } + /// <summary> + /// The log provider for writing background update exceptions to + /// </summary> + protected abstract ILogProvider Log { get; } + +#nullable enable + + ///<inheritdoc/> + public virtual async ValueTask<TSession?> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) + { + //Wrap exceptions in session exceptions + try + { + if (!IdFactory.CanService(entity)) + { + return default; + } + + //Get the session id from the entity + string? sessionId = IdFactory.TryGetSessionId(entity); + + //If regeneration is not supported, return since id is null + if (sessionId == null && !IdFactory.RegenerationSupported) + { + return default; + } + + if (sessionId == null) + { + //Get new sessionid + sessionId = IdFactory.RegenerateId(entity); + + //Create a new session + TSession? session = SessionFactory.GetNewSession(entity, sessionId, null); + + if (session != null) + { + //Enter wait for the new session, this call should not block or yield + await Serializer.WaitAsync(session, cancellationToken); + } + + return session; + } + else + { + cancellationToken.ThrowIfCancellationRequested(); + + Dictionary<string, string>? sessionData = null; + + //See if the session is currently in use, if so wait for access to it + if (Serializer.TryGetSession(sessionId, out TSession? activeSession)) + { + //reuse the session + await Serializer.WaitAsync(activeSession, cancellationToken); + + //after wait has been completed, check the status of the session + if (activeSession.IsValid(out Exception? cause)) + { + //Session is valid, we can use it + return activeSession; + } + + //release the wait, no use for the session in invalid state + Serializer.Release(activeSession); + + //Rethrow exception that cause invalidation + if (cause != null) + { + throw cause; + } + + //Regen id and continue loading + } + else + { + //Session cannot be found local, so we need to retrieve it from cache + sessionData = await Cache.GetObjectAsync<Dictionary<string, string>>(sessionId, cancellationToken); + } + + + //If the cache entry is null, we may choose to regenrate the session id + if (sessionData == null && IdFactory.RegenIdOnEmptyEntry) + { + sessionId = IdFactory.RegenerateId(entity); + } + + //Create a new session + TSession? session = SessionFactory.GetNewSession(entity, sessionId, sessionData); + + if(session != null) + { + //Enter wait for the new session + await Serializer.WaitAsync(session, cancellationToken); + } + + return session; + } + } + catch(OperationCanceledException) + { + throw; + } + catch (TimeoutException) + { + throw; + } + catch (SessionException) + { + throw; + } + catch(Exception ex) + { + throw new SessionException("An exception occured during session processing", ex); + } + } + + ///<inheritdoc/> + public virtual ValueTask ReleaseSessionAsync(TSession session, IHttpEvent entity) + { + //Get status on release + SessionStatus status = session.GetStatus(); + + //confirm the cache client is connected + if(status != SessionStatus.None && !Cache.IsConnected) + { + throw new SessionUpdateFailedException("The required session operation cannot be completed because the cache is not connected"); + } + + //Delete status is required + if (status.HasFlag(SessionStatus.Delete)) + { + //Delete the session + _ = DeleteSessionAsync(session); + } + else if (status.HasFlag(SessionStatus.RegenId)) + { + if (!IdFactory.RegenerationSupported) + { + throw new SessionException("Session id regeneration is not supported by this store"); + } + + //Get new id for session + string newId = IdFactory.RegenerateId(entity); + + //Update data and id + _ = UpdateSessionAndIdAsync(session, newId); + } + else if (status.HasFlag(SessionStatus.UpdateOnly)) + { + //Just run update + _ = UpdateSessionAndIdAsync(session, null); + } + else + { + //Always release the session after update + Serializer.Release(session); + } + + return ValueTask.CompletedTask; + } + + /// <summary> + /// Updates a mondified session on release, and optionally updates + /// its id + /// </summary> + /// <param name="session"></param> + /// <param name="newId">The session's new id</param> + /// <returns>A task that completes when the session update is complete</returns> + /// <remarks> + /// Unless overridden + /// </remarks> + protected virtual async Task UpdateSessionAndIdAsync(TSession session, string? newId) + { + try + { + //Get the session's data + IDictionary<string, string> sessionData = session.GetSessionData(); + + //Update the session's data async + await Cache.AddOrUpdateObjectAsync(session.SessionID, newId, sessionData); + + + /* + * If the session id changes, the old sesion can be invalidated + * and the session will be recovered from cache on load + */ + + if(sessionData != null) + { + session.Destroy(null); + } + else + { + session.SessionUpdateComplete(); + } + } + catch (Exception ex) + { + Log.Error("Exception raised during session update, ID {id} NewID {nid}\n{ex}", session.SessionID, newId, ex); + + //Destroy the session with an error + session.Destroy(ex); + } + finally + { + //Always release the session after update + Serializer.Release(session); + } + } + + /// <summary> + /// Delets a session when the session has the <see cref="SessionStatus.Delete"/> + /// flag set + /// </summary> + /// <param name="session">The session to delete</param> + /// <returns>A task that completes when the session is destroyed</returns> + protected virtual async Task DeleteSessionAsync(TSession session) + { + try + { + //Update the session's data async + await Cache.DeleteObjectAsync(session.SessionID); + + //Destroy the session + session.Destroy(null); + } + catch(ObjectNotFoundException) + { + //ingore onfe, if the session does not exist in cache + + //Destroy the session + session.Destroy(null); + } + catch (Exception ex) + { + Log.Error("Exception raised during session delete, ID {id}\n{ex}", session.SessionID, ex); + + //Destroy the session with an error + session.Destroy(ex); + } + finally + { + //Release the session now that delete has been set + Serializer.Release(session); + } + } + } +} |