77using System . Net . Sockets ;
88using System . Runtime . CompilerServices ;
99using System . Threading ;
10+ using System . Threading . Tasks ;
1011using Garnet . networking ;
12+ using Microsoft . Extensions . Logging ;
1113
1214namespace Garnet . common
1315{
@@ -46,6 +48,23 @@ public class GarnetTcpNetworkSender : NetworkSenderBase
4648 /// </summary>
4749 protected readonly int ThrottleMax = 8 ;
4850
51+ /// <summary>
52+ /// Longest a send waits for an earlier send to complete, in milliseconds, before the connection is
53+ /// closed as stalled; <see cref="Timeout.Infinite"/> waits indefinitely.
54+ /// </summary>
55+ readonly int sendTimeoutMs ;
56+
57+ /// <summary>
58+ /// Progressive spins a draining close performs before it blocks.
59+ /// </summary>
60+ const int DrainSpinIterations = 32 ;
61+
62+ /// <summary>
63+ /// Progressive spins a send performs while waiting for a send slot before it blocks; this matches the spin
64+ /// <see cref="SemaphoreSlim.Wait()"/> performs before it blocks.
65+ /// </summary>
66+ const int CapacitySpinIterations = 140 ;
67+
4968 readonly string remoteEndpoint ;
5069 readonly string localEndpoint ;
5170
@@ -64,12 +83,18 @@ public class GarnetTcpNetworkSender : NetworkSenderBase
6483 /// </summary>
6584 /// <param name="socket"></param>
6685 /// <param name="networkBufferSettings"></param>
86+ /// <param name="networkPool"></param>
6787 /// <param name="throttleMax"></param>
88+ /// <param name="sendTimeout">
89+ /// Longest a send waits for an earlier send to complete before the connection is closed as stalled.
90+ /// Zero or negative waits indefinitely.
91+ /// </param>
6892 public GarnetTcpNetworkSender (
6993 Socket socket ,
7094 NetworkBufferSettings networkBufferSettings ,
7195 LimitedFixedBufferPool networkPool ,
72- int throttleMax = 8 )
96+ int throttleMax = 8 ,
97+ TimeSpan sendTimeout = default )
7398 : base ( networkBufferSettings . sendBufferSize )
7499 {
75100 this . networkBufferSettings = networkBufferSettings ;
@@ -78,6 +103,7 @@ public GarnetTcpNetworkSender(
78103 this . saeaStack = new ( 2 * ThrottleMax ) ;
79104 this . responseObject = null ;
80105 this . ThrottleMax = throttleMax ;
106+ this . sendTimeoutMs = sendTimeout > TimeSpan . Zero ? ( int ) Math . Min ( sendTimeout . TotalMilliseconds , int . MaxValue ) : Timeout . Infinite ;
81107 this . spinLock = new ( ) ;
82108 this . closeRequested = 0 ;
83109
@@ -258,11 +284,19 @@ public override void SendCallback(object context)
258284 /// <inheritdoc />
259285 public override void DisposeNetworkSender ( bool waitForSendCompletion )
260286 {
261- if ( ! waitForSendCompletion )
287+ // Invariant: every in-flight send completes before the buffer stack and throttle are disposed, since a
288+ // send's completion returns its buffer to the stack and may release the throttle. Draining also closes
289+ // the sender, so any later send completes immediately without reaching the socket.
290+ //
291+ // A graceful close first lets in-flight sends reach the peer, giving up once no send has completed for the
292+ // send timeout (never, when the timeout is infinite). An abortive close, or a graceful one that gives up,
293+ // disposes the socket instead. That aborts the in-flight sends, so the unbounded drain after it is prompt
294+ // and still upholds the invariant.
295+ if ( ! waitForSendCompletion || ! TryDrainSends ( sendTimeoutMs ) )
296+ {
262297 socket . Dispose ( ) ;
263-
264- // Wait for ongoing sends to complete
265- while ( throttleCount >= 0 && Interlocked . CompareExchange ( ref throttleCount , int . MinValue , 0 ) != 0 ) Thread . Yield ( ) ;
298+ _ = TryDrainSends ( Timeout . Infinite ) ;
299+ }
266300
267301 // Empty and dispose the stack
268302 saeaStack . Dispose ( ) ;
@@ -272,6 +306,56 @@ public override void DisposeNetworkSender(bool waitForSendCompletion)
272306 socket . Dispose ( ) ;
273307 }
274308
309+ /// <summary>
310+ /// Waits for in-flight sends to complete, then closes the sender to further sends.
311+ /// </summary>
312+ /// <param name="timeoutMs">Longest to wait without any send completing, or <see cref="Timeout.Infinite"/>.</param>
313+ /// <returns>False if no send completed within <paramref name="timeoutMs"/>; the sender is then still open.</returns>
314+ bool TryDrainSends ( int timeoutMs )
315+ {
316+ // Sends complete within microseconds while the peer reads, so spin progressively first
317+ var spinner = ConfiguredSpinWait . BoundedProgressive ( DrainSpinIterations ) ;
318+ do
319+ {
320+ if ( TryCloseIfDrained ( ) )
321+ return true ;
322+ }
323+ while ( spinner . TryWait ( ) ) ;
324+
325+ // A peer that reads slowly, or not at all, can take far longer. Blocking on a task, rather than sleeping,
326+ // lets the thread pool inject a replacement for this thread while it waits.
327+ return AsyncUtils . BlockingWait ( DrainSendsAsync ( timeoutMs ) ) ;
328+ }
329+
330+ async Task < bool > DrainSendsAsync ( int timeoutMs )
331+ {
332+ using var poll = new PeriodicTimer ( TimeSpan . FromMilliseconds ( 1 ) ) ;
333+ var pending = Volatile . Read ( ref throttleCount ) ;
334+ var progressAt = Environment . TickCount64 ;
335+ while ( ! TryCloseIfDrained ( ) )
336+ {
337+ var count = Volatile . Read ( ref throttleCount ) ;
338+ if ( count != pending )
339+ {
340+ pending = count ;
341+ progressAt = Environment . TickCount64 ;
342+ }
343+ else if ( timeoutMs != Timeout . Infinite && Environment . TickCount64 - progressAt >= timeoutMs )
344+ {
345+ return false ;
346+ }
347+
348+ _ = await poll . WaitForNextTickAsync ( ) . ConfigureAwait ( false ) ;
349+ }
350+ return true ;
351+ }
352+
353+ /// <summary>
354+ /// Closes the sender to further sends if no send is in flight, or reports that it is already closed.
355+ /// </summary>
356+ bool TryCloseIfDrained ( )
357+ => Volatile . Read ( ref throttleCount ) < 0 || Interlocked . CompareExchange ( ref throttleCount , int . MinValue , 0 ) == 0 ;
358+
275359 /// <inheritdoc />
276360 public override void Throttle ( )
277361 {
@@ -329,7 +413,7 @@ private unsafe void Send(Socket socket, GarnetSaeaBuffer sendObject, int offset,
329413 return ;
330414 }
331415 if ( cnt > ThrottleMax )
332- throttle . Wait ( ) ;
416+ WaitForSendCapacity ( ) ;
333417
334418 // Debug.WriteLine($"SEND: [{size}][{System.Text.Encoding.UTF8.GetString(new Span<byte>(sendObject.socketEventAsyncArgs.Buffer, offset, size)).Replace("\n", "|").Replace("\r", "")}]");
335419
@@ -341,12 +425,62 @@ private unsafe void Send(Socket socket, GarnetSaeaBuffer sendObject, int offset,
341425 SeaaBuffer_Completed ( null , sendObject . socketEventAsyncArgs ) ;
342426 }
343427
428+ /// <summary>
429+ /// Waits for an in-flight send to complete, which frees a send slot for the caller.
430+ /// </summary>
431+ /// <remarks>
432+ /// The caller is usually a thread-pool thread that also serves other connections. Blocking on a task lets
433+ /// the thread pool inject a replacement thread, whereas blocking on the semaphore does not, so other
434+ /// connections continue to be served while this one waits. A peer that accepts no data for
435+ /// the send timeout has its connection closed, which also completes its in-flight sends.
436+ /// <para>
437+ /// A completion that frees a slot for a waiter decrements the send count before it releases the semaphore,
438+ /// so the count can reach zero, letting the sender be disposed, while that release is still to come. A waiter
439+ /// therefore leaves only once it has taken its release, even after a timeout; otherwise the release could
440+ /// land on a disposed semaphore.
441+ /// </para>
442+ /// <para>
443+ /// The timeout is applied to the wait on the untimed semaphore task rather than passed to the semaphore,
444+ /// because a timed semaphore wait allocates a timer and async state on every call. The untimed task also
445+ /// stays queued after a timeout, so it is the one that takes the owed release.
446+ /// </para>
447+ /// </remarks>
448+ /// <exception cref="GarnetException">No send completed within the send timeout.</exception>
449+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
450+ private void WaitForSendCapacity ( )
451+ {
452+ // A send usually completes shortly while the peer reads, so spin progressively, as the semaphore's own
453+ // blocking wait does, before paying for a task and a blocking wait.
454+ var spinner = ConfiguredSpinWait . BoundedProgressive ( CapacitySpinIterations , sleep1Threshold : - 1 ) ;
455+ do
456+ {
457+ if ( throttle . Wait ( 0 ) )
458+ return ;
459+ }
460+ while ( spinner . TryWait ( ) ) ;
461+
462+ var slot = throttle . WaitAsync ( ) ;
463+ if ( AsyncUtils . BlockingWait ( slot , sendTimeoutMs ) )
464+ return ;
465+
466+ // Closing the socket completes every in-flight send, so the release this waiter is owed arrives promptly
467+ _ = TryClose ( ) ;
468+ AsyncUtils . BlockingWait ( slot ) ;
469+ throw new GarnetException ( $ "Closed connection to { remoteEndpoint } : no send completed within { sendTimeoutMs } ms",
470+ LogLevel . Warning , clientResponse : false , disposeSession : true ) ;
471+ }
472+
344473 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
345474 private void SeaaBuffer_Completed ( object sender , SocketAsyncEventArgs e )
346475 {
347476 ReturnBuffer ( ( GarnetSaeaBuffer ) e . UserToken ) ;
348477 if ( Interlocked . Decrement ( ref throttleCount ) >= ThrottleMax )
478+ {
479+ #if DEBUG
480+ ExceptionInjectionHelper . ResetAndWait ( ExceptionInjectionType . Sender_Pause_Before_Slot_Release ) ;
481+ #endif
349482 throttle . Release ( ) ;
483+ }
350484 }
351485 }
352486}
0 commit comments