Files
panel/modules/empyrion-bridge/bridge/BridgePool.cs
T
2026-07-14 23:01:33 -07:00

86 lines
2.4 KiB
C#

using System.Collections.Concurrent;
namespace EmpyrionBridge;
// Lazy connection pool keyed by host:port. Reuses an EPM TCP connection
// across HTTP requests; drops it after IdleTtl with no traffic.
public class BridgePool : IAsyncDisposable
{
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(8);
public TimeSpan IdleTtl { get; init; } = TimeSpan.FromMinutes(5);
readonly ConcurrentDictionary<string, Entry> _entries = new();
readonly ILoggerFactory _logFactory;
readonly ILogger<BridgePool> _log;
readonly Timer _reaper;
public BridgePool(ILoggerFactory logFactory)
{
_logFactory = logFactory;
_log = logFactory.CreateLogger<BridgePool>();
_reaper = new Timer(Reap, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
public async Task<BridgeConnection> GetAsync(string host, int port)
{
var key = $"{host}:{port}";
Entry e;
lock (_entries)
{
if (!_entries.TryGetValue(key, out e))
{
e = new Entry { Conn = new BridgeConnection(host, port, _logFactory.CreateLogger("EPM")) };
_entries[key] = e;
}
e.LastUsed = DateTime.UtcNow;
}
if (!e.Conn.IsConnected)
{
await e.ConnectGate.WaitAsync();
try
{
if (!e.Conn.IsConnected)
{
await e.Conn.ConnectAsync(ConnectTimeout);
}
}
finally
{
e.ConnectGate.Release();
}
}
return e.Conn;
}
void Reap(object _)
{
var cutoff = DateTime.UtcNow - IdleTtl;
foreach (var kv in _entries.ToArray())
{
if (kv.Value.LastUsed < cutoff)
{
if (_entries.TryRemove(kv.Key, out var e))
{
_log.LogInformation("reaping idle connection {Key}", kv.Key);
e.Conn.Dispose();
}
}
}
}
public async ValueTask DisposeAsync()
{
await _reaper.DisposeAsync();
foreach (var e in _entries.Values) e.Conn.Dispose();
_entries.Clear();
}
class Entry
{
public BridgeConnection Conn = null!;
public DateTime LastUsed = DateTime.UtcNow;
public SemaphoreSlim ConnectGate = new(1, 1);
}
}