Skip to content

Commit a4b9c7c

Browse files
committed
scaling
1 parent 66be5a9 commit a4b9c7c

5 files changed

Lines changed: 165 additions & 49 deletions

File tree

src/RESPite/Streams/RespStream.Read.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,12 @@ private protected bool OnAfterReceive(int read, bool inline)
118118
}
119119
catch (Exception ex)
120120
{
121-
OnCleanup(SocketError.Fault, ex);
121+
OnReceiveCleanup(SocketError.Fault, ex);
122122
return false;
123123
}
124124
}
125125

126-
private protected void OnCleanup(SocketError error, Exception? fault = null)
126+
private protected virtual void OnReceiveCleanup(SocketError error, Exception? fault = null)
127127
{
128128
try
129129
{

toys/RESPite.Proxy/Program.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,29 @@
77
var proxyOptions = new ProxyServerOptions
88
{
99
Password = "letmein",
10+
UpstreamConnectionCount = 5,
1011
};
11-
using var pool = new WorkerPool();
12-
pool.AddLog(Console.WriteLine);
12+
using var pool = new WorkerPool(workers: 0); // use CPU count
13+
pool.AddDebugLog(Console.WriteLine);
1314
var proxy = new ProxyServer(proxyOptions, applicationLifetime: null);
1415
using var socket = new ProxySocketServer(proxy, pool);
1516
socket.Start(new IPEndPoint(IPAddress.Loopback, 6380));
16-
Console.WriteLine("Listening...");
17-
Console.ReadLine();
17+
18+
ulong lastOpCount = 0;
19+
int lastActiveClients = 0;
20+
bool first = true;
21+
while (true)
22+
{
23+
var opCount = proxy.GetOpCount(out var activeClients);
24+
if (first || opCount != lastOpCount || activeClients != lastActiveClients)
25+
{
26+
Console.WriteLine($"Active clients: {activeClients}; commands processed: {opCount}");
27+
lastOpCount = opCount;
28+
lastActiveClients = activeClients;
29+
first = false;
30+
}
31+
await Task.Delay(TimeSpan.FromSeconds(5));
32+
}
1833
/*
1934
2035
var builder = WebApplication.CreateBuilder(args);

toys/RESPite.Proxy/ProxyClient.cs

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ namespace RESPite.Proxy;
1212

1313
internal sealed class SocketProxyClient : ProxyClient, ICycleBufferCallback
1414
{
15-
private CycleBuffer _buffer;
15+
private CycleBuffer _receiveBuffer;
1616
private StateFlags _writeFlags;
1717
private readonly Socket _client;
1818
private readonly WorkerSocketAsyncEventArgs _readArgs, _writeArgs;
1919

2020
public SocketProxyClient(ProxyServer.InnerLeg upstream, Socket client) : base(upstream)
2121
{
22-
_buffer = CycleBuffer.Create(pool: upstream.BufferPool, callback: this);
22+
_receiveBuffer = CycleBuffer.Create(pool: upstream.BufferPool, callback: this);
2323
_readArgs = new();
2424
_readArgs.Init(this, WorkerStep.SocketProxyClientReadCallback);
2525
_writeArgs = new();
@@ -39,7 +39,8 @@ protected override void SendRawSynchronized(ReadOnlySpan<byte> frame)
3939
try
4040
{
4141
TakeWriteLock(ref lockTaken);
42-
_buffer.Write(frame);
42+
if ((_writeFlags & StateFlags.Closed) != 0) return; // torn down; nothing left to write to
43+
_receiveBuffer.Write(frame);
4344
ActivateWriterInsideLock(StateFlags.Flush);
4445
}
4546
finally
@@ -78,14 +79,17 @@ private bool CheckSend()
7879
CloseWriter();
7980
return false;
8081
}
81-
Debug.Assert(_writeArgs.BytesTransferred == _writeArgs.MemoryBuffer.Length, "incomplete buffer write");
82+
// a stream socket may legally report a partial send; we only discard what actually went out,
83+
// and the remainder stays committed for the next TryGetFirstCommittedMemory, so this is safe.
84+
Debug.Assert(_writeArgs.BytesTransferred <= _writeArgs.MemoryBuffer.Length, "over-send?!");
8285

83-
// we're done with the bytes
86+
// we're done with the bytes that made it onto the wire
8487
bool lockTaken = false;
8588
try
8689
{
8790
TakeWriteLock(ref lockTaken);
88-
_buffer.DiscardCommitted(_writeArgs.BytesTransferred);
91+
if ((_writeFlags & StateFlags.Closed) != 0) return false; // torn down; buffer already released
92+
_receiveBuffer.DiscardCommitted(_writeArgs.BytesTransferred);
8993
}
9094
finally
9195
{
@@ -106,6 +110,15 @@ private void CloseWriter()
106110
{
107111
ReleaseWriteLock(ref lockTaken);
108112
}
113+
_writeArgs.Dispose();
114+
}
115+
116+
protected override void ReleaseResources()
117+
{
118+
// dispose the socket first: this aborts any in-flight send/receive so the SAEAs are no longer
119+
// in use by the time we dispose them (a stray completion just lands as OperationAborted).
120+
try { _client.Dispose(); }
121+
catch { /* already gone */ }
109122
}
110123

