diff options
Diffstat (limited to 'Libs/VNLib.Plugins.Sessions.Cache.Client')
7 files changed, 497 insertions, 0 deletions
diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/MessageTooLargeException.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/MessageTooLargeException.cs new file mode 100644 index 0000000..15164ca --- /dev/null +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/MessageTooLargeException.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.Serialization; + +using VNLib.Net.Messaging.FBM; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// <summary> + /// Raised when a request message is too large to send to + /// the server and the server may close the connection. + /// </summary> + public class MessageTooLargeException : FBMException + { + ///<inheritdoc/> + public MessageTooLargeException() + {} + ///<inheritdoc/> + public MessageTooLargeException(string message) : base(message) + {} + ///<inheritdoc/> + public MessageTooLargeException(string message, Exception innerException) : base(message, innerException) + {} + ///<inheritdoc/> + protected MessageTooLargeException(SerializationInfo info, StreamingContext context) : base(info, context) + {} + } +} diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs new file mode 100644 index 0000000..5bb6f42 --- /dev/null +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.Serialization; + +using VNLib.Plugins.Essentials.Sessions; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + public class SessionStatusException : SessionException + { + public SessionStatusException() + {} + public SessionStatusException(string message) : base(message) + {} + public SessionStatusException(string message, Exception innerException) : base(message, innerException) + {} + protected SessionStatusException(SerializationInfo info, StreamingContext context) : base(info, context) + {} + } +} diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionUpdateFailedException.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionUpdateFailedException.cs new file mode 100644 index 0000000..1b842b7 --- /dev/null +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionUpdateFailedException.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.Serialization; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + public class SessionUpdateFailedException : SessionStatusException + { + public SessionUpdateFailedException() + {} + public SessionUpdateFailedException(string message) : base(message) + {} + public SessionUpdateFailedException(string message, Exception innerException) : base(message, innerException) + {} + protected SessionUpdateFailedException(SerializationInfo info, StreamingContext context) : base(info, context) + {} + } +} diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs new file mode 100644 index 0000000..2ab27f5 --- /dev/null +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs @@ -0,0 +1,173 @@ +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; +using VNLib.Data.Caching.Exceptions; +using VNLib.Utils.Extensions; +using VNLib.Net.Messaging.FBM.Client; +using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Essentials.Extensions; + +#nullable enable + +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 + { + protected const string CREATED_TIME_ENTRY = "__.i.ctime"; + + protected readonly FBMClient Client; + protected readonly TimeSpan UpdateTimeout; + + private readonly AsyncLazyInitializer Initializer; + + /// <summary> + /// The lazy loaded data-store + /// </summary> + protected Dictionary<string, string>? DataStore; + + public RemoteSession(string sessionId, FBMClient client, TimeSpan backgroundTimeOut) + { + SessionID = sessionId; + UpdateTimeout = backgroundTimeOut; + Client = client; + Initializer = new(InitializeAsync, null); + } + + /// <summary> + /// The data initializer, loads the data store from the connected cache server + /// </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); + } + /// <summary> + /// Delets the current session in the remote store + /// </summary> + /// <returns>A task that completes when instance has been deleted</returns> + protected virtual async Task ProcessDeleteAsync() + { + //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 + } + } + + ///<inheritdoc/> + public override DateTimeOffset Created + { + get + { + //Deserialze the base32 ms + long unixMs = this.GetValueType<string, long>(CREATED_TIME_ENTRY); + //set created time from ms + return DateTimeOffset.FromUnixTimeMilliseconds(unixMs); + } + + protected 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); + } + ///<inheritdoc/> + protected override void IndexerSet(string key, string value) + { + //If the value is null, remove the key from the store + if (value == null) + { + //Set modified flag + IsModified |= DataStore!.Remove(key); + } + else + { + //Store the value at the specified key + 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 + */ + + /// <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) + { + //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; + } + } + ///<inheritdoc/> + protected override Task OnEvictedAsync() + { + //empty the dict to help the GC + DataStore!.Clear(); + return Task.CompletedTask; + } + } +} diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs b/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs new file mode 100644 index 0000000..de0e370 --- /dev/null +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs @@ -0,0 +1,159 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils; +using VNLib.Utils.Memory.Caching; +using VNLib.Net.Http; +using VNLib.Net.Messaging.FBM.Client; +using VNLib.Plugins.Essentials.Sessions; + +#nullable enable + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + + /// <summary> + /// A client that allows access to sessions located on external servers + /// </summary> + public abstract class SessionCacheClient : VnDisposeable, ICacheHolder + { + public class LRUSessionStore<T> : LRUCache<string, T> where T : ISession, ICacheable + { + public override bool IsReadOnly => false; + protected override int MaxCapacity { get; } + + public LRUSessionStore(int maxCapacity) : base(StringComparer.Ordinal) => MaxCapacity = maxCapacity; + + protected override bool CacheMiss(string key, [NotNullWhen(true)] out T? value) + { + value = default; + return false; + } + protected override void Evicted(KeyValuePair<string, T> evicted) + { + //Evice record + evicted.Value.Evicted(); + } + } + + protected readonly LRUSessionStore<RemoteSession> CacheTable; + protected readonly object CacheLock; + protected readonly int MaxLoadedEntires; + + protected FBMClient Client { get; } + + /// <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> + public SessionCacheClient(FBMClient client, int maxCacheItems) + { + MaxLoadedEntires = maxCacheItems; + CacheLock = new(); + CacheTable = new(maxCacheItems); + Client = client; + //Listen for close events + Client.ConnectionClosed += Client_ConnectionClosed; + } + + private void Client_ConnectionClosed(object? sender, EventArgs e) => CacheHardClear(); + + /// <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) + { + Check(); + 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 + } + try + { + //Load session-data + await session.WaitAndLoadAsync(entity, cancellationToken); + return session; + } + catch + { + //Remove the invalid cached session + lock (CacheLock) + { + _ = CacheTable.Remove(sessionId); + } + throw; + } + } + 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); + + ///<inheritdoc/> + public void CacheClear() + { + + } + ///<inheritdoc/> + public void CacheHardClear() + { + //Cleanup cache when disconnected + lock (CacheLock) + { + CacheTable.Clear(); + foreach (RemoteSession session in (IEnumerable<RemoteSession>)CacheTable) + { + session.Evicted(); + } + CacheTable.Clear(); + } + } + + protected override void Free() + { + //Unsub from events + Client.ConnectionClosed -= Client_ConnectionClosed; + //Clear all cached sessions + CacheHardClear(); + } + } +} diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj new file mode 100644 index 0000000..0c12cec --- /dev/null +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj @@ -0,0 +1,33 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <Platforms>AnyCPU;x64</Platforms> + <Authors>Vaughn Nugent</Authors> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <Version>1.0.0.1</Version> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <DocumentationFile></DocumentationFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.3.44" /> + <PackageReference Include="RestSharp" Version="108.0.2" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\..\VNLib\Essentials\VNLib.Plugins.Essentials.csproj" /> + <ProjectReference Include="..\..\..\DataCaching\VNLib.Data.Caching\src\VNLib.Data.Caching.csproj" /> + </ItemGroup> + +</Project> diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.xml b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.xml new file mode 100644 index 0000000..95fafd7 --- /dev/null +++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.xml @@ -0,0 +1,69 @@ +<?xml version="1.0"?> +<doc> + <assembly> + <name>VNLib.Plugins.Sessions.Cache.Client</name> + </assembly> + <members> + <member name="T:VNLib.Plugins.Sessions.Cache.Client.MessageTooLargeException"> + <summary> + Raised when a request message is too large to send to + the server and the server may close the connection. + </summary> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.MessageTooLargeException.#ctor"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.MessageTooLargeException.#ctor(System.String)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.MessageTooLargeException.#ctor(System.String,System.Exception)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.MessageTooLargeException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)"> + <inheritdoc/> + </member> + <member name="T:VNLib.Plugins.Sessions.Cache.Client.SessionClient"> + <summary> + A client that allows access to sessions located on external servers + </summary> + </member> + <member name="P:VNLib.Plugins.Sessions.Cache.Client.SessionClient.GetSessionId"> + <summary> + A callback that produces a session-id from the connection (or a new id if needed) + </summary> + </member> + <member name="P:VNLib.Plugins.Sessions.Cache.Client.SessionClient.NewSessionId"> + <summary> + A callback that produces a new session-id for the connection (and updates the client if necessary) + </summary> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.SessionClient.#ctor(System.Int32,System.Int32,System.Int32,VNLib.Utils.Logging.ILogProvider,VNLib.Utils.Memory.PrivateHeap)"> + <summary> + Initializes a new <see cref="T:VNLib.Plugins.Sessions.Cache.Client.SessionClient"/> + </summary> + <param name="maxMessageSize">The maxium message size (in bytes) the client will allow receiving (maximum data size for sessions)</param> + <param name="recvBufferSize">The size (in bytes) of the client message receive buffer</param> + <param name="maxCacheItems">The maximum number of sessions to keep in memory</param> + <param name="log">A <see cref="T:VNLib.Utils.Logging.ILogProvider"/> to write log events to</param> + <param name="heap">The <see cref="T:VNLib.Utils.Memory.PrivateHeap"/> to allocate buffers from</param> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.SessionClient.GetSessionAsync(VNLib.Net.Http.HttpEvent,System.Threading.CancellationToken)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.SessionClient.CacheClear"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.SessionClient.CacheHardClear"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.SessionClient.OnConnected"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.SessionClient.OnError(VNLib.Net.Messaging.FBM.Client.FMBClientErrorEventArgs)"> + <inheritdoc/> + </member> + <member name="M:VNLib.Plugins.Sessions.Cache.Client.SessionClient.OnDisconnected"> + <inheritdoc/> + </member> + </members> +</doc> |