/* * Copyright (c) 2022 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Sessions.Cache.Client * File: RemoteSession.cs * * RemoteSession.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 Microsoft.VisualStudio.Threading; using VNLib.Net.Http; using VNLib.Data.Caching.Exceptions; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Sessions; using VNLib.Plugins.Essentials.Extensions; 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 IRemoteCacheStore Client { get; } protected TimeSpan UpdateTimeout { get; } private readonly AsyncLazyInitializer Initializer; /// /// The lazy loaded data-store /// protected Dictionary? DataStore; protected RemoteSession(string sessionId, IRemoteCacheStore 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; } } } }