fix(server): prevent crashed platform teardown from destroying its replacement#1167
fix(server): prevent crashed platform teardown from destroying its replacement#1167silverbucket merged 2 commits intomastersockethub/sockethub:masterfrom fix/platform-replacement-shutdown-racesockethub/sockethub:fix/platform-replacement-shutdown-raceCopy head branch name to clipboard
Conversation
…placement When a platform child crashes, its close event fires tens to hundreds of ms after the process dies. A client message arriving in that window makes ProcessManager create a replacement instance under the same identifier and Redis queue name. The stale instance's shutdown() then destroyed the replacement's shared resources: queue.shutdown() paused and obliterated the shared-name queue (deleting the replacement's pending jobs), and platformInstances.delete(this.id) evicted the replacement from the map (causing duplicate child processes on the next message). - PlatformInstance.markReplaced(): flags an instance superseded by a replacement so its teardown leaves shared resources alone - shutdown() closes queue connections without pause/obliterate when the instance was replaced or no longer owns its map slot, and only deletes the map entry it still owns - ProcessManager.ensureProcess() marks a dead instance replaced before firing its async shutdown, and does so before constructing the replacement - JobQueue.disconnect(): closes connections without pausing or obliterating the underlying Redis queue Fixes the 'Parent Process Sudden Termination' CI flake: the SIGTERM test's job was enqueued right as the previous crash's close-event teardown ran, got obliterated, and no platform ever received it. Fixes #1166
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds ChangesQueue disconnect and instance replacement lifecycle
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant ProcessManager
participant OldInstance as PlatformInstance
participant NewInstance as PlatformInstance
participant JobQueue
participant platformInstances
ProcessManager->>OldInstance: markReplaced()
ProcessManager->>OldInstance: shutdown()
ProcessManager->>NewInstance: create or reuse
ProcessManager->>platformInstances: store replacement
OldInstance->>JobQueue: disconnect() or shutdown()
OldInstance->>platformInstances: delete only if still current
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/data-layer/src/job-queue.test.ts (1)
294-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd error-path coverage for
disconnect().This test only covers the happy path (verifies
close/removeAllListenerscalled,pause/obliteratenot called).disconnect()has no internal try/catch, so a rejection fromqueue.close()/events.close()propagates directly to the caller — worth asserting that behavior, and optionally thatevents.close()is also invoked.As per path instructions,
**/*.test.ts: "Mock external dependencies (Redis, network calls) in tests and MUST test error paths, not just happy paths."✅ Suggested additional assertions
describe("disconnect", () => { it("closes connections without pausing or obliterating the queue", async () => { await jobQueue.disconnect(); sinon.assert.called(jobQueue.queue.removeAllListeners); sinon.assert.called(jobQueue.queue.close); sinon.assert.notCalled(jobQueue.queue.pause); sinon.assert.notCalled(jobQueue.queue.obliterate); + sinon.assert.called(jobQueue.events.close); }); + + it("propagates errors from the underlying connection close", async () => { + jobQueue.queue.close.rejects(new Error("close failed")); + await expect(jobQueue.disconnect()).rejects.toThrow("close failed"); + }); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/data-layer/src/job-queue.test.ts` around lines 294 - 303, `jobQueue.disconnect()` only has happy-path coverage; add an error-path test that makes `queue.close()` and/or `events.close()` reject and asserts the rejection is propagated to the caller since there is no internal try/catch. Use the existing `disconnect` test setup in `job-queue.test.ts` and the `jobQueue.disconnect`, `jobQueue.queue.close`, and `jobQueue.events.close` symbols to verify the failure behavior, while keeping the current assertions about `pause` and `obliterate` untouched.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/data-layer/src/job-queue.test.ts`:
- Around line 294-303: `jobQueue.disconnect()` only has happy-path coverage; add
an error-path test that makes `queue.close()` and/or `events.close()` reject and
asserts the rejection is propagated to the caller since there is no internal
try/catch. Use the existing `disconnect` test setup in `job-queue.test.ts` and
the `jobQueue.disconnect`, `jobQueue.queue.close`, and `jobQueue.events.close`
symbols to verify the failure behavior, while keeping the current assertions
about `pause` and `obliterate` untouched.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b873797-b3b4-49fc-b756-5f51788e5f42
📒 Files selected for processing (6)
packages/data-layer/src/job-queue.test.tspackages/data-layer/src/job-queue.tspackages/server/src/platform-instance.test.tspackages/server/src/platform-instance.tspackages/server/src/process-manager.test.tspackages/server/src/process-manager.ts
Summary
Fixes the
Parent Process Sudden TerminationCI flake (#1166), which turned out to be a real crash-recovery race in the server, not a timing issue.When a platform child crashes, its
closeevent fires tens to hundreds of ms after the process dies. If a client message arrives in that window,ProcessManager.ensureProcesscreates a replacement instance under the same identifier and Redis queue name. The stale instance'sshutdown()then destroyed the replacement's shared resources:queue.shutdown()paused andobliterate({force: true})d the shared-name queue, deleting the replacement's pending jobs — this is what vaporized the flaky test'ssigtermjob (8s of total silence in the failing run's logs)platformInstances.delete(this.id)unconditionally evicted the replacement from the instance map, so the next message spawned yet another child — the duplicate queue-inits and duplicate platform boots visible in the failing CI logsThe
ensureProcessreplacement path had the same defect ordering: it constructed the replacement first and then firedvoid existing.shutdown(), whose delayed delete/obliterate landed on the new instance.Changes
PlatformInstance.markReplaced()— flags an instance as superseded so its teardown leaves shared resources alonePlatformInstance.shutdown()— closes queue connections without pause/obliterate when replaced or no longer the map-slot owner; only deletes the map entry it still ownsProcessManager.ensureProcess()— marks a dead instance replaced before firing its async shutdown, and does both before constructing the replacementJobQueue.disconnect()(data-layer) — closes connections without pausing or obliterating the underlying Redis queueVerification
Deliberately unchanged: jobs enqueued between a child's death and its close event (while the dead instance still legitimately owns the queue) are still obliterated with it — preserving them would risk replaying the poison job that killed the platform (BullMQ stalled-job retry).
Fixes #1166
Summary by CodeRabbit
New Features
Bug Fixes
Tests