111124
internal void WorkerRead()
@@ -131,23 +144,41 @@ internal void WorkerRead()
131144
}
132145
}
133146
}
147+
catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException)
148+
{
149+
// socket torn down during shutdown/cancellation: treat as a normal close, not a fault
150+
OnReceiveCleanup(SocketError.ConnectionAborted);
151+
}
134152
catch (Exception ex)
135153
{
136-
OnCleanup(SocketError.Fault, ex);
154+
OnReceiveCleanup(SocketError.Fault, ex);
137155
}
138156
}
139157

158+
private protected override void OnReceiveCleanup(SocketError error, Exception? fault = null)
159+
{
160+
base.OnReceiveCleanup(error, fault);
161+
_readArgs.Dispose();
162+
_receiveBuffer.Release();
163+
}
164+
140165
private bool CheckReceive(bool inline)
141166
{
142167
var err = _readArgs.SocketError;
143168
if (err is not SocketError.Success)
144169
{
145-
OnCleanup(err);
170+
OnReceiveCleanup(err);
146171
return false;
147172
}
148173

149-
OnAfterReceive(_readArgs.BytesTransferred, inline);
150-
return true;
174+
if (_readArgs.BytesTransferred == 0)
175+
{
176+
// EOF
177+
OnReceiveCleanup(SocketError.Success);
178+
return false;
179+
}
180+
181+
return OnAfterReceive(_readArgs.BytesTransferred, inline);
151182
}
152183

