Repository navigation
Commit 2be2670
authored
fix(redis): size cold-connection waits to survive a dead handshake (#7764)
* fix(redis): size cold-connection waits to survive a dead handshake
A dead Redis handshake costs two command deadlines, not one. ioredis sends
CLIENT SETNAME/SETINFO on connect and dispatches the INFO ready check only
once those settle — on a socket that never answers, they settle by timing
out. Only then does the ready check start its own deadline, fail, and tear the
socket down for retryStrategy to reconnect. Every readiness wait in the
codebase was sized to a single deadline, so it expired while the first attempt
was still being diagnosed and a configured retry could never be the thing that
rescued it.
Introduces `coldConnectionBudgetMs`, which derives a wait from the command
deadline and the reconnect delays a caller's own retryStrategy would return,
and states the 2x in one place. Both waits now derive from it:
- The execution-signal subscriber's readiness budget was the same constant as
its commandTimeout (5s and 5s), so ioredis's reconnect was decorative on the
cold path that failed in production. It now has a 2s command deadline — this
client only issues SUBSCRIBE/UNSUBSCRIBE, always after ready — and a budget
with room for two dead attempts and a healthy one.
- The shared client's warm-up budget was 10s against a 5s deadline, exactly
the moment ioredis would first tear a stalled socket down, so on the case
warming exists for it gave up just before recovery could land.
Warms the execution-signal subscriber alongside the shared client at process
start, in parallel, never throwing: it is the second connection a run opens
and was paying its handshake inside its own readiness budget.
The shared redis-config mock mirrors the pure budget helper, since consumers
now evaluate it at module load and would otherwise fail to import under the
global mock.
* fix(redis): keep the subscriber's live command deadline at 5s
ioredis has one `commandTimeout` for the handshake commands and every live
command on the socket, so tightening it to diagnose dead handshakes faster also
cut the initial SUBSCRIBE from 5s to 2s — on the one path where a rejection
fails the whole run. A ready-but-slow server delays SUBSCRIBE too.
Restore the 5s tolerance that subscribe has always had and derive the
readiness budget from it: one dead handshake recovers in ~10.5s, so an 11.5s
budget still lets ioredis's reconnect rescue the case that failed. The local
test mock now delegates to the shared, drift-guarded mirror of the budget
arithmetic so the derived number under test is the one production derives.
* improvement(redis): warm the signal subscriber on intent, not at worker start
Only the tasks that execute a workflow ever subscribe to cancellation signals,
and they are a minority of runs: over a week, 71% of Trigger.dev runs never
subscribe, and a single document-processing task is 61% on its own. Warming
the subscriber in the global init hook opened a TLS connection on every one
of those runs that nothing would use.
Warm it instead at `executeWorkflowCore` — the one path every execution
shares — fire-and-forget as the first statement, so the handshake overlaps the
custom-block read and preprocessing ahead of the cancellation subscribe rather
than being paid inside that subscribe's readiness budget. No task-id list to
keep in sync: coverage follows from the funnel. The subscribe and the warm-up
share the hub's memoized readiness wait, so their budgets never compound
within a run.
The global init keeps warming only the shared client, which nearly every task
uses; the Next server keeps warming both at boot, one connection per
long-lived process.
* chore(instrumentation): import the warm-ups through the path alias
The dynamic imports in the Redis warm-up block used relative paths; the file
already resolves `@/` and the repository rule is absolute imports.
* fix(redis): give every readiness waiter its own deadline and correct the budget model
Three corrections from an independent review of the branch.
The shared readiness wait carried one timer, so a subscribe that joined an
in-flight warm-up inherited only the remainder of that warm-up's budget — down
to nothing — where before this branch it was guaranteed a full wait of its own.
Waiters now share one ready/end listener pair but each runs its own deadline,
and the signal is torn down when its last waiter gives up.
The budget model claimed a dead handshake always costs two command deadlines.
That is true only without a password: with one, ioredis treats a timed-out
AUTH as fatal and tears the socket down after a single deadline, and a connect
that never completes is bounded by connectTimeout, which the formula had no
term for. `coldConnectionBudgetMs` now charges the largest of those so it holds
for either URL shape, and moves to @sim/utils/retry: a pure helper evaluated at
module load must not live in a module the test setup replaces wholesale, which
had forced the shared mock to carry a verbatim mirror of the arithmetic. The
subscriber derives its budget in its constructor from the exact options it is
built with; the shared client's reconnect delay is a pure function used by
both retryStrategy and the warm-up budget.
The Next server no longer opens the signal subscriber at boot: the execution
entry point already warms it on intent, and an eagerly opened subscriber that
cannot reach Redis would reconnect on a five-second cadence in every idle
replica for the life of the process.
* fix(redis): leave the readiness wait exactly once per waiter
A waiter that timed out was still subscribed to the shared readiness signal,
so when that signal later settled for another waiter its cleanup ran a second
time. The waiter count drifted negative, the last-waiter teardown could never
match again, and the settled signal stayed memoized — a later subscribe during
a reconnect would have observed a readiness that had already passed.
Each waiter now leaves exactly once, and a settling signal clears itself from
the memo so the next waiter observes the connection afresh regardless of the
count.
* test(redis): assert the fresh subscribe by channel, not by call count
A reconnect also re-subscribes surviving channels, so the new subscription is
identified by its channel rather than by SUBSCRIBE having been issued once.
* test(redis): prove waiter accounting by the teardown a timed-out waiter must perform
The settle path clears the memo on its own, so a drifted waiter count only
shows when a later lone waiter times out and fails to tear the signal down.
Exercise that path directly; the test now fails without the per-waiter guard.
* test(execution-core): type the warm-up test's fixtures instead of casting to any
The partial fixtures are widened through unknown to the real ExecutionSnapshot
and LoggingSession types, as the repository's TypeScript rule prescribes.
* fix(redis): connect the signal subscriber on intent and complete the budget model
Findings from an independent review of the finished branch.
Constructing the execution-signal hub is what connects — ioredis dials in its
constructor — so the warm-up that observed that handshake and returned a
boolean its only caller discarded is gone. The execution entry point now
constructs the hub, synchronously and never throwing, and the subscribe that
follows waits on its own budget. With no observer to accommodate, the waiter
machinery collapses to a race between the shared readiness signal and a
per-waiter deadline, with a per-signal count so the last waiter detaches.
The budget's dead-attempt term was still short for an unanswered handshake:
after the ready check times out, ioredis half-closes the socket and destroys
it only after `disconnectTimeout` when a wedged peer never answers with a FIN.
Measured against such a peer: an unauthenticated connection reconnects at
12.5s, past the 11.5s the budget allowed. The term is now
`max(connectTimeout, 2 * commandTimeout + disconnectTimeout)`, with the
disconnect deadline stated in the shared connection defaults so the budget
derives from it. The model is specific to ioredis 5's handshake sequence, so it
lives beside the pinned client in `lib/core/config/redis-budget.ts` — a module
the global test mock does not replace — rather than in a generic package.
Corrects two doc claims: no caller warms the subscriber at server boot, and a
late joiner never had a full wait of its own before this branch either — the
memoized promise shared its single timer.1 parent c4f0815 commit 2be2670
12 files changed
Lines changed: 488 additions & 46 deletions
File tree
- apps/sim
- lib
- core/config
- execution
- workflows/executor
- packages/testing/src/mocks
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
407 | 407 | | |
408 | 408 | | |
409 | 409 | | |
410 | | - | |
| 410 | + | |
411 | 411 | | |
412 | 412 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
50 | 51 | | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
55 | 57 | | |
| 58 | + | |
| 59 | + | |
56 | 60 | | |
57 | 61 | | |
| 62 | + | |
58 | 63 | | |
59 | 64 | | |
60 | 65 | | |
| |||
471 | 476 | | |
472 | 477 | | |
473 | 478 | | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
474 | 489 | | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
475 | 523 | | |
476 | 524 | | |
477 | 525 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | | - | |
46 | | - | |
47 | | - | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
48 | 59 | | |
49 | 60 | | |
50 | 61 | | |
51 | | - | |
| 62 | + | |
| 63 | + | |
52 | 64 | | |
53 | 65 | | |
54 | 66 | | |
| |||
203 | 215 | | |
204 | 216 | | |
205 | 217 | | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
206 | 235 | | |
207 | | - | |
208 | | - | |
209 | | - | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
210 | 239 | | |
211 | | - | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
212 | 246 | | |
213 | 247 | | |
214 | 248 | | |
| |||
296 | 330 | | |
297 | 331 | | |
298 | 332 | | |
299 | | - | |
| 333 | + | |
300 | 334 | | |
301 | 335 | | |
302 | 336 | | |
303 | 337 | | |
304 | 338 | | |
305 | 339 | | |
306 | 340 | | |
307 | | - | |
308 | | - | |
309 | | - | |
| 341 | + | |
310 | 342 | | |
311 | 343 | | |
312 | 344 | | |
| |||
0 commit comments