Skip to content

Commit dcbcc5d

Browse files
committed
Add thread-safe IP connection tracker
Introduces ConnectionTracker for limiting concurrent connections per IP address. Includes automatic cleanup of expired entries, logging, and proper resource management via IDisposable.
1 parent fb3617d commit dcbcc5d

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Net;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Zetian.Internal
9+
{
10+
/// <summary>
11+
/// Thread-safe connection tracker for IP-based connection limiting
12+
/// </summary>
13+
internal sealed class ConnectionTracker : IDisposable
14+
{
15+
private readonly ConcurrentDictionary<IPAddress, ConnectionInfo> _connections;
16+
private readonly ILogger _logger;
17+
private readonly int _maxConnectionsPerIp;
18+
private readonly Timer _cleanupTimer;
19+
private bool _disposed;
20+
21+
public ConnectionTracker(int maxConnectionsPerIp, ILogger logger)
22+
{
23+
_maxConnectionsPerIp = maxConnectionsPerIp;
24+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
25+
_connections = new ConcurrentDictionary<IPAddress, ConnectionInfo>();
26+
27+
// Cleanup expired connection infos every 5 minutes
28+
_cleanupTimer = new Timer(CleanupExpired, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
29+
}
30+
31+
/// <summary>
32+
/// Tries to acquire a connection slot for the given IP address
33+
/// </summary>
34+
/// <returns>A connection handle if successful, null if limit exceeded</returns>
35+
public async Task<ConnectionHandle?> TryAcquireAsync(IPAddress ipAddress, CancellationToken cancellationToken = default)
36+
{
37+
if (ipAddress == null)
38+
throw new ArgumentNullException(nameof(ipAddress));
39+
40+
var info = _connections.GetOrAdd(ipAddress, _ => new ConnectionInfo(_maxConnectionsPerIp));
41+
42+
// Try to acquire the semaphore
43+
var acquired = await info.Semaphore.WaitAsync(0, cancellationToken).ConfigureAwait(false);
44+
45+
if (acquired)
46+
{
47+
info.UpdateLastAccess();
48+
var currentCount = info.IncrementCount();
49+
50+
_logger.LogDebug("Connection acquired for {IPAddress}. Current count: {Count}/{Max}",
51+
ipAddress, currentCount, _maxConnectionsPerIp);
52+
53+
return new ConnectionHandle(ipAddress, info, this);
54+
}
55+
56+
_logger.LogWarning("Connection limit exceeded for {IPAddress}. Max: {Max}",
57+
ipAddress, _maxConnectionsPerIp);
58+
59+
return null;
60+
}
61+
62+
/// <summary>
63+
/// Gets the current connection count for an IP
64+
/// </summary>
65+
public int GetConnectionCount(IPAddress ipAddress)
66+
{
67+
if (_connections.TryGetValue(ipAddress, out var info))
68+
{
69+
return info.CurrentCount;
70+
}
71+
return 0;
72+
}
73+
74+
private void ReleaseConnection(IPAddress ipAddress, ConnectionInfo info)
75+
{
76+
try
77+
{
78+
var currentCount = info.DecrementCount();
79+
info.Semaphore.Release();
80+
81+
_logger.LogDebug("Connection released for {IPAddress}. Current count: {Count}",
82+
ipAddress, currentCount);
83+
84+
// If no more connections, consider removing the entry
85+
if (currentCount == 0 && info.CanBeRemoved())
86+
{
87+
if (_connections.TryRemove(ipAddress, out var removed))
88+
{
89+
removed.Dispose();
90+
}
91+
}
92+
}
93+
catch (Exception ex)
94+
{
95+
_logger.LogError(ex, "Error releasing connection for {IPAddress}", ipAddress);
96+
}
97+
}
98+
99+
private void CleanupExpired(object? state)
100+
{
101+
try
102+
{
103+
foreach (var kvp in _connections)
104+
{
105+
if (kvp.Value.CanBeRemoved() && kvp.Value.CurrentCount == 0)
106+
{
107+
if (_connections.TryRemove(kvp.Key, out var removed))
108+
{
109+
removed.Dispose();
110+
}
111+
}
112+
}
113+
}
114+
catch (Exception ex)
115+
{
116+
_logger.LogError(ex, "Error during cleanup");
117+
}
118+
}
119+
120+
public void Dispose()
121+
{
122+
if (_disposed)
123+
return;
124+
125+
_disposed = true;
126+
_cleanupTimer?.Dispose();
127+
128+
foreach (var kvp in _connections)
129+
{
130+
kvp.Value.Dispose();
131+
}
132+
_connections.Clear();
133+
}
134+
135+
/// <summary>
136+
/// Connection handle that ensures proper cleanup
137+
/// </summary>
138+
internal sealed class ConnectionHandle : IDisposable
139+
{
140+
private readonly IPAddress _ipAddress;
141+
private readonly ConnectionInfo _info;
142+
private readonly ConnectionTracker _tracker;
143+
private int _disposed;
144+
145+
internal ConnectionHandle(IPAddress ipAddress, ConnectionInfo info, ConnectionTracker tracker)
146+
{
147+
_ipAddress = ipAddress;
148+
_info = info;
149+
_tracker = tracker;
150+
}
151+
152+
public void Dispose()
153+
{
154+
if (Interlocked.Exchange(ref _disposed, 1) == 0)
155+
{
156+
_tracker.ReleaseConnection(_ipAddress, _info);
157+
}
158+
}
159+
}
160+
161+
/// <summary>
162+
/// Per-IP connection tracking information
163+
/// </summary>
164+
internal sealed class ConnectionInfo : IDisposable
165+
{
166+
private readonly SemaphoreSlim _semaphore;
167+
private int _currentCount;
168+
private DateTime _lastAccess;
169+
private readonly object _lock = new();
170+
171+
public ConnectionInfo(int maxConnections)
172+
{
173+
_semaphore = new SemaphoreSlim(maxConnections, maxConnections);
174+
_currentCount = 0;
175+
_lastAccess = DateTime.UtcNow;
176+
}
177+
178+
public SemaphoreSlim Semaphore => _semaphore;
179+
180+
public int CurrentCount
181+
{
182+
get
183+
{
184+
lock (_lock)
185+
{
186+
return _currentCount;
187+
}
188+
}
189+
}
190+
191+
public int IncrementCount()
192+
{
193+
lock (_lock)
194+
{
195+
return ++_currentCount;
196+
}
197+
}
198+
199+
public int DecrementCount()
200+
{
201+
lock (_lock)
202+
{
203+
_currentCount = Math.Max(0, _currentCount - 1);
204+
return _currentCount;
205+
}
206+
}
207+
208+
public void UpdateLastAccess()
209+
{
210+
lock (_lock)
211+
{
212+
_lastAccess = DateTime.UtcNow;
213+
}
214+
}
215+
216+
public bool CanBeRemoved()
217+
{
218+
lock (_lock)
219+
{
220+
// Remove if not accessed for 10 minutes and no active connections
221+
return _currentCount == 0 &&
222+
(DateTime.UtcNow - _lastAccess) > TimeSpan.FromMinutes(10);
223+
}
224+
}
225+
226+
public void Dispose()
227+
{
228+
_semaphore?.Dispose();
229+
}
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)