fix(mongoose): tolerate same-name index conflicts at boot so migrations can repair them - #4005
Conversation
…ns can repair them A schema index change that alters the options of an existing same-name index (e.g. adding a partialFilterExpression) made the app unable to boot against an already-deployed database: awaitIndexBuilds() propagated the MongoDB conflict rejection (code 85 IndexOptionsConflict / 86 IndexKeySpecsConflict) before the migration runner — which runs right after it in bootstrap and owns the drop/recreate repair — could execute. The repair was gated behind the very defect it repairs. This deliberately narrows the #3990 fail-fast contract: conflict codes 85/86 describe environment state (a legacy index on a deployed database), not a code bug. They are now logged at error level — model, driver error, and best-effort declared (schema) vs live (collection) specs — and boot continues serving on the stale live index so the migration can reconcile. Every other rejection (e.g. an invalid index declaration) still propagates and fails boot exactly as before. Closes #4004
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4005 +/- ##
==========================================
+ Coverage 93.50% 93.52% +0.01%
==========================================
Files 170 170
Lines 5727 5744 +17
Branches 1839 1843 +4
==========================================
+ Hits 5355 5372 +17
Misses 302 302
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:
|
|
Warning Review limit reached
Next review available in: 47 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 (2)
Walkthrough
ChangesMongoDB index conflict tolerance
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Startup
participant awaitIndexBuilds
participant MongooseModel
participant Logger
Startup->>awaitIndexBuilds: wait for model index initialization
awaitIndexBuilds->>MongooseModel: call init()
MongooseModel-->>awaitIndexBuilds: return conflict 85/86
awaitIndexBuilds->>Logger: log model and declared/live index details
awaitIndexBuilds-->>Startup: continue boot
Possibly related PRs
🚥 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.
Actionable comments posted: 2
🤖 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/mongoose.js`:
- Around line 63-80: Update logIndexConflict to pass the raw err as the second
argument to the primary logger.error call while retaining the model context in
the message. Log the declared schema indexes immediately after
model.schema.indexes() succeeds, before awaiting listIndexes().toArray(), then
log the live indexes separately; preserve the existing best-effort catch logging
for enumeration failures.
In `@lib/services/tests/mongoose.awaitIndexBuilds.unit.tests.js`:
- Around line 236-248: Add JSDoc annotations to the setupWithConflictingModel
helper, documenting its initError and listIndexes parameters and the
Promise-based return value. Keep the existing explanatory context and helper
behavior unchanged.
🪄 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: 68ffc814-8185-47bc-8ae1-0791178d1727
📒 Files selected for processing (2)
lib/services/mongoose.jslib/services/tests/mongoose.awaitIndexBuilds.unit.tests.js
…failure Addresses CodeRabbit review on PR #4005: - logIndexConflict now passes the raw driver error as logger.error's second arg (matches this file's own convention at the timeout log a few lines below, and the codebase-wide two-arg pattern) - log the declared (schema) spec immediately after computing it, before awaiting listIndexes(), so a live-fetch failure doesn't discard an already-available declared spec - JSDoc for the new setupWithConflictingModel test helper
Summary
awaitIndexBuilds()now tolerates a same-name index build rejection from the driver (MongoDB codes 85IndexOptionsConflict/ 86IndexKeySpecsConflict) instead of propagating it. On a tolerated conflict it logs at error level — model name, the raw driver error, and (best-effort) both the schema-declared and live collection index specs — then lets boot continue. Every other rejection (invalid declarations, unsupported operators, etc.) still propagates exactly as before.awaitIndexBuilds()inbootstrap(). Propagating the rejection therefore gated the repair behind the very defect it exists to repair: the app could never boot far enough to run the migration that would fix it, with no recovery path short of manual index surgery on the database. Tolerating the conflict lets boot reach the migration runner; the app serves with the stale live index (writes keep obeying the old constraint) until the migration reconciles it.Scope
lib/services/mongoose.js(index-build tolerance) only. No downstream/module code changed.nonelow— the change only widens what boot tolerates (two specific, well-identified driver error codes); every other error path is unchanged and still fails fast.Alternative considered
Running migrations before index builds would also unblock this case, but it was rejected: it inverts the #3990 invariant that unique-index idempotency guards exist before anything writes, and it silently invalidates the ordering assumptions already codified in shipped migrations (e.g.
modules/billing/migrations/20260727120000-fix-usage-month-index-partial-filter.js, which assumes the boot-time index build has already run). Narrowing the fail-fast contract to exclude only same-name conflicts (85/86) preserves that ordering while removing the boot-blocking trap.Regression tests
4 new unit tests in
lib/tests/mongoose.awaitIndexBuilds.unit.tests.jscover:codeName(IndexKeySpecsConflict, code 86) is tolerated and logged, boot proceedsValidation
npm run lintnpm testGuardrails check
.env*,secrets/**, keys, tokens)Notes for reviewers
Summary by CodeRabbit
Bug Fixes
Tests