diff --git a/community/index/src/main/java/org/neo4j/index/internal/gbptree/DynamicSizeUtil.java b/community/index/src/main/java/org/neo4j/index/internal/gbptree/DynamicSizeUtil.java index 02af267426336..9eded791b413f 100644 --- a/community/index/src/main/java/org/neo4j/index/internal/gbptree/DynamicSizeUtil.java +++ b/community/index/src/main/java/org/neo4j/index/internal/gbptree/DynamicSizeUtil.java @@ -108,4 +108,9 @@ private static int withTombstoneFlag( int keySize ) assert (keySize & FLAG_TOMBSTONE) == 0 : "Key size " + keySize + " is to large to fit tombstone."; return keySize | FLAG_TOMBSTONE; } + + static int stripTombstone( int keySize ) + { + return keySize & ~FLAG_TOMBSTONE; + } } diff --git a/community/index/src/main/java/org/neo4j/index/internal/gbptree/InternalTreeLogic.java b/community/index/src/main/java/org/neo4j/index/internal/gbptree/InternalTreeLogic.java index 53873e6614d38..1f62edfb06982 100644 --- a/community/index/src/main/java/org/neo4j/index/internal/gbptree/InternalTreeLogic.java +++ b/community/index/src/main/java/org/neo4j/index/internal/gbptree/InternalTreeLogic.java @@ -32,6 +32,8 @@ import static org.neo4j.index.internal.gbptree.StructurePropagation.KeyReplaceStrategy.REPLACE; import static org.neo4j.index.internal.gbptree.StructurePropagation.UPDATE_MID_CHILD; import static org.neo4j.index.internal.gbptree.StructurePropagation.UPDATE_RIGHT_CHILD; +import static org.neo4j.index.internal.gbptree.TreeNode.Overflow.NEED_DEFRAG; +import static org.neo4j.index.internal.gbptree.TreeNode.Overflow.YES; import static org.neo4j.index.internal.gbptree.TreeNode.Type.INTERNAL; import static org.neo4j.index.internal.gbptree.TreeNode.Type.LEAF; @@ -555,13 +557,19 @@ private void insertInLeaf( PageCursor cursor, StructurePropagation structur createSuccessorIfNeeded( cursor, structurePropagation, UPDATE_MID_CHILD, stableGeneration, unstableGeneration ); - if ( bTreeNode.leafOverflow( cursor, keyCount, key, value ) ) + TreeNode.Overflow overflow = bTreeNode.leafOverflow( cursor, keyCount, key, value ); + if ( overflow == YES ) { // Overflow, split leaf splitLeaf( cursor, structurePropagation, key, value, keyCount, stableGeneration, unstableGeneration ); return; } + if ( overflow == NEED_DEFRAG ) + { + bTreeNode.defragmentLeaf( cursor ); + } + // No overflow, insert key and value bTreeNode.insertKeyValueAt( cursor, key, value, pos, keyCount ); TreeNode.setKeyCount( cursor, keyCount + 1 ); @@ -642,22 +650,11 @@ private void splitLeaf( PageCursor cursor, StructurePropagation structurePr // Position where newKey / newValue is to be inserted int pos = positionOf( search( cursor, LEAF, newKey, readKey, keyCount ) ); - int keyCountAfterInsert = keyCount + 1; - int middlePos = middle( keyCountAfterInsert ); structurePropagation.hasRightKeyInsert = true; structurePropagation.midChild = current; structurePropagation.rightChild = newRight; - if ( middlePos == pos ) - { - layout.copyKey( newKey, structurePropagation.rightKey ); - } - else - { - bTreeNode.keyAt( cursor, structurePropagation.rightKey, pos < middlePos ? middlePos - 1 : middlePos, LEAF ); - } - try ( PageCursor rightCursor = cursor.openLinkedCursor( newRight ) ) { // Initialize new right @@ -665,10 +662,9 @@ private void splitLeaf( PageCursor cursor, StructurePropagation structurePr bTreeNode.initializeLeaf( rightCursor, stableGeneration, unstableGeneration ); TreeNode.setRightSibling( rightCursor, oldRight, stableGeneration, unstableGeneration ); TreeNode.setLeftSibling( rightCursor, current, stableGeneration, unstableGeneration ); - int rightKeyCount = keyCountAfterInsert - middlePos; // Do split - bTreeNode.doSplitLeaf( cursor, keyCount, rightCursor, rightKeyCount, pos, newKey, newValue, middlePos ); + bTreeNode.doSplitLeaf( cursor, keyCount, rightCursor, pos, newKey, newValue, structurePropagation ); } // Update old right with new left sibling (newRight) diff --git a/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNode.java b/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNode.java index 8b476c2019975..a8cb1748b0f5a 100644 --- a/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNode.java +++ b/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNode.java @@ -72,6 +72,13 @@ enum Type INTERNAL } + enum Overflow + { + YES, + NO, + NEED_DEFRAG + } + // Shared between all node types: TreeNode and FreelistNode static final int BYTE_POS_NODE_TYPE = 0; static final byte NODE_TYPE_TREE_NODE = 1; @@ -320,7 +327,12 @@ static void goTo( PageCursor cursor, String messageOnError, long nodeId ) * Will leaf overflow if inserting new key and value? * @return true if leaf will overflow, else false. */ - abstract boolean leafOverflow( PageCursor cursor, int currentKeyCount, KEY newKey, VALUE newValue ); + abstract Overflow leafOverflow( PageCursor cursor, int currentKeyCount, KEY newKey, VALUE newValue ); + + /** + * Clean page from garbage to make room for further insert without having to split. + */ + abstract void defragmentLeaf( PageCursor cursor ); abstract boolean leafUnderflow( int keyCount ); @@ -329,15 +341,14 @@ static void goTo( PageCursor cursor, String messageOnError, long nodeId ) abstract boolean canMergeLeaves( int leftKeyCount, int rightKeyCount ); /** - * Performs the entry moving part of split in leaf. + * Calculate where split should be done and move entries between leaves participating in split. * * Keys and values from left are divide between left and right and the new key and value is inserted where it belongs. * * Key count is updated. */ - abstract void doSplitLeaf( PageCursor leftCursor, int leftKeyCount, PageCursor rightCursor, int rightKeyCount, int insertPos, - KEY newKey, - VALUE newValue, int middlePos ); + abstract void doSplitLeaf( PageCursor leftCursor, int leftKeyCount, PageCursor rightCursor, int insertPos, KEY newKey, VALUE newValue, + StructurePropagation structurePropagation ); /** * Performs the entry moving part of split in internal. diff --git a/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSize.java b/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSize.java index 46938c738a38d..d9c0bdb4b865c 100644 --- a/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSize.java +++ b/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSize.java @@ -21,6 +21,7 @@ import java.util.StringJoiner; +import org.neo4j.collection.primitive.PrimitiveIntStack; import org.neo4j.io.pagecache.PageCursor; import static java.lang.String.format; @@ -36,14 +37,16 @@ import static org.neo4j.index.internal.gbptree.DynamicSizeUtil.readKeyOffset; import static org.neo4j.index.internal.gbptree.DynamicSizeUtil.readKeySize; import static org.neo4j.index.internal.gbptree.DynamicSizeUtil.readValueSize; +import static org.neo4j.index.internal.gbptree.DynamicSizeUtil.stripTombstone; import static org.neo4j.index.internal.gbptree.GenerationSafePointerPair.read; import static org.neo4j.index.internal.gbptree.TreeNode.Type.INTERNAL; import static org.neo4j.index.internal.gbptree.TreeNode.Type.LEAF; public class TreeNodeDynamicSize extends TreeNode { - private static final int BYTE_POS_ALLOCSPACE = BASE_HEADER_LENGTH; - private static final int HEADER_LENGTH_DYNAMIC = BYTE_POS_ALLOCSPACE + BYTE_SIZE_OFFSET; + private static final int BYTE_POS_ALLOCOFFSET = BASE_HEADER_LENGTH; + private static final int BYTE_POS_DEADSPACE = BYTE_POS_ALLOCOFFSET + bytesPageOffset(); + private static final int HEADER_LENGTH_DYNAMIC = BYTE_POS_DEADSPACE + bytesPageOffset(); private static final int LEAST_NUMBER_OF_ENTRIES_PER_PAGE = 2; private static final int MINIMUM_ENTRY_SIZE_CAP = Long.SIZE; @@ -67,19 +70,7 @@ public class TreeNodeDynamicSize extends TreeNode @Override void writeAdditionalHeader( PageCursor cursor ) { - setAllocSpace( cursor, pageSize ); - } - - private void setAllocSpace( PageCursor cursor, int allocSpace ) - { - cursor.setOffset( BYTE_POS_ALLOCSPACE ); - putKeyOffset( cursor, allocSpace ); - } - - int getAllocSpace( PageCursor cursor ) - { - cursor.setOffset( BYTE_POS_ALLOCSPACE ); - return readKeyOffset( cursor ); + setAllocOffset( cursor, pageSize ); } @Override @@ -96,62 +87,20 @@ KEY keyAt( PageCursor cursor, KEY into, int pos, Type type ) } if ( type == LEAF ) { - progressCursor( cursor, DynamicSizeUtil.BYTE_SIZE_VALUE_SIZE ); + progressCursor( cursor, bytesValueSize() ); } layout.readKey( cursor, into, keySize ); return into; } - private void placeCursorAtActualKey( PageCursor cursor, int pos, Type type ) - { - // Set cursor to correct place in offset array - int keyPosOffset = keyPosOffset( pos, type ); - cursor.setOffset( keyPosOffset ); - - // Read actual offset to key - int keyOffset = readKeyOffset( cursor ); - - // Verify offset is reasonable - if ( keyOffset > pageSize ) - { - cursor.setCursorException( "Tried to read key on offset " + keyOffset + ". Page size is " + pageSize ); - } - - // Set cursor to actual offset - cursor.setOffset( keyOffset ); - } - - private int keyPosOffset( int pos, Type type ) - { - if ( type == LEAF ) - { - return keyPosOffsetLeaf( pos ); - } - else - { - return keyPosOffsetInternal( pos ); - } - } - - private int keyPosOffsetLeaf( int pos ) - { - return HEADER_LENGTH_DYNAMIC + pos * BYTE_SIZE_OFFSET; - } - - private int keyPosOffsetInternal( int pos ) - { - // header + childPointer + pos * (keyPosOffsetSize + childPointer) - return HEADER_LENGTH_DYNAMIC + childSize() + pos * keyChildSize(); - } - @Override void insertKeyAndRightChildAt( PageCursor cursor, KEY key, long child, int pos, int keyCount, long stableGeneration, long unstableGeneration ) { // Where to write key? - int currentKeyOffset = getAllocSpace( cursor ); + int currentKeyOffset = getAllocOffset( cursor ); int keySize = layout.keySize( key ); - int newKeyOffset = currentKeyOffset - BYTE_SIZE_KEY_SIZE - keySize; + int newKeyOffset = currentKeyOffset - bytesKeySize() - keySize; // Write key cursor.setOffset( newKeyOffset ); @@ -159,7 +108,7 @@ void insertKeyAndRightChildAt( PageCursor cursor, KEY key, long child, int pos, layout.writeKey( cursor, key ); // Update alloc space - setAllocSpace( cursor, newKeyOffset ); + setAllocOffset( cursor, newKeyOffset ); // Write to offset array insertSlotsAt( cursor, pos, 1, keyCount, keyPosOffsetInternal( 0 ), keyChildSize() ); @@ -172,10 +121,10 @@ void insertKeyAndRightChildAt( PageCursor cursor, KEY key, long child, int pos, void insertKeyValueAt( PageCursor cursor, KEY key, VALUE value, int pos, int keyCount ) { // Where to write key? - int currentKeyValueOffset = getAllocSpace( cursor ); + int currentKeyValueOffset = getAllocOffset( cursor ); int keySize = layout.keySize( key ); int valueSize = layout.valueSize( value ); - int newKeyValueOffset = currentKeyValueOffset - BYTE_SIZE_KEY_SIZE - BYTE_SIZE_VALUE_SIZE - keySize - valueSize; + int newKeyValueOffset = currentKeyValueOffset - bytesKeySize() - bytesValueSize() - keySize - valueSize; // Write key and value cursor.setOffset( newKeyValueOffset ); @@ -185,10 +134,10 @@ void insertKeyValueAt( PageCursor cursor, KEY key, VALUE value, int pos, int key layout.writeValue( cursor, value ); // Update alloc space - setAllocSpace( cursor, newKeyValueOffset ); + setAllocOffset( cursor, newKeyValueOffset ); // Write to offset array - insertSlotsAt( cursor, pos, 1, keyCount, keyPosOffsetLeaf( 0 ), keySize() ); + insertSlotsAt( cursor, pos, 1, keyCount, keyPosOffsetLeaf( 0 ), bytesKeyOffset() ); cursor.setOffset( keyPosOffsetLeaf( pos ) ); putKeyOffset( cursor, newKeyValueOffset ); } @@ -198,10 +147,18 @@ void removeKeyValueAt( PageCursor cursor, int pos, int keyCount ) { // Kill actual key placeCursorAtActualKey( cursor, pos, LEAF ); + int keyOffset = cursor.getOffset(); + int keySize = readKeySize( cursor ); + int valueSize = readValueSize( cursor ); + cursor.setOffset( keyOffset ); putTombstone( cursor ); + // Update dead space + int deadSpace = getDeadSpace( cursor ); + setDeadSpace( cursor, deadSpace + keySize + valueSize + bytesKeySize() ); + // Remove from offset array - removeSlotAt( cursor, pos, keyCount, keyPosOffsetLeaf( 0 ), keySize() ); + removeSlotAt( cursor, pos, keyCount, keyPosOffsetLeaf( 0 ), bytesKeyOffset() ); } @Override @@ -322,20 +279,126 @@ boolean internalOverflow( int currentKeyCount ) } @Override - boolean leafOverflow( PageCursor cursor, int currentKeyCount, KEY newKey, VALUE newValue ) + Overflow leafOverflow( PageCursor cursor, int currentKeyCount, KEY newKey, VALUE newValue ) { // How much space do we have? - int allocSpace = getAllocSpace( cursor ); + int allocOffset = getAllocOffset( cursor ); + int deadSpace = getDeadSpace( cursor ); int endOfOffsetArray = keyPosOffsetLeaf( currentKeyCount ); - int freeSpace = allocSpace - endOfOffsetArray; + int allocSpace = allocOffset - endOfOffsetArray; // How much space do we need? int keySize = layout.keySize( newKey ); int valueSize = layout.valueSize( newValue ); - int totalOverhead = keySize() + BYTE_SIZE_KEY_SIZE + BYTE_SIZE_VALUE_SIZE; + int totalOverhead = bytesKeyOffset() + bytesKeySize() + bytesValueSize(); + int neededSpace = keySize + valueSize + totalOverhead; // There is your answer! - return freeSpace < keySize + valueSize + totalOverhead; + return neededSpace < allocSpace ? Overflow.NO : + neededSpace < allocSpace + deadSpace ? Overflow.NEED_DEFRAG : Overflow.YES; + } + + @Override + void defragmentLeaf( PageCursor cursor ) + { + // Mark all offsets + PrimitiveIntStack deadKeysOffset = new PrimitiveIntStack(); + PrimitiveIntStack aliveKeysOffset = new PrimitiveIntStack(); + recordDeadAndAlive( cursor, deadKeysOffset, aliveKeysOffset ); + + /* + BEFORE MOVE + v aliveRangeOffset + [X][_][_][X][_][X][_][_] + ^ ^ deadRangeOffset + |_____________ moveRangeOffset + + AFTER MOVE + v aliveRangeOffset + [X][_][_][X][X][_][_][_] + ^ deadRangeOffset + + */ + int maxKeyCount = pageSize / (bytesKeySize() + bytesKeyOffset() + bytesValueSize()); + int[] oldOffset = new int[maxKeyCount]; + int[] newOffset = new int[maxKeyCount]; + int oldOffsetCursor = 0; + int newOffsetCursor = 0; + int aliveRangeOffset = pageSize; // Everything after this point is alive + int deadRangeOffset; // Everything between this point and aliveRangeOffset is dead space + + // Rightmost alive keys does not need to move + while ( deadKeysOffset.peek() < aliveKeysOffset.peek() ) + { + aliveRangeOffset = aliveKeysOffset.poll(); + } + + do + { + // Locate next range of dead keys + deadRangeOffset = aliveRangeOffset; + while ( aliveKeysOffset.peek() < deadKeysOffset.peek() ) + { + deadRangeOffset = deadKeysOffset.poll(); + } + + // Locate next range of alive keys + int moveOffset = deadRangeOffset; + while ( deadKeysOffset.peek() < aliveKeysOffset.peek() ) + { + int moveKey = aliveKeysOffset.poll(); + oldOffset[oldOffsetCursor++] = moveKey; + moveOffset = moveKey; + } + + // Update offset mapping + int deadRangeSize = aliveRangeOffset - deadRangeOffset; + while ( oldOffsetCursor > newOffsetCursor ) + { + newOffset[newOffsetCursor] = oldOffset[newOffsetCursor] + deadRangeSize; + newOffsetCursor++; + } + + // Do move + while ( moveOffset < (deadRangeOffset - deadRangeSize) ) + { + // Move one block + deadRangeOffset -= deadRangeSize; + aliveRangeOffset -= deadRangeSize; + cursor.copyTo( deadRangeOffset, cursor, aliveRangeOffset, deadRangeSize ); + } + // Move the last piece + int lastBlockSize = deadRangeOffset - moveOffset; + deadRangeOffset -= lastBlockSize; + aliveRangeOffset -= lastBlockSize; + cursor.copyTo( deadRangeOffset, cursor, aliveRangeOffset, lastBlockSize ); + } + while ( !aliveKeysOffset.isEmpty() ); + // Update allocOffset + setAllocOffset( cursor, aliveRangeOffset ); + + // Update offset array + int keyCount = keyCount( cursor ); + keyPos: + for ( int pos = 0; pos < keyCount; pos++ ) + { + int keyPosOffset = keyPosOffsetLeaf( pos ); + cursor.setOffset( keyPosOffset ); + int keyOffset = readKeyOffset( cursor ); + for ( int index = 0; index < oldOffsetCursor; index++ ) + { + if ( keyOffset == oldOffset[index] ) + { + // Overwrite with new offset + cursor.setOffset( keyPosOffset ); + putKeyOffset( cursor, newOffset[index] ); + continue keyPos; + } + } + } + + // Update dead space + setDeadSpace( cursor, 0 ); } @Override @@ -357,10 +420,159 @@ boolean canMergeLeaves( int leftKeyCount, int rightKeyCount ) } @Override - void doSplitLeaf( PageCursor leftCursor, int leftKeyCount, PageCursor rightCursor, int rightKeyCount, int insertPos, KEY newKey, - VALUE newValue, int middlePos ) + void doSplitLeaf( PageCursor leftCursor, int leftKeyCount, PageCursor rightCursor, int insertPos, KEY newKey, + VALUE newValue, StructurePropagation structurePropagation ) { - throw new UnsupportedOperationException( "Implement me" ); + // Find middle + int middlePos = middle( leftCursor, insertPos, newKey, newValue ); + int keyCountAfterInsert = leftKeyCount + 1; + + if ( middlePos == insertPos ) + { + layout.copyKey( newKey, structurePropagation.rightKey ); + } + else + { + keyAt( leftCursor, structurePropagation.rightKey, insertPos < middlePos ? middlePos - 1 : middlePos, LEAF ); + } + int rightKeyCount = keyCountAfterInsert - middlePos; + + if ( insertPos < middlePos ) + { + // v-------v copy + // before _,_,_,_,_,_,_,_,_,_ + // insert _,_,_,X,_,_,_,_,_,_,_ + // middle ^ + moveKeysAndValues( leftCursor, middlePos - 1, rightCursor, 0, rightKeyCount ); + + defragmentLeaf( leftCursor ); + insertKeyValueAt( leftCursor, newKey, newValue, insertPos, middlePos - 1 ); + } + else + { + // v---v first copy + // v-v second copy + // before _,_,_,_,_,_,_,_,_,_ + // insert _,_,_,_,_,_,_,_,X,_,_ + // middle ^ + + // Copy everything in one go + int newInsertPos = insertPos - middlePos; + int keysToMove = leftKeyCount - middlePos; + moveKeysAndValues( leftCursor, middlePos, rightCursor, 0, keysToMove ); + + // Insert new key and value + insertKeyValueAt( rightCursor, newKey, newValue, newInsertPos, keysToMove ); + } + TreeNode.setKeyCount( leftCursor, middlePos ); + TreeNode.setKeyCount( rightCursor, rightKeyCount ); + } + + private void recordDeadAndAlive( PageCursor cursor, PrimitiveIntStack deadKeysOffset, PrimitiveIntStack aliveKeysOffset ) + { + int currentOffset = getAllocOffset( cursor ); + while ( currentOffset < pageSize ) + { + cursor.setOffset( currentOffset ); + int keySize = readKeySize( cursor ); + int valueSize = readValueSize( cursor ); + boolean dead = hasTombstone( keySize ); + keySize = stripTombstone( keySize ); + + if ( dead ) + { + deadKeysOffset.push( currentOffset ); + } + else + { + aliveKeysOffset.push( currentOffset ); + } + currentOffset += keySize + valueSize + bytesKeySize() + bytesValueSize(); + } + } + + private void moveKeysAndValues( PageCursor fromCursor, int fromPos, PageCursor toCursor, int toPos, int count ) + { + int rightAllocOffset = getAllocOffset( toCursor ); + for ( int i = 0; i < count; i++, toPos++ ) + { + rightAllocOffset = transferRawKeyValue( fromCursor, fromPos + i, toCursor, rightAllocOffset ); + toCursor.setOffset( keyPosOffsetLeaf( toPos ) ); + putKeyOffset( toCursor, rightAllocOffset ); + } + setAllocOffset( toCursor, rightAllocOffset ); + } + + /** + * Transfer key and value from logical position in 'from' to physical position next to current alloc offset in 'to'. + * Mark transferred key as dead. + * @return new alloc offset in 'to' + */ + private int transferRawKeyValue( PageCursor fromCursor, int fromPos, PageCursor toCursor, int rightAllocOffset ) + { + // What to copy? + placeCursorAtActualKey( fromCursor, fromPos, LEAF ); + int fromKeyOffset = fromCursor.getOffset(); + int keySize = readKeySize( fromCursor ); + int valueSize = readValueSize( fromCursor ); + + // Copy + int toCopy = bytesKeySize() + bytesValueSize() + keySize + valueSize; + int newRightAllocSpace = rightAllocOffset - toCopy; + fromCursor.copyTo( fromKeyOffset, toCursor, newRightAllocSpace, toCopy ); + + // Put tombstone + fromCursor.setOffset( fromKeyOffset ); + putTombstone( fromCursor ); + return newRightAllocSpace; + } + + private int middle( PageCursor leftCursor, int insertPos, KEY newKey, VALUE newValue ) + { + int halfSpace = (pageSize - HEADER_LENGTH_DYNAMIC) / 2; + int middle = 0; + int currentPos = 0; + int middleSpace = 0; + int currentDelta = halfSpace; + int prevDelta; + boolean includedNew = false; + + do + { + // We may come closer to split by keeping one more in left + middle++; + currentPos++; + int space; + if ( currentPos == insertPos & !includedNew ) + { + space = totalSpaceOfKeyValue( newKey, newValue ); + includedNew = true; + currentPos--; + } + else + { + space = totalSpaceOfKeyValue( leftCursor, currentPos ); + } + middleSpace += space; + prevDelta = currentDelta; + currentDelta = Math.abs( middleSpace - halfSpace ); + } + while ( currentDelta < prevDelta ); + middle--; // Step back to the pos that most equally divide the available space in two + return middle; + } + + private int totalSpaceOfKeyValue( KEY key, VALUE value ) + { + return bytesKeyOffset() + bytesKeySize() + bytesValueSize() + layout.keySize( key ) + layout.valueSize( value ); + } + + private int totalSpaceOfKeyValue( PageCursor cursor, int pos ) + { + placeCursorAtActualKey( cursor, pos, LEAF ); + int keySize = readKeySize( cursor ); + int valueSize = readValueSize( cursor ); + return bytesKeyOffset() + bytesKeySize() + bytesValueSize() + keySize + valueSize; } @Override @@ -377,9 +589,77 @@ void moveKeyValuesFromLeftToRight( PageCursor leftCursor, int leftKeyCount, Page throw new UnsupportedOperationException( "Implement me" ); } + private void setAllocOffset( PageCursor cursor, int allocSpace ) + { + cursor.setOffset( BYTE_POS_ALLOCOFFSET ); + putKeyOffset( cursor, allocSpace ); + } + + int getAllocOffset( PageCursor cursor ) + { + cursor.setOffset( BYTE_POS_ALLOCOFFSET ); + return readKeyOffset( cursor ); + } + + private void setDeadSpace( PageCursor cursor, int deadSpace ) + { + cursor.setOffset( BYTE_POS_DEADSPACE ); + putKeySize( cursor, deadSpace ); + } + + private int getDeadSpace( PageCursor cursor ) + { + cursor.setOffset( BYTE_POS_DEADSPACE ); + int deadSpace = readKeySize( cursor ); + assert !hasTombstone( deadSpace ) : "Did not expect tombstone in dead space"; + return deadSpace; + } + + private void placeCursorAtActualKey( PageCursor cursor, int pos, Type type ) + { + // Set cursor to correct place in offset array + int keyPosOffset = keyPosOffset( pos, type ); + cursor.setOffset( keyPosOffset ); + + // Read actual offset to key + int keyOffset = readKeyOffset( cursor ); + + // Verify offset is reasonable + if ( keyOffset > pageSize ) + { + cursor.setCursorException( "Tried to read key on offset " + keyOffset + ". Page size is " + pageSize ); + } + + // Set cursor to actual offset + cursor.setOffset( keyOffset ); + } + + private int keyPosOffset( int pos, Type type ) + { + if ( type == LEAF ) + { + return keyPosOffsetLeaf( pos ); + } + else + { + return keyPosOffsetInternal( pos ); + } + } + + private int keyPosOffsetLeaf( int pos ) + { + return HEADER_LENGTH_DYNAMIC + pos * bytesKeyOffset(); + } + + private int keyPosOffsetInternal( int pos ) + { + // header + childPointer + pos * (keyPosOffsetSize + childPointer) + return HEADER_LENGTH_DYNAMIC + childSize() + pos * keyChildSize(); + } + private int keyChildSize() { - return BYTE_SIZE_OFFSET + SIZE_PAGE_REFERENCE; + return bytesKeyOffset() + SIZE_PAGE_REFERENCE; } private int childSize() @@ -387,7 +667,22 @@ private int childSize() return SIZE_PAGE_REFERENCE; } - private int keySize() + private static int bytesKeySize() + { + return BYTE_SIZE_KEY_SIZE; + } + + private static int bytesValueSize() + { + return BYTE_SIZE_VALUE_SIZE; + } + + private static int bytesKeyOffset() + { + return BYTE_SIZE_OFFSET; + } + + private static int bytesPageOffset() { return BYTE_SIZE_OFFSET; } @@ -399,8 +694,9 @@ public String toString() } @SuppressWarnings( "unused" ) - void printNode( PageCursor cursor, long stableGeneration, long unstableGeneration ) + void printNode( PageCursor cursor, boolean includeValue, long stableGeneration, long unstableGeneration ) { + int currentOffset = cursor.getOffset(); // [header] <- dont care // LEAF: [allocSpace=][child0,key0*,child1,...][keySize|key][keySize|key] // INTERNAL: [allocSpace=][key0*,key1*,...][offset|keySize|valueSize|key][keySize|valueSize|key] @@ -408,7 +704,7 @@ void printNode( PageCursor cursor, long stableGeneration, long unstableGeneratio Type type = isInternal( cursor ) ? INTERNAL : LEAF; // HEADER - int allocSpace = getAllocSpace( cursor ); + int allocSpace = getAllocOffset( cursor ); String additionalHeader = "[allocSpace=" + allocSpace + "]"; // OFFSET ARRAY @@ -429,18 +725,27 @@ void printNode( PageCursor cursor, long stableGeneration, long unstableGeneratio { valueSize = readValueSize( cursor ); } + if ( hasTombstone( keySize ) ) + { + singleKey.add( "X" ); + keySize = DynamicSizeUtil.stripTombstone( keySize ); + } + else + { + singleKey.add( "_" ); + } layout.readKey( cursor, readKey, keySize ); if ( type == LEAF ) { layout.readValue( cursor, readValue, valueSize ); } singleKey.add( Integer.toString( keySize ) ); - if ( type == LEAF ) + if ( type == LEAF && includeValue ) { singleKey.add( Integer.toString( valueSize ) ); } singleKey.add( readKey.toString() ); - if ( type == LEAF ) + if ( type == LEAF && includeValue ) { singleKey.add( readValue.toString() ); } @@ -448,6 +753,7 @@ void printNode( PageCursor cursor, long stableGeneration, long unstableGeneratio } System.out.println( additionalHeader + offsetArray + keys ); + cursor.setOffset( currentOffset ); } private String readOffsetArray( PageCursor cursor, long stableGeneration, long unstableGeneration, Type type ) diff --git a/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeFixedSize.java b/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeFixedSize.java index 5ef11417b87b7..eb722c7b5679b 100644 --- a/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeFixedSize.java +++ b/community/index/src/main/java/org/neo4j/index/internal/gbptree/TreeNodeFixedSize.java @@ -24,6 +24,7 @@ import static org.neo4j.index.internal.gbptree.GenerationSafePointerPair.read; import static org.neo4j.index.internal.gbptree.Layout.FIXED_SIZE_KEY; import static org.neo4j.index.internal.gbptree.Layout.FIXED_SIZE_VALUE; +import static org.neo4j.index.internal.gbptree.TreeNode.Type.LEAF; class TreeNodeFixedSize extends TreeNode { @@ -261,9 +262,14 @@ boolean internalOverflow( int currentKeyCount ) } @Override - boolean leafOverflow( PageCursor cursor, int currentKeyCount, KEY newKey, VALUE newValue ) + Overflow leafOverflow( PageCursor cursor, int currentKeyCount, KEY newKey, VALUE newValue ) { - return currentKeyCount + 1 > leafMaxKeyCount(); + return currentKeyCount + 1 > leafMaxKeyCount() ? Overflow.YES : Overflow.NO; + } + + @Override + void defragmentLeaf( PageCursor cursor ) + { // no-op } @Override @@ -285,9 +291,22 @@ boolean canMergeLeaves( int leftKeyCount, int rightKeyCount ) } @Override - void doSplitLeaf( PageCursor leftCursor, int leftKeyCount, PageCursor rightCursor, int rightKeyCount, int insertPos, KEY newKey, - VALUE newValue, int middlePos ) + void doSplitLeaf( PageCursor leftCursor, int leftKeyCount, PageCursor rightCursor, int insertPos, KEY newKey, + VALUE newValue, StructurePropagation structurePropagation ) { + int keyCountAfterInsert = leftKeyCount + 1; + int middlePos = middle( keyCountAfterInsert ); + + if ( middlePos == insertPos ) + { + layout.copyKey( newKey, structurePropagation.rightKey ); + } + else + { + keyAt( leftCursor, structurePropagation.rightKey, insertPos < middlePos ? middlePos - 1 : middlePos, LEAF ); + } + int rightKeyCount = keyCountAfterInsert - middlePos; + if ( insertPos < middlePos ) { // v-------v copy @@ -322,6 +341,11 @@ void doSplitLeaf( PageCursor leftCursor, int leftKeyCount, PageCursor rightCurso TreeNode.setKeyCount( rightCursor, rightKeyCount ); } + private static int middle( int keyCountAfterInsert ) + { + return keyCountAfterInsert / 2; + } + @Override void doSplitInternal( PageCursor leftCursor, int leftKeyCount, PageCursor rightCursor, int rightKeyCount, int insertPos, KEY newKey, long newRightChild, int middlePos, long stableGeneration, long unstableGeneration ) diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/InternalTreeLogicDynamicSizeTest.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/InternalTreeLogicDynamicSizeTest.java new file mode 100644 index 0000000000000..f0edcb62daa39 --- /dev/null +++ b/community/index/src/test/java/org/neo4j/index/internal/gbptree/InternalTreeLogicDynamicSizeTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.index.internal.gbptree; + +public class InternalTreeLogicDynamicSizeTest extends InternalTreeLogicTestBase +{ + private SimpleByteArrayLayout layout = new SimpleByteArrayLayout(); + + @Override + protected ValueMerger getAdder() + { + return ( existingKey, newKey, base, add ) -> + { + long baseSeed = layout.getSeed( base ); + long addSeed = layout.getSeed( add ); + return layout.value( baseSeed + addSeed ); + }; + } + + @Override + protected TreeNode getTreeNode( int pageSize, Layout layout ) + { + return new TreeNodeDynamicSize<>( pageSize, layout ); + } + + @Override + protected TestLayout getLayout() + { + return layout; + } +} diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/InternalTreeLogicTestBase.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/InternalTreeLogicTestBase.java index 2bf2ffe07c220..7b6db7cda4aec 100644 --- a/community/index/src/test/java/org/neo4j/index/internal/gbptree/InternalTreeLogicTestBase.java +++ b/community/index/src/test/java/org/neo4j/index/internal/gbptree/InternalTreeLogicTestBase.java @@ -46,6 +46,7 @@ import static org.junit.Assume.assumeTrue; import static org.neo4j.index.internal.gbptree.ConsistencyChecker.assertNoCrashOrBrokenPointerInGSPP; import static org.neo4j.index.internal.gbptree.GenerationSafePointerPair.pointer; +import static org.neo4j.index.internal.gbptree.TreeNode.Overflow.NO; import static org.neo4j.index.internal.gbptree.TreeNode.Type.INTERNAL; import static org.neo4j.index.internal.gbptree.TreeNode.Type.LEAF; import static org.neo4j.index.internal.gbptree.ValueMergers.overwrite; @@ -150,7 +151,7 @@ public void modifierMustSortCorrectlyOnInsertFirstInLeaf() throws Exception int keyCount = 0; KEY newKey = key( someHighSeed ); VALUE newValue = value( someHighSeed ); - while ( !node.leafOverflow( cursor, keyCount, newKey, newValue ) ) + while ( node.leafOverflow( cursor, keyCount, newKey, newValue ) == NO ) { insert( newKey, newValue ); @@ -174,7 +175,7 @@ public void modifierMustSortCorrectlyOnInsertLastInLeaf() throws Exception int keyCount = 0; KEY key = key( keyCount ); VALUE value = value( keyCount ); - while ( !node.leafOverflow( cursor, keyCount, key, value ) ) + while ( node.leafOverflow( cursor, keyCount, key, value ) == NO ) { // when insert( key, value ); @@ -201,7 +202,7 @@ public void modifierMustSortCorrectlyOnInsertInMiddleOfLeaf() throws Exception long middleValue = keyCount % 2 == 0 ? keyCount / 2 : someHighSeed - keyCount / 2; KEY key = key( middleValue ); VALUE value = value( middleValue ); - while ( !node.leafOverflow( cursor, keyCount, key, value ) ) + while ( node.leafOverflow( cursor, keyCount, key, value ) == NO ) { insert( key, value ); @@ -226,7 +227,7 @@ public void modifierMustSplitWhenInsertingMiddleOfFullLeaf() throws Exception int middle = keyCount % 2 == 0 ? keyCount : someMiddleSeed * 2 - keyCount; KEY key = key( middle ); VALUE value = value( middle ); - while ( !node.leafOverflow( cursor, keyCount, key, value ) ) + while ( node.leafOverflow( cursor, keyCount, key, value ) == NO ) { insert( key, value ); @@ -253,7 +254,7 @@ public void modifierMustSplitWhenInsertingLastInFullLeaf() throws Exception int keyCount = 0; KEY key = key( keyCount ); VALUE value = value( keyCount ); - while ( !node.leafOverflow( cursor, keyCount, key, value ) ) + while ( node.leafOverflow( cursor, keyCount, key, value ) == NO ) { insert( key, value ); assertFalse( structurePropagation.hasRightKeyInsert ); @@ -279,7 +280,7 @@ public void modifierMustSplitWhenInsertingFirstInFullLeaf() throws Exception int keyCount = 0; KEY key = key( keyCount + 1 ); VALUE value = value( keyCount + 1 ); - while ( !node.leafOverflow( cursor, keyCount, key, value ) ) + while ( node.leafOverflow( cursor, keyCount, key, value ) == NO ) { insert( key, value ); assertFalse( structurePropagation.hasRightKeyInsert ); @@ -306,7 +307,7 @@ public void modifierMustUpdatePointersInSiblingsToSplit() throws Exception int keyCount = 0; KEY key = key( someLargeSeed - keyCount ); VALUE value = value( someLargeSeed - keyCount ); - while ( !node.leafOverflow( cursor, keyCount, key, value ) ) + while ( node.leafOverflow( cursor, keyCount, key, value ) == NO ) { insert( key, value ); @@ -381,7 +382,7 @@ public void modifierMustRemoveFirstInFullLeaf() throws Exception int maxKeyCount = 0; KEY key = key( maxKeyCount ); VALUE value = value( maxKeyCount ); - while ( !node.leafOverflow( cursor, maxKeyCount, key, value ) ) + while ( node.leafOverflow( cursor, maxKeyCount, key, value ) == NO ) { insert( key, value ); @@ -413,7 +414,7 @@ public void modifierMustRemoveInMiddleInFullLeaf() throws Exception int maxKeyCount = 0; KEY key = key( maxKeyCount ); VALUE value = value( maxKeyCount ); - while ( !node.leafOverflow( cursor, maxKeyCount, key, value ) ) + while ( node.leafOverflow( cursor, maxKeyCount, key, value ) == NO ) { insert( key, value ); @@ -447,7 +448,7 @@ public void modifierMustRemoveLastInFullLeaf() throws Exception int maxKeyCount = 0; KEY key = key( maxKeyCount ); VALUE value = value( maxKeyCount ); - while ( !node.leafOverflow( cursor, maxKeyCount, key, value ) ) + while ( node.leafOverflow( cursor, maxKeyCount, key, value ) == NO ) { insert( key, value ); @@ -537,7 +538,7 @@ public void modifierMustNotRemoveWhenKeyDoesNotExist() throws Exception int maxKeyCount = 0; KEY key = key( maxKeyCount ); VALUE value = value( maxKeyCount ); - while ( !node.leafOverflow( cursor, maxKeyCount, key, value ) ) + while ( node.leafOverflow( cursor, maxKeyCount, key, value ) == NO ) { insert( key, value ); @@ -1228,7 +1229,7 @@ public void shouldCreateNewVersionWhenInsertInStableRootAsInternal() throws Exce int keyCount = 0; KEY key = key( keyCount ); VALUE value = value( keyCount ); - while ( !node.leafOverflow( cursor, keyCount, key, value ) ) + while ( node.leafOverflow( cursor, keyCount, key, value ) == NO ) { insert( key, value ); keyCount++; @@ -1247,7 +1248,7 @@ public void shouldCreateNewVersionWhenInsertInStableRootAsInternal() throws Exce long rightChild = childAt( readCursor, 1, stableGeneration, unstableGeneration ); goTo( readCursor, rightChild ); int rightChildKeyCount = TreeNode.keyCount( readCursor ); - while ( !node.leafOverflow( readCursor, rightChildKeyCount, key, value ) ) + while ( node.leafOverflow( readCursor, rightChildKeyCount, key, value ) == NO ) { insert( key, value ); keyCount++; diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/KeySearchTest.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/KeySearchTest.java index 5fbef4fff5249..aace9f8c062fb 100644 --- a/community/index/src/test/java/org/neo4j/index/internal/gbptree/KeySearchTest.java +++ b/community/index/src/test/java/org/neo4j/index/internal/gbptree/KeySearchTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.assertTrue; import static org.neo4j.index.internal.gbptree.GBPTreeTestUtil.contains; import static org.neo4j.index.internal.gbptree.KeySearch.search; +import static org.neo4j.index.internal.gbptree.TreeNode.Overflow.NO; import static org.neo4j.index.internal.gbptree.TreeNode.Type.INTERNAL; import static org.neo4j.index.internal.gbptree.TreeNode.Type.LEAF; import static org.neo4j.io.pagecache.ByteArrayPageCursor.wrap; @@ -463,7 +464,7 @@ public void shouldSearchAndFindOnRandomData() throws Exception { MutableLong expectedKey = layout.newKey(); key.setValue( currentKey ); - if ( node.leafOverflow( cursor, keyCount, key, dummyValue ) ) + if ( node.leafOverflow( cursor, keyCount, key, dummyValue ) != NO ) { break; } diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/PageAwareByteArrayCursor.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/PageAwareByteArrayCursor.java index 88bd1cb674500..07f984c9921cd 100644 --- a/community/index/src/test/java/org/neo4j/index/internal/gbptree/PageAwareByteArrayCursor.java +++ b/community/index/src/test/java/org/neo4j/index/internal/gbptree/PageAwareByteArrayCursor.java @@ -124,7 +124,7 @@ public boolean next( long pageId ) throws IOException @Override public int copyTo( int sourceOffset, PageCursor targetCursor, int targetOffset, int lengthInBytes ) { - if ( sourceOffset < 0 || targetOffset < 0 || lengthInBytes < 1 ) + if ( sourceOffset < 0 || targetOffset < 0 || lengthInBytes < 0 ) { throw new IllegalArgumentException(); } diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSizeTest.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSizeTest.java index f42a9f8b6f7b0..59cbbfba15161 100644 --- a/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSizeTest.java +++ b/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeDynamicSizeTest.java @@ -43,7 +43,7 @@ protected TreeNode getNode( int pageSize, Layout node, int pageSize ) { // When - int currentAllocSpace = ((TreeNodeDynamicSize) node).getAllocSpace( cursor ); + int currentAllocSpace = ((TreeNodeDynamicSize) node).getAllocOffset( cursor ); // Then assertEquals("allocSpace point to end of page", pageSize, currentAllocSpace ); diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeTestBase.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeTestBase.java index 4398e9ce78549..e9aa34855bbc8 100644 --- a/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeTestBase.java +++ b/community/index/src/test/java/org/neo4j/index/internal/gbptree/TreeNodeTestBase.java @@ -38,6 +38,8 @@ import static org.neo4j.index.internal.gbptree.GenerationSafePointerPair.pointer; import static org.neo4j.index.internal.gbptree.GenerationSafePointerPair.resultIsFromSlotA; import static org.neo4j.index.internal.gbptree.TreeNode.NO_NODE_FLAG; +import static org.neo4j.index.internal.gbptree.TreeNode.Overflow.NEED_DEFRAG; +import static org.neo4j.index.internal.gbptree.TreeNode.Overflow.YES; import static org.neo4j.index.internal.gbptree.TreeNode.Type.INTERNAL; import static org.neo4j.index.internal.gbptree.TreeNode.Type.LEAF; @@ -73,7 +75,7 @@ public void prepareCursor() throws IOException private KEY key( long seed ) { return layout.key( seed ); - }; + } private VALUE value( long seed ) { @@ -371,6 +373,146 @@ public void shouldSetAndGetSuccessor() throws Exception assertEquals( 123, successor( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ) ); } + @Test + public void shouldDefragLeafWithTombstoneOnLast() throws Exception + { + // GIVEN + node.initializeLeaf( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ); + KEY key = key( 1 ); + VALUE value = value( 1 ); + node.insertKeyValueAt( cursor, key, value, 0, 0 ); + key = key( 2 ); + value = value( 2 ); + node.insertKeyValueAt( cursor, key, value, 1, 1 ); + + // AND + node.removeKeyValueAt( cursor, 1, 2 ); + + // WHEN + node.defragmentLeaf( cursor ); + + // THEN + assertKeyEquals( key( 1 ), node.keyAt( cursor, layout.newKey(), 0, LEAF ) ); + } + + @Test + public void shouldDefragLeafWithTombstoneOnFirst() throws Exception + { + // GIVEN + node.initializeLeaf( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ); + KEY key = key( 1 ); + VALUE value = value( 1 ); + node.insertKeyValueAt( cursor, key, value, 0, 0 ); + key = key( 2 ); + value = value( 2 ); + node.insertKeyValueAt( cursor, key, value, 1, 1 ); + + // AND + node.removeKeyValueAt( cursor, 0, 2 ); + + // WHEN + node.defragmentLeaf( cursor ); + + // THEN + assertKeyEquals( key( 2 ), node.keyAt( cursor, layout.newKey(), 0, LEAF ) ); + } + + @Test + public void shouldDefragLeafWithTombstoneInterleaved() throws Exception + { + // GIVEN + node.initializeLeaf( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ); + KEY key = key( 1 ); + VALUE value = value( 1 ); + node.insertKeyValueAt( cursor, key, value, 0, 0 ); + key = key( 2 ); + value = value( 2 ); + node.insertKeyValueAt( cursor, key, value, 1, 1 ); + key = key( 3 ); + value = value( 3 ); + node.insertKeyValueAt( cursor, key, value, 2, 2 ); + + // AND + node.removeKeyValueAt( cursor, 1, 3 ); + + // WHEN + node.defragmentLeaf( cursor ); + + // THEN + assertKeyEquals( key( 1 ), node.keyAt( cursor, layout.newKey(), 0, LEAF ) ); + assertKeyEquals( key( 3 ), node.keyAt( cursor, layout.newKey(), 1, LEAF ) ); + } + + @Test + public void shouldDefragLeafWithMultipleTombstonesInterleavedOdd() throws Exception + { + // GIVEN + node.initializeLeaf( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ); + KEY key = key( 1 ); + VALUE value = value( 1 ); + node.insertKeyValueAt( cursor, key, value, 0, 0 ); + key = key( 2 ); + value = value( 2 ); + node.insertKeyValueAt( cursor, key, value, 1, 1 ); + key = key( 3 ); + value = value( 3 ); + node.insertKeyValueAt( cursor, key, value, 2, 2 ); + key = key( 4 ); + value = value( 4 ); + node.insertKeyValueAt( cursor, key, value, 3, 3 ); + key = key( 5 ); + value = value( 5 ); + node.insertKeyValueAt( cursor, key, value, 4, 4 ); + + // AND + node.removeKeyValueAt( cursor, 1, 5 ); + node.removeKeyValueAt( cursor, 2, 4 ); + TreeNode.setKeyCount( cursor, 3 ); + + // WHEN + node.defragmentLeaf( cursor ); + + // THEN + assertKeyEquals( key( 1 ), node.keyAt( cursor, layout.newKey(), 0, LEAF ) ); + assertKeyEquals( key( 3 ), node.keyAt( cursor, layout.newKey(), 1, LEAF ) ); + assertKeyEquals( key( 5 ), node.keyAt( cursor, layout.newKey(), 2, LEAF ) ); + } + + @Test + public void shouldDefragLeafWithMultipleTombstonesInterleavedEven() throws Exception + { + // GIVEN + node.initializeLeaf( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ); + KEY key = key( 1 ); + VALUE value = value( 1 ); + node.insertKeyValueAt( cursor, key, value, 0, 0 ); + key = key( 2 ); + value = value( 2 ); + node.insertKeyValueAt( cursor, key, value, 1, 1 ); + key = key( 3 ); + value = value( 3 ); + node.insertKeyValueAt( cursor, key, value, 2, 2 ); + key = key( 4 ); + value = value( 4 ); + node.insertKeyValueAt( cursor, key, value, 3, 3 ); + key = key( 5 ); + value = value( 5 ); + node.insertKeyValueAt( cursor, key, value, 4, 4 ); + + // AND + node.removeKeyValueAt( cursor, 0, 5 ); + node.removeKeyValueAt( cursor, 1, 4 ); + node.removeKeyValueAt( cursor, 2, 3 ); + TreeNode.setKeyCount( cursor, 2 ); + + // WHEN + node.defragmentLeaf( cursor ); + + // THEN + assertKeyEquals( key( 2 ), node.keyAt( cursor, layout.newKey(), 0, LEAF ) ); + assertKeyEquals( key( 4 ), node.keyAt( cursor, layout.newKey(), 1, LEAF ) ); + } + @Test public void shouldInsertAndRemoveRandomKeysAndValues() throws Exception { @@ -398,7 +540,12 @@ public void shouldInsertAndRemoveRandomKeysAndValues() throws Exception while ( contains( expectedKeys, newKey, layout ) ); VALUE newValue = value( random.nextLong() ); - if ( !node.leafOverflow( cursor, expectedKeyCount + 1, newKey, newValue ) ) + TreeNode.Overflow overflow = node.leafOverflow( cursor, expectedKeyCount, newKey, newValue ); + if ( overflow == NEED_DEFRAG ) + { + node.defragmentLeaf( cursor ); + } + if ( overflow != YES ) { // there's room int position = expectedKeyCount == 0 ? 0 : random.nextInt( expectedKeyCount ); // ensure unique @@ -419,7 +566,7 @@ public void shouldInsertAndRemoveRandomKeysAndValues() throws Exception node.removeKeyValueAt( cursor, position, expectedKeyCount ); KEY expectedKey = expectedKeys.remove( position ); VALUE expectedValue = expectedValues.remove( position ); - assertTrue( "Key differ with expected, key=" + readKey + ", expectedKey=" + expectedKey, + assertTrue( String.format( "Key differ with expected%n readKey=%s %nexpectedKey=%s%n", readKey, expectedKey ), layout.compare( expectedKey, readKey ) == 0 ); assertTrue( "Value differ with expected, value=" + readValue + ", expectedValue=" + expectedValue, layout.compareValue( expectedValue, readValue ) == 0 ); diff --git a/community/primitive-collections/src/main/java/org/neo4j/collection/primitive/PrimitiveIntStack.java b/community/primitive-collections/src/main/java/org/neo4j/collection/primitive/PrimitiveIntStack.java index 7d7524ccaf87f..bdd52c82a3379 100644 --- a/community/primitive-collections/src/main/java/org/neo4j/collection/primitive/PrimitiveIntStack.java +++ b/community/primitive-collections/src/main/java/org/neo4j/collection/primitive/PrimitiveIntStack.java @@ -113,10 +113,18 @@ private void ensureCapacity() } /** - * @return the top most item, or -1 if stack is empty + * @return the top most item and remove it from stack, or -1 if stack is empty */ public int poll() { return cursor == -1 ? -1 : array[cursor--]; } + + /** + * @return the top most item, or -1 if stack is empty + */ + public int peek() + { + return cursor == -1 ? -1 : array[cursor]; + } }