Skip to content

Slot merge does not cover records that outgrew their page: chunked and placeholder updates poison the page unconditionally #6129

Description

@lvca

Spun out of the #6127 item 3 re-triage of Issue5279ConcurrentUpdateTest.growingUpdatesUnderContentionKeepTheDatabaseConsistent. That test was being written off as a chronic flake; it is not flaky, and it is not a hole in the slot-merge logic either. It is the engine hitting a regime the merge does not cover at all.

What the measurement showed

The test runs 8 threads over 40 records of a single-bucket type, each round rewriting its own records with a longer payload (400 * (round + 1) chars). Instrumenting every poison site in LocalBucket.updateRecordInternal plus the commit-time outcomes in TransactionContext.commit1stPhase, one failing run gives:

counter value
slot rebases performed (isRebasableSlotPage taken) 146
records spilled out of their page (writeMultiPageRecord / placeholder branch) 30
ConcurrentModificationException reaching the retry loop 16
threads that ran out of the default 3 retries 5 of 8

While the payloads still fit the shared 64 KB page, the disjoint-slot merge absorbs everything - 146 successful rebases, zero conflicts. From round ~4 on, 40 * 3200 bytes no longer fit, every record becomes a multi-page chunk record, and from then on every update takes the FIRST_CHUNK branch, which poisons the page unconditionally. Eight threads then contend on one page where nothing is mergeable: one commit wins per round and the other seven get a CME. With TX_RETRIES at 3 that is not enough budget, and threads give up.

With a larger budget every run converges, CHECK DATABASE reports zero errors and every record holds exactly its last written value - so nothing is corrupt. The retry budget is the symptom; the unmergeable shape is the cause.

The gap

TX_PAGE_SLOT_MERGE covers, on a bucket page: an insert into a free slot, an in-place overwrite of the same size or smaller, an in-page growth (growRecordInPage, #5279), and the delete of a plain in-place record (#5569). It covers none of the shapes a record takes once it outgrows its page:

  • a slot holding a FIRST_CHUNK header whose continuation chunks live on other pages;
  • a slot holding a RECORD_PLACEHOLDER_POINTER, when the pointed-to content record has to be rebuilt rather than updated in place;
  • a RECORD_PLACEHOLDER_CONTENT record growing on its own page (excluded by the !isPlaceHolder guard in both growth branches).

Every one of those poisons its page, so on a bucket where records have grown past the page size, concurrent updates of unrelated records conflict for good - which is precisely the false conflict #5279 set out to remove, reappearing one size class up.

Worth investigating

The three shapes are not equally hard, and they should be judged separately rather than as one feature:

  1. Placeholder content growth looks closest to already-solved. It is a single slot on its own page and growRecordInPage already knows the placeholder marker (isPlaceHolder); what is missing is carrying that flag through SlotRebaseBuffer so rebaseRecordOnPage accepts a rs[0] < RECORD_PLACEHOLDER_CONTENT pre-image instead of bailing on rs[0] <= 0. The pointer on the owner page is untouched by an in-place growth, so the RID is stable.
  2. Placeholder pointer rewrite is a fixed-size 8-byte slot write, but it is paired with a delete + create on another page, so the merge would have to reason about more than one page atomically.
  3. Chunk chains are the hardest: one logical write spans several pages and every one of them would have to rebase for the commit to survive, so the failure probability compounds rather than falls.

(1) alone may be enough to move the needle, and it is the one with a self-contained proof obligation. It is worth measuring what fraction of the conflicts in a realistic large-record workload each shape accounts for before committing to (2) or (3).

Whatever is decided, the payoff is concrete: Issue5279ConcurrentUpdateTest.growingUpdatesUnderContentionKeepTheDatabaseConsistent should pass at the TX_RETRIES default instead of needing an explicit budget, and its sibling growingUpdatesThatStayInsideTheirPageNeverConflict (added in the #6127 PR) should extend to payloads that outgrow the page with the conflict count still asserted at zero.

Not a regression

Nothing here is new behaviour; the shapes have never been rebasable. What is new is knowing that this - and not a race in growRecordInPage - is what the test has been reporting.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions