diff options
author | vnugent <public@vaughnnugent.com> | 2023-03-09 01:48:40 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-03-09 01:48:40 -0500 |
commit | d673bd34945699df96e38c54f70352608430fbc4 (patch) | |
tree | dd5e17d02f3fe73e4d1a54689bd9c7d41f1a5a71 /libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStore.cs | |
parent | 11a8cea8a6445bd5127eb4c97fc582cd944f72ea (diff) |
Omega cache, session, and account provider complete overhaul
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStore.cs')
-rw-r--r-- | libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionStore.cs | 312 |
1 files changed, 312 insertions, 0 deletions
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); + } + } + } +} |