153184
public void WorkerReadCallback()
@@ -165,8 +196,13 @@ internal void WorkerWrite()
165196
while (true)
166197
{
167198
TakeWriteLock(ref lockTaken);
199+
if ((_writeFlags & StateFlags.Closed) != 0)
200+
{
201+
_writeFlags &= ~StateFlags.ActiveWriter;
202+
break; // torn down (buffer released); stop pumping
203+
}
168204
var minBytes = (_writeFlags & StateFlags.Flush) == 0 ? -1 : 1;
169-
var success = _buffer.TryGetFirstCommittedMemory(minBytes, out var memory);
205+
var success = _receiveBuffer.TryGetFirstCommittedMemory(minBytes, out var memory);
170206
if (!success)
171207
{
172208
_writeFlags &= ~StateFlags.ActiveWriter;
@@ -198,7 +234,7 @@ internal void WorkerWrite()
198234

199235
private void ActivateWriterInsideLock(StateFlags newFlags)
200236
{
201-
Debug.Assert(Monitor.IsEntered(this), $"{nameof(ActivateWriterInsideLock)} must be called while holding the writer lock.");
237+
Debug.Assert(Monitor.IsEntered(_writeLock), $"{nameof(ActivateWriterInsideLock)} must be called while holding the writer lock.");
202238

203239
var state = _writeFlags;
204240
if ((state & StateFlags.Closed) != 0) return;
@@ -215,11 +251,16 @@ private void ActivateWriterInsideLock(StateFlags newFlags)
215251
}
216252
}
217253

254+
// guards _buffer/_writeFlags on the write side; a dedicated object (rather than 'this') so no
255+
// external code can contend the lock, and reentrancy via ICycleBufferCallback.PageComplete is
256+
// still safe (Monitor is reentrant on the same thread).
257+
private readonly object _writeLock = new();
258+
218259
private void TakeWriteLock(ref bool lockTaken)
219260
{
220261
if (!lockTaken)
221262
{
222-
Monitor.TryEnter(this, 10_000, ref lockTaken);
263+
Monitor.TryEnter(_writeLock, 10_000, ref lockTaken);
223264
if (!lockTaken) Throw();
224265
}
225266
static void Throw() => throw new TimeoutException("Unable to acquire writer lock");
@@ -229,7 +270,7 @@ private void ReleaseWriteLock(ref bool lockTaken)
229270
{
230271
if (lockTaken)
231272
{
232-
Monitor.Exit(this);
273+
Monitor.Exit(_writeLock);
233274
lockTaken = false;
234275
}
235276
}
@@ -259,9 +300,12 @@ internal abstract class ProxyClient(ProxyServer.InnerLeg upstream) : RespStream
259300
protected CancellationToken Lifetime => upstream.Lifetime;
260301
public int Id { get; set; }
261302
public int Database => _db;
303+
public ulong OpCount => Interlocked.Read(ref _opCount);
304+
262305
private int _db;
263306
private TaskCompletionSource _completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
264307

308+
private ulong _opCount;
265309
private bool OnSelect(int db)
266310
{
267311
if (db < 0 | db > 999_999_999) return false;
@@ -275,8 +319,14 @@ public Task ExecuteAsync(PipeReader source)
275319
return _completionSource.Task;
276320
}
277321

322+
private int _closed; // 0 = open, 1 = closed; guards teardown so it runs exactly once
323+
278324
private protected override void RecordConnectionFailed(StreamFailureKind kind, Exception? fault = null)
279325
{
326+
// teardown can be reached from the read pump *and*, once the worker is multi-threaded, from a
327+
// write-side fault on another thread; make it idempotent so we don't double-remove/double-dispose
328+
if (Interlocked.Exchange(ref _closed, 1) != 0) return;
329+
280330
if (fault is null)
281331
{
282332
_completionSource.TrySetResult();
@@ -287,13 +337,22 @@ private protected override void RecordConnectionFailed(StreamFailureKind kind, E
287337
}
288338

289339
upstream.Remove(this);
340+
ReleaseResources();
290341
}
291342

343+
/// <summary>
344+
/// Releases connection-scoped resources (sockets, buffers, ...). Invoked exactly once, after the
345+
/// connection has been removed from the pool.
346+
/// </summary>
347+
protected virtual void ReleaseResources() { }
348+
292349
protected override unsafe void OnReadFrame(
293350
RespPrefix prefix,
294351
ReadOnlySpan<byte> frame,
295352
ref IMemoryOwner<byte>? memoryOwner)
296353
{
354+
_opCount++;
355+
297356
ReadOnlyMemory<byte> localResponse = default;
298357
IDisposable? lease = null;
299358
KnownCommands command = KnownCommands.Unknown;

toys/RESPite.Proxy/ProxyServer.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,29 @@ internal void RegisterClient(ProxyClient client)
7171

7272
internal bool TryGetClient(int id, out ProxyClient client) => _clients.TryGetValue(id, out client!);
7373

74-
internal void RemoveClient(ProxyClient client) => _clients.TryRemove(client.Id, out _);
74+
internal void RemoveClient(ProxyClient client)
75+
{
76+
var opCount = client.OpCount;
77+
if (_clients.TryRemove(client.Id, out _))
78+
{
79+
Interlocked.Add(ref _opCountFromDeadConnections, opCount);
80+
}
81+
}
82+
83+
public ulong GetOpCount(out int activeClients)
84+
{
85+
ulong tally = 0;
86+
activeClients = 0;
87+
foreach (var client in _clients)
88+
{
89+
tally += client.Value.OpCount;
90+
activeClients++;
91+
}
92+
93+
// intentionally delay this read
94+
return tally + Interlocked.Read(ref _opCountFromDeadConnections);
95+
}
96+
private ulong _opCountFromDeadConnections;
7597

7698
internal sealed class InnerLeg(ProxyServer server, Stream tail) : RespStream
7799
{

0 commit comments

Comments
 (0)