feat(transport): in-memory BundleSessionStore with TTL + lockout
This commit is contained in:
74
src/ScadaLink.Transport/Import/BundleSessionStore.cs
Normal file
74
src/ScadaLink.Transport/Import/BundleSessionStore.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.Commons.Interfaces.Transport;
|
||||
using ScadaLink.Commons.Types.Transport;
|
||||
|
||||
namespace ScadaLink.Transport.Import;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of <see cref="IBundleSessionStore"/> backed by a
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}"/>. Sessions are evicted lazily
|
||||
/// at read time (<see cref="Get"/>) and on-demand via <see cref="EvictExpired"/>;
|
||||
/// there is no background timer.
|
||||
/// <para>
|
||||
/// TTL is supplied by the importer via <see cref="BundleSession.ExpiresAt"/>;
|
||||
/// this store does not impose its own. The injected <see cref="TimeProvider"/>
|
||||
/// is used purely to determine <c>now</c> when checking <c>ExpiresAt</c>, which
|
||||
/// keeps unit tests deterministic.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The 3-strike unlock lockout is owned by <see cref="BundleSession"/>
|
||||
/// (<c>FailedUnlockAttempts</c> / <c>Locked</c>); the store just hands out the
|
||||
/// shared session reference so the importer can mutate the counter in place.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class BundleSessionStore : IBundleSessionStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, BundleSession> _sessions = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
// Options are accepted to honor the documented constructor contract and to
|
||||
// be ready for future per-store knobs (e.g. max in-flight sessions). The
|
||||
// current store does not read any field — TTL is on the session itself.
|
||||
public BundleSessionStore(IOptions<TransportOptions> options, TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_ = options.Value;
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public BundleSession Open(BundleSession session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
// Overwrite on collision is defensive: GUIDs are random so practical
|
||||
// collisions don't happen, but if a caller reuses an id we always
|
||||
// honor the latest Open call.
|
||||
_sessions[session.SessionId] = session;
|
||||
return session;
|
||||
}
|
||||
|
||||
public BundleSession? Get(Guid sessionId)
|
||||
{
|
||||
if (!_sessions.TryGetValue(sessionId, out var session)) return null;
|
||||
if (session.ExpiresAt > _timeProvider.GetUtcNow()) return session;
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Remove(Guid sessionId)
|
||||
{
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
}
|
||||
|
||||
public void EvictExpired()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
foreach (var kv in _sessions)
|
||||
{
|
||||
if (kv.Value.ExpiresAt <= now)
|
||||
{
|
||||
_sessions.TryRemove(kv.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.Commons.Interfaces.Transport;
|
||||
using ScadaLink.Transport.Export;
|
||||
using ScadaLink.Transport.Import;
|
||||
|
||||
namespace ScadaLink.Transport;
|
||||
|
||||
@@ -12,7 +15,9 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
services.AddOptions<TransportOptions>().BindConfiguration(OptionsSection);
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.AddScoped<DependencyResolver>();
|
||||
services.AddSingleton<IBundleSessionStore, BundleSessionStore>();
|
||||
// Remaining concrete services added in later tasks.
|
||||
return services;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user