aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Sessions.Cache.Client
diff options
context:
space:
mode:
authorLibravatar vman <public@vaughnnugent.com>2022-10-30 02:28:12 -0400
committerLibravatar vman <public@vaughnnugent.com>2022-10-30 02:28:12 -0400
commita8510fb835dcc5e1142d700164ce5a4bd44e1a25 (patch)
tree28caab320f777a384cb6883b68dd999cdc8c0a3f /Libs/VNLib.Plugins.Sessions.Cache.Client
Add project files.
Diffstat (limited to 'Libs/VNLib.Plugins.Sessions.Cache.Client')
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/MessageTooLargeException.cs27
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionStatusException.cs19
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/Exceptions/SessionUpdateFailedException.cs17
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs173
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/SessionCacheClient.cs159
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj33
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.xml69
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>