aboutsummaryrefslogtreecommitdiff
path: root/libs/VNLib.Plugins.Sessions.Cache.Client/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.Cache.Client/src')
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs18
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs4
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteSession.cs73
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionFactory.cs47
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionIdFactory.cs74
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionSerialzer.cs46
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/ISessionStore.cs60
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs170
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs251
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionDataSerialzer.cs150
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionSerializer.cs352
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStatus.cs53
-rw-r--r--libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStore.cs312
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);
+ }
+ }
+ }
+}