@@ -12,14 +12,14 @@ namespace RESPite.Proxy;
1212
1313internal 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 ;
0 commit comments