Commit 132d57f
A write that lands is not a write that failed, and a silent consumer stops everything (#152)
* A write that lands is not a write that failed, and a silent consumer stops everything
The crawler stopped at 20:48 on 2026-08-27 and did not start again for
seventeen hours. Nothing was broken in the places worth suspecting: the
worker was healthy and picked jobs up 66ms after they were added, an empty
write transaction against Turso answered in 0.34s, Redis was 1MB with no
restarts and no evictions, and the jobs themselves ran and committed with
their full return values written to the events stream.
Nobody was reading that stream. `CLIENT LIST` on the production Redis showed
fourteen clients and not one of them running `xread` — the poller's
`QueueEvents` connection had gone away without the socket ever erroring, so
the server had no such client left while the process sat in a read that would
never return. `checkConnectionError` only retries a read that *errors*, and
there was no error, so the loop never came round again and never said so.
That is a hard stall rather than a slowdown, because `createWriteFolder`
keeps one job in flight per process: every caller waited out the full
`WAIT_MS` and the whole cluster fell to one write attempt per two minutes.
The signature is in the log, where every failure is exactly 120000ms or
240000ms and every success is under a second, with nothing in between — a
slow database gives a continuum, a lost notification gives two spikes.
Three changes, in the order they matter:
- `recoverFinished` asks the job what became of it when the wait expires,
using `isFinished` — the same primitive `waitUntilFinished` polls with
before it starts listening, and the one answer that does not depend on the
stream that just failed. A committed write is handed back to its caller
instead of being reported as a failure, which is the half of this that was
quietly corrupting state: the crawler recorded an error against healthy
feeds and re-crawled rows it had already stored.
- `removeOnComplete` keeps the last 200 jobs rather than deleting each one
the instant it succeeds. Tidy, and it was what made a lost notification
unrecoverable — the waiter went looking for the job and found nothing.
- `reviveEvents` replaces a consumer that has stopped consuming, guarded on a
pulse so a genuinely slow job does not cost a reconnect. `entry.events` is
read through the entry at every wait, because a destructured copy would pin
every future caller to the corpse. An `error` listener goes on both objects
so the next connection failure is a log line rather than nothing at all.
Production was recovered by restarting the poller before this landed; write
throughput went from ~30 statements per ten minutes to 943, and the OPML
import that had been frozen at 30,000 rows since 20:45 resumed.
Tested against stubs rather than a broker, for the reason `runWriteJob` is:
the judgement is in what to believe when the notification never came, and the
BullMQ plumbing is not the part that was wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLtMz1jBZVziV2xn8iTh2j
* Say only what was measured about why the consumer went quiet
The first version of these comments asserted a mechanism — the consumer hung
forever in a blocking read that never errored — on the strength of there being
no `xread` client in Redis during the outage.
That inference does not hold. Sampled thirty times over thirty seconds against
a fully healthy crawler, none of the samples showed an `xread` client either,
so its absence says nothing about whether the consumer is alive. BullMQ 6.1.2
also already carries a watchdog for precisely the hang that was being claimed,
which the story did not account for.
What was actually measured is unchanged and is enough: for seventeen hours
every wait expired while the jobs ran and committed, their completion events
and return values were written to the stream, nothing acted on them, and
restarting the process fixed it. The fix does not depend on knowing why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLtMz1jBZVziV2xn8iTh2j
* Poll the job beside the wait, because the stall outlived the restart
Restarting the poller at 14:11 restored the crawler — 943 statements in a
ten-minute tally against the ~30 it had been managing — and by 14:41 it had
stalled again, with the same 120000ms waits on feeds that were being written
successfully. So the condition recurs on its own, and a fix that only takes
effect after the deadline is not a fix: recovering the result once the wait
expires tells the caller the truth and still leaves `createWriteFolder` with
one job in flight per two minutes, which is a stopped crawler with accurate
error messages.
`settleJob` runs the poll *beside* the notification instead of after it.
`waitUntilFinished` stays the fast path, so a working stream still settles a
write on one Redis round trip. Alongside it, `isFinished` — the job's own
state, which owes nothing to any consumer — is asked every 500ms, and
whichever answer comes first wins. There is only ever one job in flight per
process, so the cost is two small reads a second against a failure mode that
costs everything.
The deadline still exists and still means what it meant: if neither the
stream nor two minutes of polling can find a finished job, the job really is
unfinished and the caller fails.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLtMz1jBZVziV2xn8iTh2j
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 30f3766 commit 132d57f
2 files changed
Lines changed: 442 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
58 | 76 | | |
59 | 77 | | |
60 | 78 | | |
| |||
209 | 227 | | |
210 | 228 | | |
211 | 229 | | |
212 | | - | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
213 | 234 | | |
214 | 235 | | |
215 | 236 | | |
216 | 237 | | |
217 | 238 | | |
218 | 239 | | |
219 | 240 | | |
220 | | - | |
| 241 | + | |
221 | 242 | | |
222 | 243 | | |
223 | 244 | | |
| |||
241 | 262 | | |
242 | 263 | | |
243 | 264 | | |
244 | | - | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
245 | 277 | | |
246 | 278 | | |
247 | 279 | | |
248 | 280 | | |
249 | 281 | | |
250 | 282 | | |
251 | | - | |
252 | | - | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
253 | 296 | | |
254 | 297 | | |
255 | 298 | | |
| |||
305 | 348 | | |
306 | 349 | | |
307 | 350 | | |
308 | | - | |
| 351 | + | |
309 | 352 | | |
310 | 353 | | |
311 | 354 | | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
312 | 366 | | |
313 | 367 | | |
314 | 368 | | |
| |||
325 | 379 | | |
326 | 380 | | |
327 | 381 | | |
328 | | - | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
329 | 385 | | |
| 386 | + | |
330 | 387 | | |
331 | 388 | | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
332 | 395 | | |
333 | 396 | | |
334 | 397 | | |
335 | 398 | | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 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 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
336 | 590 | | |
337 | 591 | | |
338 | 592 | | |
| |||
0 commit comments