perf(sessions): bounded-parallel teardown in lease sweep and shutdown
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Security.Cryptography;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -20,6 +21,12 @@ public sealed class SessionManager : ISessionManager
|
||||
public const string DetachGraceExpiredReason = "detach-grace-expired";
|
||||
public const string FaultedReason = "faulted-reaped";
|
||||
|
||||
// Bounded so a mass expiry (or a host stop with a full registry) cannot stampede
|
||||
// worker-process teardown: every concurrent close is one x86 worker being shut down or
|
||||
// killed, and the point of the fan-out is to hide a few hung workers, not to tear the whole
|
||||
// registry down at once.
|
||||
private const int MaxParallelSessionCloses = 4;
|
||||
|
||||
private readonly ISessionRegistry _registry;
|
||||
private readonly ISessionWorkerClientFactory _workerClientFactory;
|
||||
private readonly GatewayMetrics _metrics;
|
||||
@@ -252,7 +259,10 @@ public sealed class SessionManager : ISessionManager
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int closedCount = 0;
|
||||
// Selection phase — deliberately sequential. Only the close calls below run in parallel:
|
||||
// deciding WHICH sessions to close must stay a single ordered pass so the sweep-precedence
|
||||
// rule and the TOCTOU re-check keep their meaning.
|
||||
List<(GatewaySession Session, string Reason)> selected = [];
|
||||
foreach (GatewaySession session in _registry.Snapshot())
|
||||
{
|
||||
// A session is swept when its normal lease has expired, it has FAULTED (a faulted
|
||||
@@ -288,45 +298,100 @@ public sealed class SessionManager : ISessionManager
|
||||
continue;
|
||||
}
|
||||
|
||||
await CloseSessionCoreAsync(session, reason, cancellationToken).ConfigureAwait(false);
|
||||
closedCount++;
|
||||
selected.Add((session, reason));
|
||||
}
|
||||
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int closedCount = 0;
|
||||
object failureSyncRoot = new();
|
||||
ExceptionDispatchInfo? firstFailure = null;
|
||||
|
||||
// Close phase. Each close is bounded by the worker shutdown timeout (default 10 s), so a
|
||||
// mass expiry with a few hung workers would serialize reaping and starve session slots.
|
||||
// Parallel close is safe because TryBeginCloseIfExpired above already flipped every
|
||||
// selected session to Closing under its own lock — that idempotent begin-close is the
|
||||
// per-session exclusivity invariant, so no two teardowns can run against one session and
|
||||
// a session selected here cannot be re-selected by a concurrent sweep.
|
||||
await Parallel.ForEachAsync(
|
||||
selected,
|
||||
new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = MaxParallelSessionCloses,
|
||||
CancellationToken = cancellationToken,
|
||||
},
|
||||
async (candidate, closeToken) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await CloseSessionCoreAsync(candidate.Session, candidate.Reason, closeToken).ConfigureAwait(false);
|
||||
Interlocked.Increment(ref closedCount);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// The sequential sweep let a close failure propagate to the lease monitor,
|
||||
// which logs it; that signal is preserved by rethrowing the first failure
|
||||
// below. It is captured rather than thrown here so one failed (or hung)
|
||||
// teardown does not abandon the rest of the already-selected set.
|
||||
lock (failureSyncRoot)
|
||||
{
|
||||
firstFailure ??= ExceptionDispatchInfo.Capture(exception);
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
firstFailure?.Throw();
|
||||
|
||||
return closedCount;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (GatewaySession session in _registry.Snapshot())
|
||||
{
|
||||
try
|
||||
// Sessions are drained in parallel: at a worst-case worker shutdown timeout each, a
|
||||
// one-at-a-time drain of a full registry outruns any host stop-timeout and leaves the
|
||||
// tail to the orphan killer. Per-session exclusivity comes from GatewaySession.CloseAsync's
|
||||
// own close gate, and each iteration touches only its own session plus thread-safe
|
||||
// registry/metrics state.
|
||||
await Parallel.ForEachAsync(
|
||||
_registry.Snapshot(),
|
||||
new ParallelOptions
|
||||
{
|
||||
await CloseSessionCoreAsync(session, GatewayShutdownReason, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
MaxDegreeOfParallelism = MaxParallelSessionCloses,
|
||||
CancellationToken = cancellationToken,
|
||||
},
|
||||
async (session, closeToken) =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
exception,
|
||||
"Graceful shutdown failed for session {SessionId}; killing worker.",
|
||||
session.SessionId);
|
||||
|
||||
if (_registry.TryGet(session.SessionId, out _))
|
||||
try
|
||||
{
|
||||
try
|
||||
await CloseSessionCoreAsync(session, GatewayShutdownReason, closeToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
exception,
|
||||
"Graceful shutdown failed for session {SessionId}; killing worker.",
|
||||
session.SessionId);
|
||||
|
||||
if (_registry.TryGet(session.SessionId, out _))
|
||||
{
|
||||
await KillWorkerAsync(session.SessionId, GatewayShutdownReason, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SessionManagerException killException)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
killException,
|
||||
"Worker kill fallback failed for session {SessionId}.",
|
||||
session.SessionId);
|
||||
try
|
||||
{
|
||||
await KillWorkerAsync(session.SessionId, GatewayShutdownReason, closeToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SessionManagerException killException)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
killException,
|
||||
"Worker kill fallback failed for session {SessionId}.",
|
||||
session.SessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<SessionCloseResult> CloseSessionCoreAsync(
|
||||
|
||||
Reference in New Issue
Block a user