Skip to content

recover_deadlock: sync deferred data cursors before non-SI lock release - #6212

Open
dorinhogea wants to merge 1 commit into
bloomberg:mainfrom
dorinhogea:recovdlock-sync-dta-reland
Open

dorinhogea wants to merge 1 commit into
bloomberg:mainfrom
dorinhogea:recovdlock-sync-dta-reland

Conversation

@dorinhogea

Copy link
Copy Markdown
Contributor

The original merge of this PR (#6035) was not passing legacy regression tests due to changes in sqlite plan operation id numbers.
It was reverted here #6210.
We'll submit again here and take care of still failing legacy tests before merging.

FTR , the original PR description:
A non-snapshot read on a non-covering index defers its data lookup to a later seek (OP_DeferredSeek). If the scan releases locks in the meantime to let a waiting replication thread through, a concurrent DELETE can commit underneath it and the seek then fails with "Dta lookup lost the race". The same window loses out-of-line blobs, which is worse: no layer below sql reports a missed blob, so the row comes back with a silently zero-length column.

The fix is to pre-fetch before releasing. Each index cursor's paired data cursor -- the pairing is recorded when the vdbe emits the hint -- is read to its current row, blobs included, so the deferred seek is served from the captured row instead of going back to the btree. This lets the release itself widen to where the race actually lives: previously only SERIAL released here, now every non-SNAPISOL mode does, including the default read-committed path. To keep that safe the new release drops page locks only, holding the curtran and its table read locks so a schema change still cannot slip in; a waiter parked too long falls back to the old full release rather than starve behind a long scan. For the paths that do drop table locks, cursors re-resolve their table through the thread's rootpage map and compare table version, so a query racing a DROP reports CDB2ERR_SCHEMA instead of touching freed memory.

The visible cost is that readers now hand page locks back under contention where they used to hold them for the length of a scan. That is the intended trade, but it is a real behaviour change: rep_blocker.test measures exactly how long a reader can block replication and turns the feature off.

Tunables: recover_deadlock_sync_dta (on) enables the sync; pagelock_release_interval_ms (100) paces re-releases while a waiter persists; pagelock_release_max_wait_ms (60000) is the full-release fallback, 0 to disable. Adds tests/recovdlock.test.

@roborivers roborivers 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.

Cbuild submission: Success ✓.
Regression testing: Success ✓.

The first 10 failing tests are:
analyze_partial_index_off_generated [failed with core dumped] **quarantined**
analyze [failed with core dumped] **quarantined**
tsa
consumer_non_atomic_default_consumer_generated **quarantined**
tunables
sc_downgrade [timeout] **quarantined**
skipscan [timeout] **quarantined**

@dorinhogea

Copy link
Copy Markdown
Contributor Author

/plugin-branch sqlqueries

@roborivers roborivers 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.

Cbuild submission: Error ⚠.
Regression testing: Success ✓.

The first 10 failing tests are:
analyze_partial_index_off_generated [failed with core dumped] **quarantined**
analyze [failed with core dumped] **quarantined**
sc_resume_logicalsc_generated **quarantined**
consumer_non_atomic_default_consumer_generated **quarantined**
tunables
sc_downgrade [timeout] **quarantined**
skipscan [timeout] **quarantined**

@roborivers roborivers 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.

Cbuild submission: Success ✓.
Regression testing: Success ✓.

The first 10 failing tests are:
analyze_partial_index_off_generated [failed with core dumped] **quarantined**
analyze [failed with core dumped] **quarantined**
comdb2sys **quarantined**
consumer_non_atomic_default_consumer_generated **quarantined**
tunables
sc_downgrade [timeout] **quarantined**
skipscan [timeout] **quarantined**

A non-snapshot read on a non-covering index defers its data lookup to a later
seek (OP_DeferredSeek).  If the scan releases locks in the meantime to let a
waiting replication thread through, a concurrent DELETE can commit underneath
it and the seek then fails with "Dta lookup lost the race".  The same window
loses out-of-line blobs, which is worse: no layer below sql reports a missed
blob, so the row comes back with a silently zero-length column.

The fix is to pre-fetch before releasing.  Each index cursor's paired data
cursor -- the pairing is recorded when the vdbe emits the hint -- is read to
its current row, blobs included, so the deferred seek is served from the
captured row instead of going back to the btree.  This lets the release itself
widen to where the race actually lives: previously only SERIAL released here,
now every non-SNAPISOL mode does, including the default read-committed path.
To keep that safe the new release drops page locks only, holding the curtran
and its table read locks so a schema change still cannot slip in; a waiter
parked too long falls back to the old full release rather than starve behind a
long scan.  For the paths that do drop table locks, cursors re-resolve their
table through the thread's rootpage map and compare table version, so a query
racing a DROP reports CDB2ERR_SCHEMA instead of touching freed memory.

The visible cost is that readers now hand page locks back under contention
where they used to hold them for the length of a scan.  That is the intended
trade, but it is a real behaviour change: rep_blocker.test measures exactly
how long a reader can block replication and turns the feature off.

Tunables: recover_deadlock_sync_dta (on) enables the sync;
pagelock_release_interval_ms (100) paces re-releases while a waiter persists;
pagelock_release_max_wait_ms (60000) is the full-release fallback, 0 to
disable.  Adds tests/recovdlock.test.

Signed-off-by: Dorin Hogea <dhogea@bloomberg.net>
@dorinhogea
dorinhogea force-pushed the recovdlock-sync-dta-reland branch from 9551810 to 418aa72 Compare September 17, 2026 14:32

@roborivers roborivers 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.

Cbuild submission: Success ✓.
Regression testing: Success ✓.

The first 10 failing tests are:
analyze_partial_index_off_generated [failed with core dumped] **quarantined**
analyze [failed with core dumped] **quarantined**
sc_resume_logicalsc_generated **quarantined**
consumer_non_atomic_default_consumer_generated **quarantined**
sc_downgrade [timeout] **quarantined**
skipscan [timeout] **quarantined**

@dorinhogea

Copy link
Copy Markdown
Contributor Author

cdb2test Sep 17 11:53:59 2026 success recovdlock-sync-dta-reland.R20260917.7

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.

2 participants