@@ -1037,12 +1037,16 @@ private void deleteRecordInternal(final RID rid, final boolean deletePlaceholder
10371037 .getPageToModify (new PageId (database , file .getFileId (), chunkPageId ), pageSize , false );
10381038 chunkRecordPositionInPage = getRecordPositionInPage (chunkPage , chunkPositionInPage );
10391039 if (chunkRecordPositionInPage == 0 )
1040- throw new DatabaseOperationException ("Error on fetching multi page record " + rid + " chunk " + chunkId );
1040+ // Chunk was deleted by a concurrent update — signal retry
1041+ throw new ConcurrentModificationException (
1042+ "Multi-page record " + rid + " chunk " + chunkId + " was modified concurrently. Please retry" );
10411043
10421044 recordSize = chunkPage .readNumberAndSize (chunkRecordPositionInPage );
10431045
10441046 if (recordSize [0 ] != NEXT_CHUNK )
1045- throw new DatabaseOperationException ("Error on fetching multi page record " + rid + " chunk " + chunkId );
1047+ // Chunk was overwritten by a concurrent operation — signal retry
1048+ throw new ConcurrentModificationException (
1049+ "Multi-page record " + rid + " chunk " + chunkId + " was modified concurrently. Please retry" );
10461050
10471051 try {
10481052 deleteRecordInternal (new RID (database , fileId , nextChunkPointer ), false , true );
@@ -1316,54 +1320,81 @@ private Binary loadMultiPageRecord(final RID originalRID, BasePage firstPage, in
13161320 final long initialFirstPageVersion = firstPage .getVersion ();
13171321
13181322 // READ ALL THE CHUNKS TILL THE END
1323+ boolean chainInconsistent = false ;
13191324 final Binary record = new Binary ();
1320- BasePage page = firstPage ;
1321- int currentRecordPositionInPage = recordPositionInPage ;
1322- long [] currentRecordSize = recordSize ;
1323-
1324- while (true ) {
1325- final int chunkSize = page .readInt ((int ) (currentRecordPositionInPage + currentRecordSize [1 ]));
1326- final long nextChunkPointer = page .readLong (
1327- (int ) (currentRecordPositionInPage + currentRecordSize [1 ] + INT_SERIALIZED_SIZE ));
1328- final Binary chunk = page .getImmutableView (
1329- (int ) (currentRecordPositionInPage + currentRecordSize [1 ] + INT_SERIALIZED_SIZE + LONG_SERIALIZED_SIZE ),
1330- chunkSize );
1331- record .append (chunk );
1332-
1333- if (nextChunkPointer == 0 )
1334- // LAST CHUNK
1335- break ;
1336-
1337- final int chunkPageId = (int ) (nextChunkPointer / maxRecordsInPage );
1338- final int chunkPositionInPage = (int ) (nextChunkPointer % maxRecordsInPage );
1325+ try {
1326+ BasePage page = firstPage ;
1327+ int currentRecordPositionInPage = recordPositionInPage ;
1328+ long [] currentRecordSize = recordSize ;
1329+
1330+ while (true ) {
1331+ final int chunkSize = page .readInt ((int ) (currentRecordPositionInPage + currentRecordSize [1 ]));
1332+ final long nextChunkPointer = page .readLong (
1333+ (int ) (currentRecordPositionInPage + currentRecordSize [1 ] + INT_SERIALIZED_SIZE ));
1334+ final Binary chunk = page .getImmutableView (
1335+ (int ) (currentRecordPositionInPage + currentRecordSize [1 ] + INT_SERIALIZED_SIZE + LONG_SERIALIZED_SIZE ),
1336+ chunkSize );
1337+ record .append (chunk );
1338+
1339+ if (nextChunkPointer == 0 )
1340+ // LAST CHUNK
1341+ break ;
1342+
1343+ final int chunkPageId = (int ) (nextChunkPointer / maxRecordsInPage );
1344+ final int chunkPositionInPage = (int ) (nextChunkPointer % maxRecordsInPage );
1345+
1346+ if (chunkPageId >= getTotalPages ()) {
1347+ chainInconsistent = true ;
1348+ break ;
1349+ }
13391350
1340- if ( chunkPageId >= getTotalPages () )
1341- throw new DatabaseOperationException ( "Invalid pointer to a chunk for record " + originalRID );
1351+ final BasePage nextPage = database . getTransaction ( )
1352+ . getPage ( new PageId ( database , file . getFileId (), chunkPageId ), pageSize );
13421353
1343- final BasePage nextPage = database .getTransaction ()
1344- .getPage (new PageId (database , file .getFileId (), chunkPageId ), pageSize );
1354+ final int nextRecordPositionInPage = getRecordPositionInPage (nextPage , chunkPositionInPage );
1355+ if (nextRecordPositionInPage == 0 ) {
1356+ chainInconsistent = true ;
1357+ break ;
1358+ }
13451359
1346- final int nextRecordPositionInPage = getRecordPositionInPage (nextPage , chunkPositionInPage );
1347- if (nextRecordPositionInPage == 0 )
1348- throw new DatabaseOperationException ("Chunk of record " + originalRID + " was deleted" );
1360+ if (nextPage .equals (page ) && currentRecordPositionInPage == nextRecordPositionInPage )
1361+ throw new DatabaseOperationException (
1362+ "Infinite loop on loading multi-page record " + originalRID + " chunk " + chunkPageId + "/"
1363+ + chunkPositionInPage );
13491364
1350- if (nextPage .equals (page ) && currentRecordPositionInPage == nextRecordPositionInPage ) {
1351- // AVOID INFINITE LOOP?
1352- LogManager .instance ().log (this , Level .SEVERE ,
1353- "Infinite loop on loading multi-page record " + originalRID + " chunk " + chunkPageId + "/"
1354- + chunkPositionInPage );
1355- throw new DatabaseOperationException (
1356- "Infinite loop on loading multi-page record " + originalRID + " chunk " + chunkPageId + "/"
1357- + chunkPositionInPage );
1358- }
1365+ page = nextPage ;
1366+ currentRecordPositionInPage = nextRecordPositionInPage ;
13591367
1360- page = nextPage ;
1361- currentRecordPositionInPage = nextRecordPositionInPage ;
1368+ currentRecordSize = page .readNumberAndSize (currentRecordPositionInPage );
13621369
1363- currentRecordSize = page .readNumberAndSize (currentRecordPositionInPage );
1370+ if (currentRecordSize [0 ] != NEXT_CHUNK ) {
1371+ chainInconsistent = true ;
1372+ break ;
1373+ }
1374+ }
1375+ } catch (final Exception e ) {
1376+ chainInconsistent = true ;
1377+ }
13641378
1365- if (currentRecordSize [0 ] != NEXT_CHUNK )
1366- throw new DatabaseOperationException ("Error on fetching multi page record " + originalRID );
1379+ if (chainInconsistent ) {
1380+ // Chain was modified by a concurrent transaction — retry by re-fetching the first page
1381+ if (retry < maxRetries ) {
1382+ LogManager .instance ().log (this , Level .FINE ,
1383+ "Multi-page record %s chain inconsistent during read (attempt %d/%d), retrying..." , originalRID ,
1384+ retry + 1 , maxRetries );
1385+ firstPage = database .getPageManager ().getImmutablePage (firstPageId , pageSize , false , true );
1386+ if (firstPage == null )
1387+ throw new ConcurrentModificationException (
1388+ "First page of multi-page record " + originalRID + " was removed during read" );
1389+ recordPositionInPage = getRecordPositionInPage (firstPage , (int ) (originalRID .getPosition () % maxRecordsInPage ));
1390+ if (recordPositionInPage == 0 )
1391+ throw new ConcurrentModificationException (
1392+ "Multi-page record " + originalRID + " was deleted during read" );
1393+ recordSize = firstPage .readNumberAndSize (recordPositionInPage );
1394+ continue ;
1395+ }
1396+ throw new ConcurrentModificationException (
1397+ "Multi-page record " + originalRID + " chain inconsistent after " + maxRetries + " retries" );
13671398 }
13681399
13691400 // VALIDATE: Check if the first page was modified during our read.
@@ -1452,13 +1483,28 @@ private void writeMultiPageRecord(final RID originalRID, final Binary buffer, Mu
14521483
14531484 if (!pageAnalysis .createNewPage ) {
14541485 nextPage = database .getTransaction ().getPageToModify (pageAnalysis .page .pageId , pageSize , false );
1455- newPosition = pageAnalysis .newRecordPositionInPage ;
1456- recordIdInPage = pageAnalysis .availablePositionIndex ;
14571486
1458- nextPage .writeUnsignedInt (PAGE_RECORD_TABLE_OFFSET + recordIdInPage * INT_SERIALIZED_SIZE , newPosition );
1487+ if (nextPage .getVersion () != pageAnalysis .page .getVersion ()) {
1488+ // Page was modified by another committed transaction since the space analysis — skip reuse
1489+ nextPage = null ;
1490+ } else {
1491+ newPosition = pageAnalysis .newRecordPositionInPage ;
1492+ recordIdInPage = pageAnalysis .availablePositionIndex ;
1493+
1494+ // Verify the slot is still available on the actual mutable page (guard against stale analysis)
1495+ final int existSlotOff = (int ) nextPage .readUnsignedInt (
1496+ PAGE_RECORD_TABLE_OFFSET + recordIdInPage * INT_SERIALIZED_SIZE );
1497+ final short curRecCount = nextPage .readShort (PAGE_RECORD_COUNT_IN_PAGE_OFFSET );
1498+ if (recordIdInPage < curRecCount && existSlotOff != 0 ) {
1499+ // Slot was consumed by another operation — fall through to create a new page
1500+ nextPage = null ;
1501+ } else {
1502+ nextPage .writeUnsignedInt (PAGE_RECORD_TABLE_OFFSET + recordIdInPage * INT_SERIALIZED_SIZE , newPosition );
14591503
1460- if (recordIdInPage >= pageAnalysis .totalRecordsInPage )
1461- nextPage .writeShort (PAGE_RECORD_COUNT_IN_PAGE_OFFSET , (short ) (recordIdInPage + 1 ));
1504+ if (recordIdInPage >= pageAnalysis .totalRecordsInPage )
1505+ nextPage .writeShort (PAGE_RECORD_COUNT_IN_PAGE_OFFSET , (short ) (recordIdInPage + 1 ));
1506+ }
1507+ }
14621508 }
14631509
14641510 if (nextPage == null ) {
@@ -1549,12 +1595,16 @@ private void updateMultiPageRecord(final RID originalRID, final Binary buffer, M
15491595 nextPage = database .getTransaction ().getPageToModify (new PageId (database , file .getFileId (), chunkPageId ), pageSize , false );
15501596 final int recordPositionInPage = getRecordPositionInPage (nextPage , chunkPositionInPage );
15511597 if (recordPositionInPage == 0 )
1552- throw new DatabaseOperationException ("Error on fetching multi page record " + originalRID );
1598+ // Chunk was deleted by a concurrent update to the same record — signal retry
1599+ throw new ConcurrentModificationException (
1600+ "Multi-page record " + originalRID + " chunk was modified concurrently. Please retry the operation" );
15531601
15541602 final long [] recordSize = nextPage .readNumberAndSize (recordPositionInPage );
15551603
15561604 if (recordSize [0 ] != NEXT_CHUNK )
1557- throw new DatabaseOperationException ("Error on fetching multi page record " + originalRID );
1605+ // Chunk was overwritten by a concurrent operation — signal retry
1606+ throw new ConcurrentModificationException (
1607+ "Multi-page record " + originalRID + " chunk was modified concurrently. Please retry the operation" );
15581608
15591609 newPosition = (int ) (recordPositionInPage + recordSize [1 ]);
15601610 chunkSize = nextPage .readInt (newPosition );
@@ -1570,18 +1620,32 @@ private void updateMultiPageRecord(final RID originalRID, final Binary buffer, M
15701620 final PageAnalysis pageAnalysis = findAvailableSpace (currentPage .pageId .getPageNumber (), totalSpaceNeeded , txPageCounter ,
15711621 true );
15721622 if (!pageAnalysis .createNewPage ) {
1573- // FOUND IT
15741623 nextPage = database .getTransaction ().getPageToModify (pageAnalysis .page .pageId , pageSize , false );
1575- newPosition = pageAnalysis .newRecordPositionInPage ;
1576- recordIdInPage = pageAnalysis .availablePositionIndex ;
1577-
1578- nextPage .writeUnsignedInt (PAGE_RECORD_TABLE_OFFSET + pageAnalysis .availablePositionIndex * INT_SERIALIZED_SIZE ,
1579- newPosition );
15801624
1581- if (recordIdInPage >= pageAnalysis .totalRecordsInPage )
1582- nextPage .writeShort (PAGE_RECORD_COUNT_IN_PAGE_OFFSET , (short ) (recordIdInPage + 1 ));
1583-
1584- updatePageStatistics (nextPage .pageId .getPageNumber (), pageAnalysis .spaceAvailableInCurrentPage , -totalSpaceNeeded );
1625+ if (nextPage .getVersion () != pageAnalysis .page .getVersion ()) {
1626+ // Page was modified by another committed transaction since the space analysis — skip reuse
1627+ nextPage = null ;
1628+ } else {
1629+ newPosition = pageAnalysis .newRecordPositionInPage ;
1630+ recordIdInPage = pageAnalysis .availablePositionIndex ;
1631+
1632+ // Verify the slot is still available on the actual mutable page (guard against stale analysis)
1633+ final int existSlotOff2 = (int ) nextPage .readUnsignedInt (
1634+ PAGE_RECORD_TABLE_OFFSET + recordIdInPage * INT_SERIALIZED_SIZE );
1635+ final short curRecCount2 = nextPage .readShort (PAGE_RECORD_COUNT_IN_PAGE_OFFSET );
1636+ if (recordIdInPage < curRecCount2 && existSlotOff2 != 0 ) {
1637+ // Slot was consumed by another operation — fall through to create a new page
1638+ nextPage = null ;
1639+ } else {
1640+ nextPage .writeUnsignedInt (PAGE_RECORD_TABLE_OFFSET + pageAnalysis .availablePositionIndex * INT_SERIALIZED_SIZE ,
1641+ newPosition );
1642+
1643+ if (recordIdInPage >= pageAnalysis .totalRecordsInPage )
1644+ nextPage .writeShort (PAGE_RECORD_COUNT_IN_PAGE_OFFSET , (short ) (recordIdInPage + 1 ));
1645+
1646+ updatePageStatistics (nextPage .pageId .getPageNumber (), pageAnalysis .spaceAvailableInCurrentPage , -totalSpaceNeeded );
1647+ }
1648+ }
15851649 }
15861650
15871651 if (nextPage == null ) {
0 commit comments