Fixed FOR UPDATE lock leaking to relation fetches during post edit#26864
Fixed FOR UPDATE lock leaking to relation fetches during post edit#26864
Conversation
WalkthroughThe edit method in the CRUD operations file was modified to create a separate fetchOptions object from the provided options when transacting is enabled. This fetchOptions object is used for the model.fetch call with the forUpdate lock applied, rather than mutating the original options object. Corresponding test assertions were updated to verify that the fetch method receives a shallow copy of options with an undefined lock property, and that the save method is called with the original filteredOptions object reference. 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
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 |
9eab148 to
04df7e0
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ghost/core/test/unit/server/models/base/crud.test.js (1)
162-170: Strengthen this test to actually verify the options-copy contract.Right now this can still pass even if
fetchreceives the same object asfilteredOptions. Add explicit reference inequality (and method boundary) checks so regressions are caught.✅ Suggested assertion upgrade
// fetch receives a shallow copy of options (without lock or method) const fetchOptions = fetchStub.args[0][0]; + assert.notEqual(fetchOptions, filteredOptions); assert.equal(fetchOptions.id, filteredOptions.id); assert.equal(fetchOptions.lock, undefined); + assert.equal(fetchOptions.method, undefined); const filteredData = filterDataSpy.returnValues[0]; assert.equal(saveStub.args[0][0], filteredData); assert.equal(saveStub.args[0][1].method, 'update'); assert.equal(saveStub.args[0][1], filteredOptions);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/test/unit/server/models/base/crud.test.js` around lines 162 - 170, The test must assert that fetch received a shallow copy of filteredOptions rather than the same object: add an assertion that fetchOptions is not the same reference as filteredOptions (e.g., assert.notStrictEqual(fetchOptions, filteredOptions)) and verify method/boundary isolation by asserting fetchOptions.method is undefined while saveStub.args[0][1].method === 'update'; optionally mutate fetchOptions (e.g., set a temp prop) and assert filteredOptions unchanged to prove they are distinct. Ensure you reference fetchStub, filteredOptions, fetchOptions, saveStub, and filterDataSpy when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@ghost/core/test/unit/server/models/base/crud.test.js`:
- Around line 162-170: The test must assert that fetch received a shallow copy
of filteredOptions rather than the same object: add an assertion that
fetchOptions is not the same reference as filteredOptions (e.g.,
assert.notStrictEqual(fetchOptions, filteredOptions)) and verify method/boundary
isolation by asserting fetchOptions.method is undefined while
saveStub.args[0][1].method === 'update'; optionally mutate fetchOptions (e.g.,
set a temp prop) and assert filteredOptions unchanged to prove they are
distinct. Ensure you reference fetchStub, filteredOptions, fetchOptions,
saveStub, and filterDataSpy when adding these assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ad91e6f3-21da-49de-84ab-ef91740a2179
📒 Files selected for processing (2)
ghost/core/core/server/models/base/plugins/crud.jsghost/core/test/unit/server/models/base/crud.test.js
Bookshelf propagates the `lock` option (e.g. 'forUpdate') to all eager-loaded relation queries via `_handleEager`. During post edits, this caused FOR UPDATE locks on tags, authors, products, revisions, posts_meta, emails, roles, and users tables — not just the post row. This large lock surface caused database deadlocks when multiple posts sharing the same tags/authors were edited concurrently. The fix overrides `_handleEager` on Ghost's base model to strip the `lock` option before it reaches relation queries. The FOR UPDATE lock still applies to the primary row fetch for update collision detection.
04df7e0 to
34be886
Compare
Summary
_handleEageron Ghost's base Bookshelf model to strip thelockoption before it reaches eager-loaded relation querieslock: 'forUpdate'acquired FOR UPDATE locks on 9 tables (posts, tags, users, products, post_revisions, posts_meta, emails, roles, roles_users) via Bookshelf's eager loading propagationContext
A customer was experiencing database deadlocks at the
PUT /ghost/api/admin/posts/:idendpoint. The root cause:crud.jssetsoptions.lock = 'forUpdate'for edit operations, and Bookshelf propagates this lock to all eager-loaded relation queries via_handleEager→EagerRelation.fetch()→Syncconstructor.Example deadlocking query from production:
The FOR UPDATE on relation fetches is unnecessary:
post_id), so no conflictTest plan
Innodb_row_lock_waits,Innodb_row_lock_time, andInnodb_deadlocksafter deploy