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 { /// /// A client that allows access to sessions located on external servers /// public abstract class SessionCacheClient : VnDisposeable, ICacheHolder { public class LRUSessionStore : LRUCache 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 evicted) { //Evice record evicted.Value.Evicted(); } } protected readonly LRUSessionStore CacheTable; protected readonly object CacheLock; protected readonly int MaxLoadedEntires; protected FBMClient Client { get; } /// /// Initializes a new /// /// /// The maximum number of sessions to keep in memory 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(); /// /// Attempts to get a session from the cache identified by its sessionId asynchronously /// /// The connection/request to attach the session to /// The ID of the session to retrieve /// A token to cancel the operation /// A that resolves the remote session /// public virtual async ValueTask 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); } } /// /// Gets a new instances for the given sessionId, /// and places it a the head of internal cache /// /// The session identifier /// The new session for the given ID protected abstract RemoteSession SessionCtor(string sessionId); /// public void CacheClear() { } /// public void CacheHardClear() { //Cleanup cache when disconnected lock (CacheLock) { CacheTable.Clear(); foreach (RemoteSession session in (IEnumerable)CacheTable) { session.Evicted(); } CacheTable.Clear(); } } protected override void Free() { //Unsub from events Client.ConnectionClosed -= Client_ConnectionClosed; //Clear all cached sessions CacheHardClear(); } } }