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 { /// /// Base class for cacheable lazy initialized session entires /// that exist in a remote caching server /// 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; /// /// The lazy loaded data-store /// protected Dictionary? DataStore; public RemoteSession(string sessionId, FBMClient client, TimeSpan backgroundTimeOut) { SessionID = sessionId; UpdateTimeout = backgroundTimeOut; Client = client; Initializer = new(InitializeAsync, null); } /// /// The data initializer, loads the data store from the connected cache server /// /// A task that completes when the get operation completes 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>(SessionID, cancellationToken: cts.Token); } /// /// Updates the current sessin agaisnt the cache store /// /// A task that complets when the update has completed 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); } /// /// Delets the current session in the remote store /// /// A task that completes when instance has been deleted 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 } } /// public override DateTimeOffset Created { get { //Deserialze the base32 ms long unixMs = this.GetValueType(CREATED_TIME_ENTRY); //set created time from ms return DateTimeOffset.FromUnixTimeMilliseconds(unixMs); } protected set => this.SetValueType(CREATED_TIME_ENTRY, value.ToUnixTimeMilliseconds()); } /// 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); } /// 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 */ /// /// Waits for exclusive access to the session, and initializes /// session data (loads it from the remote store) /// /// The event to attach a session to /// A token to cancel the operaion /// 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; } } /// protected override Task OnEvictedAsync() { //empty the dict to help the GC DataStore!.Clear(); return Task.CompletedTask; } } }