fix(migrations): claim-with-status — interrupted runs resume instead of being skipped forever - #4001
Conversation
…of skipping forever (#3992) A hard kill (OOM/SIGKILL/pod eviction) mid-up() left the pre-existing claim-before-up() insert in place with no completion signal, so the runner treated a partially-applied migration as done and skipped it forever on every later boot. Migration claims now carry status:'running'|'done' plus startedAt, finishedAt, pid, and host. up() success flips the claim to 'done' atomically; the thrown-error path (unclaim, retry next boot) is unchanged. Boot-time resolveStaleClaims() settles every leftover 'running' claim before the executed-set comparison: within config.migrations.staleRunningGraceMs (default 10 min) it waits and polls (could be a genuinely concurrent runner mid-deploy); past the grace window it warns loud, deletes the claim, and lets it re-run. Back-compat: a record with no status field (pre-#3992) is always treated as done, never re-interpreted. New house rule documented in MIGRATIONS.md: every migration's up() must be idempotent (already the de facto style; now enforced by the resume behavior). Claude-Session: https://claude.ai/code/session_01K6NtYTXHfNcpAQ3TJXWJxL
Post-verify cleanup from a 4-angle review: - resolveStaleClaims resolves running claims concurrently (Promise.all) instead of a sequential for-loop, mirroring mongoose.js's awaitIndexBuilds shape. Sequential resolution made boot time scale additively with the number of in-flight claims on a multi-replica rolling deploy; each claim is independent so there is no ordering need to serialize them. - Drop the dead current.executedAt fallback in resolveRunningClaim: repository.claim() is the only writer of status:'running' and always sets startedAt, so the branch (and its dedicated test) was unreachable in production. - migration.repository.js#claim now stamps executedAt and startedAt from a single Date() instead of two separate calls. Skipped (would change intended behavior or add net complexity, not reduce it): reusing lib/services/distributedLock.js's TTL-lock model (different semantics — the issue's own spec calls for a status field, not a renewable lock); merging create()/claim() into one function (they serve genuinely different callers); replacing the pollIntervalMs test-seam parameter with jest fake timers (the parameter mirrors the codebase's own cfg-param convention in awaitIndexBuilds); collapsing the two "waits" unit tests into one it.each (they exercise distinct exit branches of the poll loop). Claude-Session: https://claude.ai/code/session_01K6NtYTXHfNcpAQ3TJXWJxL
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
WalkthroughMigration execution now records ChangesMigration lifecycle recovery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant MigrationRunner
participant MigrationRepository
participant MongoDB
participant MigrationFile
MigrationRunner->>MigrationRepository: resolve running claims
MigrationRepository->>MongoDB: list and inspect claim records
MongoDB-->>MigrationRepository: running or completed status
MigrationRunner->>MigrationRepository: delete stale claim or wait for completion
MigrationRunner->>MigrationRepository: claim migration with pid and host
MigrationRunner->>MigrationFile: execute up()
MigrationRunner->>MigrationRepository: mark migration done
MigrationRepository->>MongoDB: persist done status and finishedAt
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4001 +/- ##
==========================================
+ Coverage 93.43% 93.50% +0.07%
==========================================
Files 170 170
Lines 5693 5727 +34
Branches 1828 1840 +12
==========================================
+ Hits 5319 5355 +36
+ Misses 304 302 -2
Partials 70 70
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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.
Inline comments:
In `@lib/services/migrations.js`:
- Around line 152-177: Update resolveRunningClaim to treat a non-finite ageMs,
caused by an unusable startedAt, as immediately stale alongside records whose
age reaches graceMs. Ensure the stale-claim warning and unclaimMigration path
execute without waiting, while preserving the existing grace-window polling
behavior for finite ages.
In `@modules/core/repositories/migration.repository.js`:
- Around line 83-100: Update markDone and the failure-path deleteByName usage to
match the caller’s owned claim, filtering completion by name plus status
"running" and ownership fields. Change
lib/services/migrations.js#unclaimMigration to pass the current pid and host,
while keeping resolveRunningClaim’s stale-residue deletion unscoped. Ensure an
old runner cannot mark or delete a newer runner’s record.
In `@modules/core/tests/migrations.integration.tests.js`:
- Around line 104-109: Replace the billing-dependent targetName in
modules/core/tests/migrations.integration.tests.js at lines 104-109 with a
core-owned no-op migration fixture that the suite creates and tears down with
its Migration record. In modules/core/tests/migrations.unit.tests.js at lines
155-167, replace realNoopMigration with a local fixture module exporting async
up() {}, avoiding database dependence. Keep both suites self-contained within
modules/core.
- Around line 72-84: Update the “claim-with-status (`#3992`)” test to create and
claim a migration record within the test, then call the relevant markDone flow
before asserting status, timestamps, pid, and host. Stop querying the
pre-existing add-meter-fields record so assertions always target the record
created by this suite.
- Around line 119-134: Guard the restore logic in the afterEach hook by checking
that originalRecord exists before accessing its fields or calling
Migration.updateOne. If the beforeEach snapshot was not created, skip
restoration so the original assertion failure is preserved.
In `@modules/core/tests/migrations.unit.tests.js`:
- Around line 250-321: Add a test in the resolveRunningClaim (`#3992`) suite for a
running record without startedAt, such as no-started-at.js. Invoke
migrations.resolveRunningClaim with the existing grace/poll options and assert
it resolves immediately without deleting the claim or logging a warning,
covering the missing-startedAt guard path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5e3b077b-205a-4a29-af26-b5bb8c672aa0
📒 Files selected for processing (7)
MIGRATIONS.mdconfig/defaults/development.config.jslib/services/migrations.jsmodules/core/models/migration.model.mongoose.jsmodules/core/repositories/migration.repository.jsmodules/core/tests/migrations.integration.tests.jsmodules/core/tests/migrations.unit.tests.js
…rd (#3992) CodeRabbit convergence on #4001: - resolveRunningClaim: a running claim with a missing/unparsable startedAt now resolves immediately as stale (Number.isFinite guard) instead of spinning the poll loop forever on a NaN-coerced ~1ms setTimeout. - New ownership-scoped unclaim path: a runner's own failure (import error, no up() export, up() throws) now deletes ONLY the claim it inserted (name + status:'running' + matching pid/host, migrationRepository.deleteClaim), never a differently-owned record. markDone is now scoped to status:'running' too. Together they close a race where a late failure from a runner whose claim was already cleared as stale could delete a newer runner's completed record. resolveRunningClaim's own stale-residue deletion stays unscoped by design (it intentionally clears a claim it does not own). - migrations.unit/integration.tests.js no longer depend on modules/billing's migration file as a test fixture (module self-containment) — new modules/core/tests/fixtures/noop-migration.js (unit) and modules/core/migrations/20260728130000-noop-core-test-fixture.js (integration, real no-op migration owned by core). - afterEach in the boot-time stale-claim resolution suite now guards against a missing snapshot so a beforeEach sanity-check failure isn't masked by a TypeError. Claude-Session: https://claude.ai/code/session_01K6NtYTXHfNcpAQ3TJXWJxL
What — Migration claims carry
status: running|done(+ started/finished/pid/host). A hard kill mid-up()no longer strands the migration as falsely-executed: boot detects stalerunningclaims (grace windowmigrations.staleRunningGraceMs, default 10 min; younger = concurrent runner → wait-poll), warns loud, and re-runs. Legacy claims withoutstatusare treateddonein exactly one place. MIGRATIONS.md now makes the de-facto idempotence style an explicit contract (verified across all 17 in-tree migrations).Why — The runner claimed BEFORE
up()completed: OOM/eviction mid-migration left it permanently skipped with a partially-applied database (found reviewing #3990's backfill).Review gate: kimi OK-with-nits — the one medium is the design's own documented residual (a >grace-window slow migration can be re-claimed; mitigations: config override + the idempotence mandate). Suites: unit 2284, integration 534, lint clean. Simplify pass kept (concurrent stale-resolution, 2 dead-code drops).
Closes #3992
Summary by CodeRabbit
Bug Fixes
Configuration
Documentation