Skip to content

Record placeholders are the pre-chunk spill mechanism, and the 14 bytes that force them are usually available #6149

Description

@lvca

Noticed while working on #6129 (PR #6140), where the placeholder turned out to be the least valuable of the three shapes that issue listed - and the one behind the still-open #6141.

Placeholders are still reachable, but they are the pre-chunk mechanism and there is exactly one way to get one

LocalBucket has a single site that creates a placeholder pointer, in the "record must spill out of the page" branch of updateRecordInternal:

int availableSpaceInCurrentPage = (int) (recordSize[0] + recordSize[1]);
if (lastRecordPositionInPage == recordPositionInPage)
  availableSpaceInCurrentPage += page.getMaxContentSize() - pageOccupiedInBytes;

// TODO: LOOK FOR 1/2 OF THE RECORD SIZE
if (availableSpaceInCurrentPage < 2 + LONG_SERIALIZED_SIZE + INT_SERIALIZED_SIZE) {
  page.writeNumber(recordPositionInPage, RECORD_PLACEHOLDER_POINTER);
  final RID realRID = createRecordInternal(record, true, false);   // the only isPlaceHolder=true call in the class
  ...
} else {
  // SPLIT THE RECORD IN CHUNKS ... ISSUE https://github.com/ArcadeData/arcadedb/issues/332

So a placeholder happens when, and only when, a record that has to spill occupies fewer than 14 bytes in its slot (plus the page tail if it is the last record) - too little to host a FIRST_CHUNK header, which needs 1 marker + 4 size + 8 next-pointer. Everything else spills into chunks. The multi-page chunk chain arrived later (#332) and took over every case the placeholder used to serve except that one; MINIMUM_RECORD_SIZE's own comment records the overlap ("5 BYTES IS THE SPACE REQUIRED TO HOST THE PLACEHOLDER AND 1ST CHUCK FOR MULTI-PAGE CONTENT").

It is not dead code - Issue5279ConcurrentUpdateTest.aPlaceholderPointerRebuiltUnderConflictFallsBackAndStaysCorrect and Issue6129ChunkedSlotMergeTest.updatingPlaceholderContentCommutesWithItsPageNeighbours both produce one and assert totalPlaceholderRecords == 1 - but every route to it is that single 14-byte test.

The 14 bytes are usually there for the asking

The branch is reached because the page could not host the record's full new size. That says nothing about whether it has 14 bytes: growRecordInPage already shifts the records that follow to the right, and refuses only when

additionalSpaceNeeded >= page.getMaxContentSize() - pageOccupiedInBytes

Getting a 9-byte slot up to 14 needs 5 more bytes, not 30 KB of them. A page that has just refused a large growth commonly has kilobytes free - in the #6129 test above, page 0 has roughly 1 KB free when the placeholder is created. The code simply never asks.

Proposal

Before falling back to the placeholder, try to grow the slot to the chunk-header minimum through the existing in-page shift, and spill into chunks. That reuses machinery already on this path, and leaves the placeholder reachable only when the page cannot spare ~13 bytes at all - if it can be shown to be unreachable, the whole RECORD_PLACEHOLDER_POINTER/RECORD_PLACEHOLDER_CONTENT family becomes removable (they are read in ~10 places across the scan, check, delete, update, compaction and space-accounting paths).

Why it is worth more than the code it deletes

  1. It would close A concurrent update of the same placeholder-backed record is silently lost, with no conflict raised #6141 by construction. That silent lost update - two transactions updating the same placeholder-backed record, neither raising a conflict - is placeholder-only. Chunked records do not have it, because any update rewrites the head chunk on the page the transaction pinned.
  2. It is the one record shape the disjoint-slot merge still cannot replay end to end: the pointer rewrite changes two pages at once (Slot merge does not cover records that outgrew their page: chunked and placeholder updates poison the page unconditionally #6129 covers the content record, not the pointer).
  3. Records that outgrow their page would have exactly one representation instead of two, and every reader (check, scan, delete, compressPage, getPageOccupiedInBytes, getOrderedRecordsInPage) would lose a branch.

Backward compatibility is the obvious constraint: existing databases contain placeholders, so the readers have to stay until a format migration retires them. Only the writer would stop producing new ones.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions