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
|
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using VNLib.Net.Http;
using VNLib.Net.Sessions;
using VNLib.Utils;
using VNLib.Utils.Async;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Extensions;
#nullable enable
namespace VNLib.Plugins.Essentials.Sessions.Memory
{
/// <summary>
/// An <see cref="ISessionProvider"/> for in-process-memory backed sessions
/// </summary>
internal sealed class MemorySessionStore : ISessionProvider
{
private readonly Dictionary<string, MemorySession> SessionsStore;
internal readonly MemorySessionConfig Config;
internal readonly SessionIdFactory IdFactory;
public MemorySessionStore(MemorySessionConfig config)
{
Config = config;
SessionsStore = new(config.MaxAllowedSessions, StringComparer.Ordinal);
IdFactory = new(config.SessionIdSizeBytes, config.SessionCookieID, config.SessionTimeout);
}
///<inheritdoc/>
public async ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
{
static ValueTask SessionHandleClosedAsync(ISession session, IHttpEvent ev)
{
return (session as MemorySession)!.UpdateAndRelease(true, ev);
}
//Try to get the id for the session
if (IdFactory.TryGetSessionId(entity, out string? sessionId))
{
//Try to get the old record or evict it
ERRNO result = SessionsStore.TryGetOrEvictRecord(sessionId, out MemorySession session);
if(result > 0)
{
//Valid, now wait for exclusive access
await session.WaitOneAsync(cancellationToken);
return new (session, SessionHandleClosedAsync);
}
else
{
//try to cleanup expired records
GC();
//Make sure there is enough room to add a new session
if (SessionsStore.Count >= Config.MaxAllowedSessions)
{
entity.Server.SetNoCache();
//Set 503 when full
entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable);
//Cannot service new session
return new(null, FileProcessArgs.VirtualSkip, null);
}
//Initialze a new session
session = new(sessionId, entity.Server.GetTrustedIp(), UpdateSessionId);
//Increment the semaphore
(session as IWaitHandle).WaitOne();
//store the session in cache while holding semaphore, and set its expiration
SessionsStore.StoreRecord(session.SessionID, session, Config.SessionTimeout);
//Init new session handle
return new (session, SessionHandleClosedAsync);
}
}
else
{
return SessionHandle.Empty;
}
}
private string UpdateSessionId(IHttpEvent entity, string oldId)
{
//Generate and set a new sessionid
string newid = IdFactory.GenerateSessionId(entity);
//Aquire lock on cache
lock (SessionsStore)
{
//Change the cache lookup id
if (SessionsStore.Remove(oldId, out MemorySession? session))
{
SessionsStore.Add(newid, session);
}
}
return newid;
}
/// <summary>
/// Evicts all sessions from the current store
/// </summary>
public void Cleanup()
{
//Expire all old records to cleanup all entires
this.SessionsStore.CollectRecords(DateTime.MaxValue);
}
/// <summary>
/// Collects all expired records from the current store
/// </summary>
public void GC()
{
//collect expired records
this.SessionsStore.CollectRecords();
}
}
}
|