aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Sessions.Cache.Client/RemoteSession.cs
blob: e2a972558ff91e359941801b3f00a4a986bab34c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/*
* 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;
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;
        }
    }
}