fix(runtime): explicit 30s command timeout on the Direct-apply artifact read

This commit is contained in:
Joseph Doherty
2026-08-17 03:49:27 -04:00
parent 6ee553cf8f
commit dba5f806d3
2 changed files with 162 additions and 0 deletions
@@ -2093,6 +2093,35 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
try
{
using var db = _dbFactory.CreateDbContext();
// Bound this read EXPLICITLY. It runs off the mailbox on a pool thread with no
// CancellationToken (BeginDirectApply's Task.Run), so the actor itself cannot be wedged by a
// hung central-SQL read — but the read would pin a pool thread for as long as SQL takes to
// answer. Until now the only bound was SqlClient's IMPLICIT 30 s command-timeout default:
// nothing on the ConfigDb path sets one (AddPooledDbContextFactory uses a plain
// UseSqlServer(connectionString), and no connection string carries "Command Timeout="), so a
// future connection-string edit or provider-default change could silently unbound it. Stating
// it here pins the bound to this call site.
//
// Deliberately LOCAL to this lease, not on the pooled factory's options: re-timing the global
// factory would re-time every ConfigDb consumer — migrations and long admin queries included.
// Verified against EF Core 10.0.7 (pool size 1, same context instance re-leased):
// Database.SetCommandTimeout does NOT survive the return to the pool — RelationalConnection
// resets it as an IResettableService — so the next lease sees the default again and there is
// no pool pollution to undo.
//
// Deliberately NOT a CancellationToken: threading one would mean making the FirstOrDefault
// below async, and the only caller-side token is actor lifetime — the timeout gives the same
// bound without touching the actor seam. A timeout surfaces as an exception through this
// method's existing catch-to-null, so the apply FAILS and the revision does not advance,
// keeping last-known-good served (the #485-family "no answer, never tear down" semantics —
// already correct, unchanged here).
//
// Guarded on IsRelational: the timeout is a relational-only concept and the extension throws
// on a non-relational provider (the in-memory provider the actor tests run against).
if (db.Database.IsRelational())
db.Database.SetCommandTimeout(TimeSpan.FromSeconds(30));
return db.Deployments.AsNoTracking()
.Where(d => d.DeploymentId == deploymentId.Value)
.Select(d => d.ArtifactBlob)