Repository navigation
fix(permalink): use a shared lock when re-reading a concurrently created permalink - #45102
Conversation
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #45102 +/- ##
=======================================
Coverage 82.83% 82.83%
=======================================
Files 3017 3017
Lines 192698 192709 +11
Branches 44914 44914
=======================================
+ Hits 159624 159635 +11
Misses 30040 30040
Partials 3034 3034
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/review |
CodeAnt PR Risk: Low Risk
Assessed commit: |
There was a problem hiding this comment.
Thanks for tracking this down. The shared-lock re-read makes sense: a locking read still returns the winner's committed row under REPEATABLE READ, and shared locks don't conflict with each other, so the 3+ loser deadlock goes away.
Suggestion: replace the booleans with a lock-options object
get_entry now has two mutually exclusive flags, for_update and for_share. Nothing enforces that. With if/elif, passing both silently ignores for_share. Every new lock mode (nowait, skip_locked, ...) would also need another boolean. Query.with_for_update already takes several arguments, so I'd model them directly:
@dataclass(frozen=True)
class RowLock:
read: bool = False # shared lock (FOR SHARE / LOCK IN SHARE MODE)
nowait: bool = False
skip_locked: bool = False
key_share: bool = False # Postgres only
def apply(self, query: Query) -> Query:
return query.with_for_update(
read=self.read,
nowait=self.nowait,
skip_locked=self.skip_locked,
key_share=self.key_share,
)
def get_entry(cls, resource, key, lock: RowLock | None = None) -> KeyValueEntry | None:
query = db.session.query(KeyValueEntry).filter_by(**get_filter(resource, key))
if lock is not None:
query = lock.apply(query)
return query.first()…ted permalink On InnoDB, each request that loses the duplicate-key race already holds a shared lock on the duplicate index record. With 3 or more concurrent identical requests, their exclusive FOR UPDATE re-reads wait on each other's shared locks and InnoDB aborts one with a deadlock, so the request still returns 500. Re-read the winner's row with a shared locking read instead. Shared locks are compatible with each other, and a locking read still sees the latest committed row under REPEATABLE READ. Nothing after the re-read writes to the row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ea8bb63 to
e3e60fc
Compare
…ted permalink (#45102) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit cb8ff28) Backport notes: dropped the hunks for ReportConfigDAO, the ownership-checked KV lock release and their tests, which do not exist on 6.2. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SUMMARY
After #45059, bursts of 3 or more identical permalink requests for the same dashboard can still return 500 on MySQL. Each request that loses the race already holds a shared lock on the duplicate row, then asks for an exclusive lock to re-read it. The losers end up waiting on each other and MySQL aborts one of them with a deadlock (
1213 Deadlock found when trying to get lock). Two concurrent requests don't trigger it.This change makes that re-read take a shared lock instead. Shared locks don't conflict with each other, so the deadlock goes away, and the re-read still sees the row the winning request just committed. The exclusive lock used by the distributed-lock release is unchanged.
TESTING INSTRUCTIONS
Unit tests added and updated.
ADDITIONAL INFORMATION
🤖 Generated with Claude Code