Skip to content

fix(migrations): claim-with-status — interrupted runs resume instead of being skipped forever - #4001

Merged
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3992-migration-claim-status
Jul 28, 2026
Merged

fix(migrations): claim-with-status — interrupted runs resume instead of being skipped forever#4001
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3992-migration-claim-status

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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 stale running claims (grace window migrations.staleRunningGraceMs, default 10 min; younger = concurrent runner → wait-poll), warns loud, and re-runs. Legacy claims without status are treated done in 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

    • Interrupted migrations can now be retried instead of remaining permanently skipped.
    • Active migration runs are given time to complete, while stale claims are safely cleared and re-executed.
    • Existing migration records remain compatible and continue to be treated as completed.
  • Configuration

    • Added a configurable grace period for resolving interrupted migration runs, defaulting to 10 minutes.
  • Documentation

    • Added migration guidance, including the requirement that migration steps be safe to run more than once.

…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
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4e7734ee-8538-4168-9547-832b31ccee05

📥 Commits

Reviewing files that changed from the base of the PR and between 0163a94 and d8ca792.

📒 Files selected for processing (6)
  • lib/services/migrations.js
  • modules/core/migrations/20260728130000-noop-core-test-fixture.js
  • modules/core/repositories/migration.repository.js
  • modules/core/tests/fixtures/noop-migration.js
  • modules/core/tests/migrations.integration.tests.js
  • modules/core/tests/migrations.unit.tests.js

Walkthrough

Migration execution now records running and done states, preserves claim context, resolves stale claims during boot, supports legacy records, and adds configuration, documentation, unit tests, and integration tests for the updated lifecycle.

Changes

Migration lifecycle recovery

Layer / File(s) Summary
Migration claim and repository contracts
modules/core/models/migration.model.mongoose.js, modules/core/repositories/migration.repository.js
Migration records now track status, timestamps, process context, running claims, completion, and legacy records without status.
Runner claim execution and stale recovery
lib/services/migrations.js, config/defaults/development.config.js, MIGRATIONS.md
The runner resolves stale claims before filtering executed migrations, waits for fresh claims, deletes expired claims, and marks successful migrations done using the configured grace period.
Lifecycle and recovery validation
modules/core/tests/migrations.unit.tests.js, modules/core/tests/migrations.integration.tests.js
Tests cover claim metadata, completion and cleanup paths, legacy filtering, polling, stale deletion, warning logs, and configuration fallback.

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
Loading

Suggested labels: Fix

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is informative, but it does not follow the repository template and omits required Summary, Scope, Validation, and Guardrails sections. Rewrite the description to match the template headings and include Summary, Scope, Validation, Guardrails check, and Notes sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the migration-claim recovery behavior change and is specific to the main PR work.
Linked Issues check ✅ Passed The changes align with #3992 by adding running/done claim states, stale-claim boot recovery, and idempotence documentation.
Out of Scope Changes check ✅ Passed The code and docs changes stay focused on migration claim lifecycle handling and related tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3992-migration-claim-status

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.50%. Comparing base (fa3d02c) to head (d8ca792).
⚠️ Report is 1 commits behind head on master.

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              
Flag Coverage Δ
integration 61.79% <95.45%> (+0.26%) ⬆️
unit 75.99% <93.18%> (+0.19%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update fa3d02c...d8ca792. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fa3d02c and 0163a94.

📒 Files selected for processing (7)
  • MIGRATIONS.md
  • config/defaults/development.config.js
  • lib/services/migrations.js
  • modules/core/models/migration.model.mongoose.js
  • modules/core/repositories/migration.repository.js
  • modules/core/tests/migrations.integration.tests.js
  • modules/core/tests/migrations.unit.tests.js

Comment thread lib/services/migrations.js
Comment thread modules/core/repositories/migration.repository.js
Comment thread modules/core/tests/migrations.integration.tests.js
Comment thread modules/core/tests/migrations.integration.tests.js Outdated
Comment thread modules/core/tests/migrations.integration.tests.js
Comment thread modules/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
@PierreBrisorgueil
PierreBrisorgueil merged commit fb771a8 into master Jul 28, 2026
6 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the fix/3992-migration-claim-status branch July 28, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔧 Migration runner: crash mid-up() leaves migration marked executed — no retry path

1 participant