Files
scadalink-design/deprecated/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/Sessions/SessionManager.cs
Joseph Doherty 9dccf8e72f deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL
adapter files, and related docs to deprecated/. Removed LmxProxy registration
from DataConnectionFactory, project reference from DCL, protocol option from
UI, and cleaned up all requirement docs.
2026-04-08 15:56:23 -04:00

174 lines
5.9 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Serilog;
namespace ZB.MOM.WW.LmxProxy.Host.Sessions
{
/// <summary>
/// Tracks active client sessions in memory.
/// Thread-safe via ConcurrentDictionary.
/// </summary>
public sealed class SessionManager : IDisposable
{
private static readonly ILogger Log = Serilog.Log.ForContext<SessionManager>();
private readonly ConcurrentDictionary<string, SessionInfo> _sessions
= new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
private readonly Timer? _scavengingTimer;
private readonly TimeSpan _inactivityTimeout;
private Action<string>? _onSessionScavenged;
/// <summary>
/// Creates a SessionManager with optional inactivity scavenging.
/// </summary>
/// <param name="inactivityTimeoutMinutes">
/// Sessions inactive for this many minutes are automatically terminated.
/// Set to 0 to disable scavenging.
/// </param>
public SessionManager(int inactivityTimeoutMinutes = 5)
{
_inactivityTimeout = TimeSpan.FromMinutes(inactivityTimeoutMinutes);
if (inactivityTimeoutMinutes > 0)
{
// Check every 60 seconds
_scavengingTimer = new Timer(ScavengeInactiveSessions, null,
TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
}
}
/// <summary>
/// Register a callback invoked when a session is scavenged due to inactivity.
/// The callback receives the session ID.
/// </summary>
public void OnSessionScavenged(Action<string> callback)
{
_onSessionScavenged = callback;
}
/// <summary>Gets the count of active sessions.</summary>
public int ActiveSessionCount => _sessions.Count;
/// <summary>
/// Creates a new session.
/// Returns the 32-character hex GUID session ID.
/// </summary>
public string CreateSession(string clientId, string apiKey)
{
var sessionId = Guid.NewGuid().ToString("N"); // 32-char lowercase hex, no hyphens
var sessionInfo = new SessionInfo(sessionId, clientId, apiKey);
_sessions[sessionId] = sessionInfo;
Log.Information("Session created: {SessionId} for client {ClientId}", sessionId, clientId);
return sessionId;
}
/// <summary>
/// Validates a session ID. Updates LastActivity on success.
/// Returns true if the session exists.
/// </summary>
public bool ValidateSession(string sessionId)
{
if (_sessions.TryGetValue(sessionId, out var session))
{
session.TouchLastActivity();
return true;
}
return false;
}
/// <summary>
/// Terminates a session. Returns true if the session existed.
/// </summary>
public bool TerminateSession(string sessionId)
{
if (_sessions.TryRemove(sessionId, out _))
{
Log.Information("Session terminated: {SessionId}", sessionId);
return true;
}
return false;
}
/// <summary>Gets session info by ID, or null if not found.</summary>
public SessionInfo? GetSession(string sessionId)
{
_sessions.TryGetValue(sessionId, out var session);
return session;
}
/// <summary>Gets a snapshot of all active sessions.</summary>
public IReadOnlyList<SessionInfo> GetAllSessions()
{
return _sessions.Values.ToList().AsReadOnly();
}
/// <summary>
/// Scavenges sessions that have been inactive for longer than the timeout.
/// </summary>
private void ScavengeInactiveSessions(object? state)
{
if (_inactivityTimeout <= TimeSpan.Zero) return;
var cutoff = DateTime.UtcNow - _inactivityTimeout;
var expired = _sessions.Where(kvp => kvp.Value.LastActivity < cutoff).ToList();
foreach (var kvp in expired)
{
if (_sessions.TryRemove(kvp.Key, out _))
{
Log.Information("Session {SessionId} scavenged (inactive since {LastActivity})",
kvp.Key, kvp.Value.LastActivity);
try
{
_onSessionScavenged?.Invoke(kvp.Key);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in session scavenge callback for {SessionId}", kvp.Key);
}
}
}
}
public void Dispose()
{
_scavengingTimer?.Dispose();
_sessions.Clear();
}
}
/// <summary>
/// Information about an active client session.
/// </summary>
public class SessionInfo
{
public SessionInfo(string sessionId, string clientId, string apiKey)
{
SessionId = sessionId;
ClientId = clientId;
ApiKey = apiKey;
ConnectedAt = DateTime.UtcNow;
LastActivity = DateTime.UtcNow;
}
public string SessionId { get; }
public string ClientId { get; }
public string ApiKey { get; }
public DateTime ConnectedAt { get; }
public DateTime LastActivity { get; private set; }
public long ConnectedSinceUtcTicks => ConnectedAt.Ticks;
/// <summary>Updates the last activity timestamp to now.</summary>
public void TouchLastActivity()
{
LastActivity = DateTime.UtcNow;
}
}
}