From 4018fd7d6de05dd9bd0ce54b363d397c9a240b58 Mon Sep 17 00:00:00 2001 From: Anton Persson Date: Fri, 2 Dec 2016 12:34:46 +0100 Subject: [PATCH] Introduce Freelist All pages that can be reused are added to the freelist, like a linked list of pages. Even freelistpages themselves are added to the freelist when they become empty. The freelist is managed by the FreelistIdProvider New index state fields: freeListWritePageId freeListReadPageId freeListWritePos freeListReadPos --- .../index/gbptree/FreeListIdProvider.java | 220 ++++++++++++++++++ .../org/neo4j/index/gbptree/FreelistNode.java | 98 ++++++++ .../java/org/neo4j/index/gbptree/GBPTree.java | 67 +++--- .../neo4j/index/gbptree/GenSafePointer.java | 29 +-- .../org/neo4j/index/gbptree/IdProvider.java | 4 +- .../index/gbptree/InternalTreeLogic.java | 11 +- .../org/neo4j/index/gbptree/TreeNode.java | 25 +- .../org/neo4j/index/gbptree/TreeState.java | 134 +++++++++-- .../index/gbptree/FreeListIdProviderTest.java | 204 ++++++++++++++++ .../neo4j/index/gbptree/FreelistNodeTest.java | 162 +++++++++++++ .../org/neo4j/index/gbptree/GBPTreeTest.java | 120 +++++++++- .../index/gbptree/InternalTreeLogicTest.java | 4 +- .../index/gbptree/PointerCheckingTest.java | 3 +- .../neo4j/index/gbptree/SeekCursorTest.java | 4 +- .../neo4j/index/gbptree/SimpleIdProvider.java | 24 +- .../org/neo4j/index/gbptree/TreeNodeTest.java | 2 + .../index/gbptree/TreeStatePairTest.java | 6 +- .../neo4j/index/gbptree/TreeStateTest.java | 32 ++- 18 files changed, 1037 insertions(+), 112 deletions(-) create mode 100644 community/index/src/main/java/org/neo4j/index/gbptree/FreeListIdProvider.java create mode 100644 community/index/src/main/java/org/neo4j/index/gbptree/FreelistNode.java create mode 100644 community/index/src/test/java/org/neo4j/index/gbptree/FreeListIdProviderTest.java create mode 100644 community/index/src/test/java/org/neo4j/index/gbptree/FreelistNodeTest.java diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/FreeListIdProvider.java b/community/index/src/main/java/org/neo4j/index/gbptree/FreeListIdProvider.java new file mode 100644 index 0000000000000..3c74abd8c775f --- /dev/null +++ b/community/index/src/main/java/org/neo4j/index/gbptree/FreeListIdProvider.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2002-2016 "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.gbptree; + +import java.io.IOException; + +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; + +class FreeListIdProvider implements IdProvider +{ + private final byte[] zeroPage; + + private final PagedFile pagedFile; + + /** + * {@link FreelistNode} governs physical layout of a free-list. + */ + private final FreelistNode freelistNode; + + /** + * There's one free-list which both stable and unstable state (the state pages A/B) shares. + * Each free list page links to a potential next free-list page, by using the last entry containing + * page id to the next. + *

+ * Each entry in the the free list consist of a page id and the generation in which it was freed. + *

+ * Read pointer cannot go beyond entries belonging to stable generation. + * About the free-list id/offset variables below: + *

+     * Every cell in picture contains generation, page id is omitted for briefness.
+     * StableGen   = 1
+     * UnstableGen = 2
+     *
+     *        {@link #readPos}                         {@link #writePos}
+     *        v                               v
+     *  ┌───┬───┬───┬───┬───┬───┐   ┌───┬───┬───┬───┬───┬───┐
+     *  │ 1 │ 1 │ 1 │ 2 │ 2 │ 2 │-->│ 2 │ 2 │   │   │   │   │
+     *  └───┴───┴───┴───┴───┴───┘   └───┴───┴───┴───┴───┴───┘
+     *  ^                           ^
+     *  {@link #readPageId}                  {@link #writePageId}
+     * 
+ */ + private volatile long writePageId; + private volatile long readPageId; + private volatile int writePos; + private volatile int readPos; + + /** + * Last allocated page id, used for allocating new ids as more data gets inserted into the tree. + */ + private volatile long lastId; + + FreeListIdProvider( PagedFile pagedFile, int pageSize, long lastId ) + { + this.pagedFile = pagedFile; + this.freelistNode = new FreelistNode( pageSize ); + this.lastId = lastId; + this.zeroPage = new byte[pageSize]; + } + + void initialize( long lastId, long writePageId, long readPageId, int writePos, int readPos ) + { + this.lastId = lastId; + this.writePageId = writePageId; + this.readPageId = readPageId; + this.writePos = writePos; + this.readPos = readPos; + } + + @Override + public long acquireNewId( long stableGeneration, long unstableGeneration ) throws IOException + { + return acquireNewId( stableGeneration, unstableGeneration, true ); + } + + private long acquireNewId( long stableGeneration, long unstableGeneration, boolean allowTakeLastFromPage ) + throws IOException + { + // Acquire id from free-list or end of store file + long acquiredId = acquireNewIdFromFreelistOrEnd( stableGeneration, unstableGeneration, allowTakeLastFromPage ); + + // Zero the page + try ( PageCursor cursor = pagedFile.io( acquiredId, PagedFile.PF_SHARED_WRITE_LOCK ) ) + { + if ( !cursor.next() ) + { + throw new IllegalStateException( "Could not go to newly allocated page " + acquiredId ); + } + // TODO use cursor.clear() when available + cursor.putBytes( zeroPage ); + } + return acquiredId; + } + + private long acquireNewIdFromFreelistOrEnd( long stableGeneration, long unstableGeneration, + boolean allowTakeLastFromPage ) throws IOException + { + if ( (readPageId != writePageId || readPos < writePos) && + (allowTakeLastFromPage || readPos < freelistNode.maxEntries() - 1) ) + { + // It looks like read pos is < write pos so check if we can grab the next id + try ( PageCursor cursor = pagedFile.io( readPageId, PagedFile.PF_SHARED_READ_LOCK ) ) + { + if ( !cursor.next() ) + { + throw new IOException( "Couldn't go to free-list read page " + readPageId ); + } + + long resultPageId; + do + { + resultPageId = freelistNode.read( cursor, stableGeneration, readPos ); + } + while ( cursor.shouldRetry() ); + + if ( resultPageId != FreelistNode.NO_PAGE_ID ) + { + // FreelistNode compares generation and so this means that we have an available + // id in the free list which we can acquire from a stable generation + if ( ++readPos >= freelistNode.maxEntries() ) + { + readPos = 0; + do + { + readPageId = freelistNode.next( cursor ); + } while ( cursor.shouldRetry() ); + + // Put the exhausted free-list page id itself on the free-list + long exhaustedFreelistPageId = cursor.getCurrentPageId(); + releaseId( stableGeneration, unstableGeneration, exhaustedFreelistPageId ); + } + return resultPageId; + } + } + } + + // Fall-back to acquiring at the end of the file + return ++lastId; + } + + @Override + public void releaseId( long stableGeneration, long unstableGeneration, long id ) throws IOException + { + try ( PageCursor cursor = pagedFile.io( writePageId, PagedFile.PF_SHARED_WRITE_LOCK ) ) + { + if ( !cursor.next() ) + { + throw new IllegalStateException( "Couldn't go to free-list write page " + writePageId ); + } + freelistNode.write( cursor, unstableGeneration, id, writePos ); + writePos++; + } + + if ( writePos >= freelistNode.maxEntries() ) + { + // Current free-list write page is full, allocate a new one. + long nextFreelistPage = acquireNewId( stableGeneration, unstableGeneration, false ); + try ( PageCursor cursor = pagedFile.io( writePageId, PagedFile.PF_SHARED_WRITE_LOCK ) ) + { + if ( !cursor.next() ) + { + throw new IllegalStateException( "Couldn't go to free-list write page " + writePageId ); + } + freelistNode.initialize( cursor ); + freelistNode.setNext( cursor, nextFreelistPage ); + } + writePageId = nextFreelistPage; + writePos = 0; + } + } + + long lastId() + { + return lastId; + } + + long writePageId() + { + return writePageId; + } + + long readPageId() + { + return readPageId; + } + + int writePos() + { + return writePos; + } + + int readPos() + { + return readPos; + } + + // test-access method + int entriesPerPage() + { + return freelistNode.maxEntries(); + } +} diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/FreelistNode.java b/community/index/src/main/java/org/neo4j/index/gbptree/FreelistNode.java new file mode 100644 index 0000000000000..49374ffd89aa9 --- /dev/null +++ b/community/index/src/main/java/org/neo4j/index/gbptree/FreelistNode.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2002-2016 "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.gbptree; + +import org.neo4j.io.pagecache.PageCursor; + +import static org.neo4j.index.gbptree.PageCursorUtil.get6BLong; +import static org.neo4j.index.gbptree.PageCursorUtil.getUnsignedInt; +import static org.neo4j.index.gbptree.PageCursorUtil.put6BLong; + +class FreelistNode +{ + static final int PAGE_ID_SIZE = GenSafePointer.POINTER_SIZE; + static final int BYTE_POS_NEXT = TreeNode.BYTE_POS_NODE_TYPE + Byte.BYTES; + static final int HEADER_LENGTH = BYTE_POS_NEXT + PAGE_ID_SIZE; + static final int ENTRY_SIZE = GenSafePointer.GENERATION_SIZE + PAGE_ID_SIZE; + static final long NO_PAGE_ID = TreeNode.NO_NODE_FLAG; + + private final int maxEntries; + + FreelistNode( int pageSize ) + { + this.maxEntries = (pageSize - HEADER_LENGTH) / ENTRY_SIZE; + } + + void initialize( PageCursor cursor ) + { + cursor.putByte( TreeNode.BYTE_POS_NODE_TYPE, TreeNode.NODE_TYPE_FREE_LIST_NODE ); + } + + void write( PageCursor cursor, long unstableGeneration, long pageId, int pos ) + { + if ( pageId == NO_PAGE_ID ) + { + throw new IllegalArgumentException( "Tried to write pageId " + pageId + " which means null" ); + } + assertPos( pos ); + GenSafePointer.assertGenerationOnWrite( unstableGeneration ); + cursor.setOffset( HEADER_LENGTH + pos * ENTRY_SIZE ); + cursor.putInt( (int) unstableGeneration ); + put6BLong( cursor, pageId ); + } + + private void assertPos( int pos ) + { + if ( pos >= maxEntries ) + { + throw new IllegalArgumentException( "Pos " + pos + " too big, max entries " + maxEntries ); + } + if ( pos < 0 ) + { + throw new IllegalArgumentException( "Negative pos " + pos ); + } + } + + long read( PageCursor cursor, long stableGeneration, int pos ) + { + assertPos( pos ); + cursor.setOffset( HEADER_LENGTH + pos * ENTRY_SIZE ); + long generation = getUnsignedInt( cursor ); + long result = generation <= stableGeneration ? get6BLong( cursor ) : NO_PAGE_ID; + return result; + } + + int maxEntries() + { + return maxEntries; + } + + void setNext( PageCursor cursor, long nextFreelistPage ) + { + cursor.setOffset( BYTE_POS_NEXT ); + put6BLong( cursor, nextFreelistPage ); + } + + long next( PageCursor cursor ) + { + cursor.setOffset( BYTE_POS_NEXT ); + return get6BLong( cursor ); + } +} diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/GBPTree.java b/community/index/src/main/java/org/neo4j/index/gbptree/GBPTree.java index ee34e34be23a9..6bc964d82bf27 100644 --- a/community/index/src/main/java/org/neo4j/index/gbptree/GBPTree.java +++ b/community/index/src/main/java/org/neo4j/index/gbptree/GBPTree.java @@ -118,7 +118,7 @@ * @param type of keys * @param type of values */ -public class GBPTree implements Index, IdProvider +public class GBPTree implements Index { /** * For monitoring {@link GBPTree}. @@ -150,7 +150,6 @@ default void checkpointCompleted() * {@link File} to map in {@link PageCache} for storing this tree. */ private final File indexFile; - private final byte[] zeroPage; /** * User-provided layout of key/value as well as custom additional meta information. @@ -165,6 +164,12 @@ default void checkpointCompleted() */ private final TreeNode bTreeNode; + /** + * A free-list of released ids. Acquiring new ids involves first trying out the free-list and then, + * as a fall-back allocate a new id at the end of the store. + */ + private final FreeListIdProvider freeList; + /** * A single instance {@link IndexWriter} because tree only supports single writer. */ @@ -195,16 +200,6 @@ default void checkpointCompleted() */ private boolean created; - /** - * Current page id which contains the root of the tree. - */ - private volatile long rootId = IdSpace.MIN_TREE_NODE_ID; - - /** - * Last allocated page id, used for allocating new ids as more data gets inserted into the tree. - */ - private volatile long lastId = rootId; - /** * Generation of the tree. This variable contains both stable and unstable generation and is * represented as one long to get atomic updates of both stable and unstable generation for readers. @@ -218,6 +213,11 @@ default void checkpointCompleted() */ private volatile long generation; + /** + * Current page id which contains the root of the tree. + */ + private volatile long rootId = IdSpace.MIN_TREE_NODE_ID; + /** * Supplier of generation to readers. This supplier will actually very rarely be used, because normally * a {@link SeekCursor} is bootstrapped from {@link #generation}. The only time this supplier will be @@ -258,13 +258,17 @@ public GBPTree( PageCache pageCache, File indexFile, Layout layout, i this.layout = layout; this.pagedFile = openOrCreate( pageCache, indexFile, tentativePageSize, layout ); this.bTreeNode = new TreeNode<>( pageSize, layout ); - this.writer = new SingleIndexWriter( new InternalTreeLogic<>( this, bTreeNode, layout ) ); - this.zeroPage = new byte[pageSize]; + this.freeList = new FreeListIdProvider( pagedFile, pageSize, rootId ); + this.writer = new SingleIndexWriter( new InternalTreeLogic<>( freeList, bTreeNode, layout ) ); if ( created ) { initializeAfterCreation( layout ); } + else + { + loadState( pagedFile ); + } } private void initializeAfterCreation( Layout layout ) throws IOException @@ -277,11 +281,14 @@ private void initializeAfterCreation( Layout layout ) throws IOExcept { long stableGeneration = stableGeneration( generation ); long unstableGeneration = unstableGeneration( generation ); - goToRoot( cursor, stableGeneration, unstableGeneration ); + PageCursorUtil.goTo( cursor, "first root", rootId ); bTreeNode.initializeLeaf( cursor, stableGeneration, unstableGeneration ); checkOutOfBounds( cursor ); } + // Initialize free-list + long freelistPageId = freeList.lastId() + 1; + freeList.initialize( freelistPageId, freelistPageId, freelistPageId, 0, 0 ); checkpoint( IOLimiter.unlimited() ); } @@ -297,7 +304,6 @@ private PagedFile openOrCreate( PageCache pageCache, File indexFile, { readMeta( indexFile, layout, pagedFile ); pagedFile = mapWithCorrectPageSize( pageCache, indexFile, pagedFile ); - loadState( pagedFile ); return pagedFile; } catch ( Throwable t ) @@ -336,9 +342,15 @@ private void loadState( PagedFile pagedFile ) throws IOException { Pair states = readStatePages( pagedFile ); TreeState state = TreeStatePair.selectNewestValidState( states ); - rootId = state.rootId(); - lastId = state.lastId(); generation = Generation.generation( state.stableGeneration(), state.unstableGeneration() ); + rootId = state.rootId(); + + long lastId = state.lastId(); + long freeListWritePageId = state.freeListWritePageId(); + long freeListReadPageId = state.freeListReadPageId(); + int freeListWritePos = state.freeListWritePos(); + int freeListReadPos = state.freeListReadPos(); + freeList.initialize( lastId, freeListWritePageId, freeListReadPageId, freeListWritePos, freeListReadPos ); } private void writeState( PagedFile pagedFile ) throws IOException @@ -349,7 +361,9 @@ private void writeState( PagedFile pagedFile ) throws IOException try ( PageCursor cursor = pagedFile.io( pageToOverwrite, PagedFile.PF_SHARED_WRITE_LOCK ) ) { PageCursorUtil.goTo( cursor, "state page", pageToOverwrite ); - TreeState.write( cursor, stableGeneration( generation ), unstableGeneration( generation ), rootId, lastId ); + TreeState.write( cursor, stableGeneration( generation ), unstableGeneration( generation ), rootId, + freeList.lastId(), freeList.writePageId(), freeList.readPageId(), + freeList.writePos(), freeList.readPos() ); checkOutOfBounds( cursor ); } } @@ -458,19 +472,6 @@ public RawCursor,IOException> seek( KEY fromInclusive, KEY toExcl stableGeneration, unstableGeneration, generationSupplier ); } - @Override - public long acquireNewId() throws IOException - { - lastId++; - try ( PageCursor cursor = pagedFile.io( lastId, PagedFile.PF_SHARED_WRITE_LOCK ) ) - { - cursor.next(); - // TODO use cursor.clear() when available - cursor.putBytes( zeroPage ); - } - return lastId; - } - @Override public void checkpoint( IOLimiter ioLimiter ) throws IOException { @@ -654,7 +655,7 @@ public void merge( KEY key, VALUE value, ValueMerger valueMerger ) throws if ( structurePropagation.hasSplit ) { // New root - long newRootId = acquireNewId(); + long newRootId = freeList.acquireNewId( stableGeneration, unstableGeneration ); PageCursorUtil.goTo( cursor, "new root", newRootId ); bTreeNode.initializeInternal( cursor, stableGeneration, unstableGeneration ); diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/GenSafePointer.java b/community/index/src/main/java/org/neo4j/index/gbptree/GenSafePointer.java index 1a3003507958e..aa987ca755966 100644 --- a/community/index/src/main/java/org/neo4j/index/gbptree/GenSafePointer.java +++ b/community/index/src/main/java/org/neo4j/index/gbptree/GenSafePointer.java @@ -21,6 +21,10 @@ import org.neo4j.io.pagecache.PageCursor; +import static org.neo4j.index.gbptree.PageCursorUtil.get6BLong; +import static org.neo4j.index.gbptree.PageCursorUtil.getUnsignedInt; +import static org.neo4j.index.gbptree.PageCursorUtil.put6BLong; + /** * Provides static methods for getting and manipulating GSP (gen-safe pointer) data. * All interaction is made using a {@link PageCursor}. These methods are about a single GSP, @@ -51,11 +55,13 @@ class GenSafePointer static final long MAX_POINTER = 0xFFFF_FFFFFFFFL; static final int UNSIGNED_SHORT_MASK = 0xFFFF; + static final int GENERATION_SIZE = 4; + static final int POINTER_SIZE = 6; static final int CHECKSUM_SIZE = 2; static final int SIZE = - 4 + // generation (unsigned int) - 6 + // pointer (6B long) - CHECKSUM_SIZE; // checksum for generation & pointer + GENERATION_SIZE + + POINTER_SIZE + + CHECKSUM_SIZE; /** * Writes GSP at the given {@code offset}, the two fields (generation, pointer) + a checksum will be written. @@ -93,7 +99,7 @@ private static void assertPointerOnWrite( long pointer ) public static long readGeneration( PageCursor cursor ) { - return cursor.getInt() & GENERATION_MASK; + return getUnsignedInt( cursor ); } public static long readPointer( PageCursor cursor ) @@ -113,22 +119,7 @@ public static boolean verifyChecksum( PageCursor cursor, long generation, long p return checksum == checksumOf( generation, pointer ); } - private static long get6BLong( PageCursor cursor ) - { - long lsb = cursor.getInt() & GENERATION_MASK; - long msb = cursor.getShort() & UNSIGNED_SHORT_MASK; - return lsb | (msb << Integer.SIZE); - } - // package visible for test purposes - static void put6BLong( PageCursor cursor, long value ) - { - int lsb = (int) value; - short msb = (short) (value >>> Integer.SIZE); - cursor.putInt( lsb ); - cursor.putShort( msb ); - } - /** * Calculates a 2-byte checksum from GSP data. * diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/IdProvider.java b/community/index/src/main/java/org/neo4j/index/gbptree/IdProvider.java index 203e61df97101..4029f9b1a736d 100644 --- a/community/index/src/main/java/org/neo4j/index/gbptree/IdProvider.java +++ b/community/index/src/main/java/org/neo4j/index/gbptree/IdProvider.java @@ -27,5 +27,7 @@ */ interface IdProvider { - long acquireNewId() throws IOException; + long acquireNewId( long stableGeneration, long unstableGeneration ) throws IOException; + + void releaseId( long stableGeneration, long unstableGeneration, long id ) throws IOException; } diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/InternalTreeLogic.java b/community/index/src/main/java/org/neo4j/index/gbptree/InternalTreeLogic.java index 0d5550e8e5553..d4e8df20b115b 100644 --- a/community/index/src/main/java/org/neo4j/index/gbptree/InternalTreeLogic.java +++ b/community/index/src/main/java/org/neo4j/index/gbptree/InternalTreeLogic.java @@ -231,7 +231,7 @@ private void splitInternal( PageCursor cursor, StructurePropagation structu long current = cursor.getCurrentPageId(); long oldRight = bTreeNode.rightSibling( cursor, stableGeneration, unstableGeneration ); PointerChecking.checkPointer( oldRight, true ); - long newRight = idProvider.acquireNewId(); + long newRight = idProvider.acquireNewId( stableGeneration, unstableGeneration ); // Find position to insert new key int pos = positionOf( search( cursor, bTreeNode, primKey, readKey, keyCount ) ); @@ -387,7 +387,7 @@ private void splitLeaf( PageCursor cursor, StructurePropagation structurePr long current = cursor.getCurrentPageId(); long oldRight = bTreeNode.rightSibling( cursor, stableGeneration, unstableGeneration ); PointerChecking.checkPointer( oldRight, true ); - long newRight = idProvider.acquireNewId(); + long newRight = idProvider.acquireNewId( stableGeneration, unstableGeneration ); // BALANCE KEYS AND VALUES // Two different scenarios @@ -594,7 +594,7 @@ private VALUE removeFromLeaf( PageCursor cursor, StructurePropagation struc * Create a new node and copy content from current node (where {@code cursor} sits) if current node is not already * of {@code unstableGeneration}. *

- * Neighbouring nodes' sibling pointers will be updated to point to new node. + * Neighboring nodes' sibling pointers will be updated to point to new node. *

* Current node will be updated with new gen pointer to new node. *

@@ -610,6 +610,7 @@ private VALUE removeFromLeaf( PageCursor cursor, StructurePropagation struc private void createUnstableVersionIfNeeded( PageCursor cursor, StructurePropagation structurePropagation, long stableGeneration, long unstableGeneration ) throws IOException { + long oldGenId = cursor.getCurrentPageId(); long nodeGen = bTreeNode.gen( cursor ); if ( nodeGen == unstableGeneration ) { @@ -618,7 +619,7 @@ private void createUnstableVersionIfNeeded( PageCursor cursor, StructurePropagat } // Do copy - long newGenId = idProvider.acquireNewId(); + long newGenId = idProvider.acquireNewId( stableGeneration, unstableGeneration ); try ( PageCursor newGenCursor = cursor.openLinkedCursor( newGenId ) ) { goTo( newGenCursor, "new gen", newGenId ); @@ -664,5 +665,7 @@ private void createUnstableVersionIfNeeded( PageCursor cursor, StructurePropagat // Propagate structure change structurePropagation.hasNewGen = true; structurePropagation.left = newGenId; + + idProvider.releaseId( stableGeneration, unstableGeneration, oldGenId ); } } diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/TreeNode.java b/community/index/src/main/java/org/neo4j/index/gbptree/TreeNode.java index d8254ec996bd3..d36b14f9ebb4f 100644 --- a/community/index/src/main/java/org/neo4j/index/gbptree/TreeNode.java +++ b/community/index/src/main/java/org/neo4j/index/gbptree/TreeNode.java @@ -38,9 +38,9 @@ *

  * # = empty space
  *
- * [                      HEADER   81B                    ]|[      KEYS     ]|[     CHILDREN             ]
- * [TYPE][GEN][KEYCOUNT][RIGHTSIBLING][LEFTSIBLING][NEWGEN]|[[KEY][KEY]...##]|[[CHILD][CHILD][CHILD]...##]
- *  0     1    5         9             33           57
+ * [                            HEADER   82B                        ]|[      KEYS     ]|[     CHILDREN             ]
+ * [NODETYPE][TYPE][GEN][KEYCOUNT][RIGHTSIBLING][LEFTSIBLING][NEWGEN]|[[KEY][KEY]...##]|[[CHILD][CHILD][CHILD]...##]
+ *  0         1     6    10        34            58
  * 
* Calc offset for key i (starting from 0) * HEADER_LENGTH + i * SIZE_KEY @@ -51,9 +51,9 @@ * Using Separate design the leaf nodes should look like * *
- * [                      HEADER   81B                    ]|[      KEYS     ]|[     VALUES        ]
- * [TYPE][GEN][KEYCOUNT][RIGHTSIBLING][LEFTSIBLING][NEWGEN]|[[KEY][KEY]...##]|[[VALUE][VALUE]...##]
- *  0     1    5         9             33           57
+ * [                            HEADER   82B                        ]|[      KEYS     ]|[     VALUES        ]
+ * [NODETYPE][TYPE][GEN][KEYCOUNT][RIGHTSIBLING][LEFTSIBLING][NEWGEN]|[[KEY][KEY]...##]|[[VALUE][VALUE]...##]
+ *  0         1     6    10        34            58
  * 
* * Calc offset for key i (starting from 0) @@ -67,8 +67,13 @@ */ class TreeNode { + // Shared between all node types: TreeNode and FreelistNode + static final int BYTE_POS_NODE_TYPE = 0; + static final byte NODE_TYPE_TREE_NODE = 1; + static final byte NODE_TYPE_FREE_LIST_NODE = 2; + static final int SIZE_PAGE_REFERENCE = GenSafePointerPair.SIZE; - static final int BYTE_POS_TYPE = 0; + static final int BYTE_POS_TYPE = BYTE_POS_NODE_TYPE + Byte.BYTES; static final int BYTE_POS_GEN = BYTE_POS_TYPE + Byte.BYTES; static final int BYTE_POS_KEYCOUNT = BYTE_POS_GEN + Integer.BYTES; static final int BYTE_POS_RIGHTSIBLING = BYTE_POS_KEYCOUNT + Integer.BYTES; @@ -108,8 +113,14 @@ class TreeNode } } + static byte nodeType( PageCursor cursor ) + { + return cursor.getByte( BYTE_POS_NODE_TYPE ); + } + private void initialize( PageCursor cursor, byte type, long stableGeneration, long unstableGeneration ) { + cursor.putByte( BYTE_POS_NODE_TYPE, NODE_TYPE_TREE_NODE ); cursor.putByte( BYTE_POS_TYPE, type ); setGen( cursor, unstableGeneration ); setKeyCount( cursor, 0 ); diff --git a/community/index/src/main/java/org/neo4j/index/gbptree/TreeState.java b/community/index/src/main/java/org/neo4j/index/gbptree/TreeState.java index 0ae61cce27410..61e9f98482630 100644 --- a/community/index/src/main/java/org/neo4j/index/gbptree/TreeState.java +++ b/community/index/src/main/java/org/neo4j/index/gbptree/TreeState.java @@ -19,6 +19,8 @@ */ package org.neo4j.index.gbptree; +import java.util.Objects; + import org.neo4j.io.pagecache.PageCursor; class TreeState @@ -28,15 +30,25 @@ class TreeState private final long unstableGeneration; private final long rootId; private final long lastId; - private final boolean valid; - - TreeState( long pageId, long stableGeneration, long unstableGeneration, long rootId, long lastId, boolean valid ) + private final long freeListWritePageId; + private final long freeListReadPageId; + private final int freeListWritePos; + private final int freeListReadPos; + private boolean valid; + + TreeState( long pageId, long stableGeneration, long unstableGeneration, long rootId, long lastId, + long freeListWritePageId, long freeListReadPageId, int freeListWritePos, int freeListReadPos, + boolean valid ) { this.pageId = pageId; this.stableGeneration = stableGeneration; this.unstableGeneration = unstableGeneration; this.rootId = rootId; this.lastId = lastId; + this.freeListWritePageId = freeListWritePageId; + this.freeListReadPageId = freeListReadPageId; + this.freeListWritePos = freeListWritePos; + this.freeListReadPos = freeListReadPos; this.valid = valid; } @@ -65,59 +77,131 @@ long lastId() return lastId; } + long freeListWritePageId() + { + return freeListWritePageId; + } + + long freeListReadPageId() + { + return freeListReadPageId; + } + + int freeListWritePos() + { + return freeListWritePos; + } + + int freeListReadPos() + { + return freeListReadPos; + } + boolean isValid() { return valid; } static void write( PageCursor cursor, long stableGeneration, long unstableGeneration, long rootId, - long lastId ) + long lastId, long freeListWritePageId, long freeListReadPageId, int freeListWritePos, + int freeListReadPos ) { GenSafePointer.assertGenerationOnWrite( stableGeneration ); GenSafePointer.assertGenerationOnWrite( unstableGeneration ); - writeStateOnce( cursor, stableGeneration, unstableGeneration, rootId, lastId ); // Write state - writeStateOnce( cursor, stableGeneration, unstableGeneration, rootId, lastId ); // Write checksum + writeStateOnce( cursor, stableGeneration, unstableGeneration, rootId, lastId, + freeListWritePageId, freeListReadPageId, freeListWritePos, freeListReadPos ); // Write state + writeStateOnce( cursor, stableGeneration, unstableGeneration, rootId, lastId, + freeListWritePageId, freeListReadPageId, freeListWritePos, freeListReadPos ); // Write checksum } static TreeState read( PageCursor cursor ) { - long pageId = cursor.getCurrentPageId(); + TreeState state = readStateOnce( cursor ); + TreeState checksumState = readStateOnce( cursor ); - long stableGeneration = cursor.getInt() & GenSafePointer.GENERATION_MASK; - long unstableGeneration = cursor.getInt() & GenSafePointer.GENERATION_MASK; - long rootId = cursor.getLong(); - long lastId = cursor.getLong(); + boolean valid = state.equals( checksumState ); + + boolean isEmpty = state.isEmpty(); + valid &= !isEmpty; - long checksumStableGeneration = cursor.getInt() & GenSafePointer.GENERATION_MASK; - long checksumUnstableGeneration = cursor.getInt() & GenSafePointer.GENERATION_MASK; - long checksumRootId = cursor.getLong(); - long checksumLastId = cursor.getLong(); + return state.setValid( valid ); + } - boolean valid = stableGeneration == checksumStableGeneration && - unstableGeneration == checksumUnstableGeneration && - rootId == checksumRootId && - lastId == checksumLastId; + private TreeState setValid( boolean valid ) + { + this.valid = valid; + return this; + } - boolean isEmpty = stableGeneration == 0L && unstableGeneration == 0L && rootId == 0L && lastId == 0L; - valid &= !isEmpty; + private boolean isEmpty() + { + return stableGeneration == 0L && unstableGeneration == 0L && rootId == 0L && lastId == 0L && + freeListWritePageId == 0L && freeListReadPageId == 0L && freeListWritePos == 0 && freeListReadPos == 0; + } - return new TreeState( pageId, stableGeneration, unstableGeneration, rootId, lastId, valid ); + private static TreeState readStateOnce( PageCursor cursor ) + { + long pageId = cursor.getCurrentPageId(); + long stableGeneration = cursor.getInt() & GenSafePointer.GENERATION_MASK; + long unstableGeneration = cursor.getInt() & GenSafePointer.GENERATION_MASK; + long rootId = cursor.getLong(); + long lastId = cursor.getLong(); + long freeListWritePageId = cursor.getLong(); + long freeListReadPageId = cursor.getLong(); + int freeListWritePos = cursor.getInt(); + int freeListReadPos = cursor.getInt(); + return new TreeState( pageId, stableGeneration, unstableGeneration, rootId, lastId, + freeListWritePageId, freeListReadPageId, freeListWritePos, freeListReadPos, true ); } private static void writeStateOnce( PageCursor cursor, long stableGeneration, long unstableGeneration, - long rootId, long lastId ) + long rootId, long lastId, long freeListWritePageId, long freeListReadPageId, int freeListWritePos, + int freeListReadPos ) { cursor.putInt( (int) stableGeneration ); cursor.putInt( (int) unstableGeneration ); cursor.putLong( rootId ); cursor.putLong( lastId ); + cursor.putLong( freeListWritePageId ); + cursor.putLong( freeListReadPageId ); + cursor.putInt( freeListWritePos ); + cursor.putInt( freeListReadPos ); } @Override public String toString() { - return String.format( "pageId=%d, stableGeneration=%d, unstableGeneration=%d, rootId=%s, lastId=%d, valid=%b", - pageId, stableGeneration, unstableGeneration, rootId, lastId, valid ); + return String.format( "pageId=%d, stableGeneration=%d, unstableGeneration=%d, rootId=%s, lastId=%d, " + + "freeListWritePageId=%d, freeListReadPageId=%d, freeListWritePos=%d, freeListReadPos=%d, valid=%b", + pageId, stableGeneration, unstableGeneration, rootId, lastId, + freeListWritePageId, freeListReadPageId, freeListWritePos, freeListReadPos, valid ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { return true; } + if ( o == null || getClass() != o.getClass() ) + { return false; } + TreeState treeState = (TreeState) o; + return pageId == treeState.pageId && + stableGeneration == treeState.stableGeneration && + unstableGeneration == treeState.unstableGeneration && + rootId == treeState.rootId && + lastId == treeState.lastId && + freeListWritePageId == treeState.freeListWritePageId && + freeListReadPageId == treeState.freeListReadPageId && + freeListWritePos == treeState.freeListWritePos && + freeListReadPos == treeState.freeListReadPos && + valid == treeState.valid; + } + + @Override + public int hashCode() + { + return Objects.hash( pageId, stableGeneration, unstableGeneration, rootId, lastId, freeListWritePageId, + freeListReadPageId, freeListWritePos, freeListReadPos, valid ); } } diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/FreeListIdProviderTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/FreeListIdProviderTest.java new file mode 100644 index 0000000000000..77b341fdcbc48 --- /dev/null +++ b/community/index/src/test/java/org/neo4j/index/gbptree/FreeListIdProviderTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2002-2016 "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.gbptree; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import org.neo4j.collection.primitive.Primitive; +import org.neo4j.collection.primitive.PrimitiveLongSet; +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; +import org.neo4j.test.rule.RandomRule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class FreeListIdProviderTest +{ + private static final int PAGE_SIZE = 128; + private static final long GENERATION_ONE = GenSafePointer.MIN_GENERATION; + private static final long GENERATION_TWO = GENERATION_ONE + 1; + private static final long GENERATION_THREE = GENERATION_TWO + 1; + private static final long GENERATION_FOUR = GENERATION_THREE + 1; + private static final long BASE_ID = 5; + + private final PageAwareByteArrayCursor cursor = new PageAwareByteArrayCursor( PAGE_SIZE ); + private final PagedFile pagedFile = mock( PagedFile.class ); + private final FreeListIdProvider freelist = new FreeListIdProvider( pagedFile, PAGE_SIZE, BASE_ID ); + + @Rule + public final RandomRule random = new RandomRule(); + + @Before + public void setUpPagedFile() throws IOException + { + when( pagedFile.io( anyLong(), anyInt() ) ).thenAnswer( + invocation -> cursor.duplicate( invocation.getArgumentAt( 0, Long.class ).longValue() ) ); + freelist.initialize( BASE_ID + 1, BASE_ID + 1, BASE_ID + 1, 0, 0 ); + } + + @Test + public void shouldReleaseAndAcquireId() throws Exception + { + // GIVEN + long releasedId = 11; + fillPageWithCrapData( releasedId ); + + // WHEN + freelist.releaseId( GENERATION_ONE, GENERATION_TWO, releasedId ); + long acquiredId = freelist.acquireNewId( GENERATION_TWO, GENERATION_THREE ); + + // THEN + assertEquals( releasedId, acquiredId ); + cursor.next( acquiredId ); + assertEmpty( cursor ); + } + + @Test + public void shouldReleaseAndAcquireIdsFromMultiplePages() throws Exception + { + // GIVEN + int entries = freelist.entriesPerPage() + freelist.entriesPerPage() / 2; + long baseId = 101; + for ( int i = 0; i < entries; i++ ) + { + freelist.releaseId( GENERATION_ONE, GENERATION_TWO, baseId + i ); + } + + // WHEN/THEN + for ( int i = 0; i < entries; i++ ) + { + long acquiredId = freelist.acquireNewId( GENERATION_TWO, GENERATION_THREE ); + assertEquals( baseId + i, acquiredId ); + } + } + + @Test + public void shouldPutFreedFreeListPagesIntoFreeListAsWell() throws Exception + { + // GIVEN + long prevId; + long acquiredId = BASE_ID + 1; + long freelistPageId = BASE_ID + 1; + PrimitiveLongSet released = Primitive.longSet(); + do + { + prevId = acquiredId; + acquiredId = freelist.acquireNewId( GENERATION_ONE, GENERATION_TWO ); + freelist.releaseId( GENERATION_ONE, GENERATION_TWO, acquiredId ); + released.add( acquiredId ); + } + while ( acquiredId - prevId == 1 ); + + // WHEN + while ( !released.isEmpty() ) + { + long reAcquiredId = freelist.acquireNewId( GENERATION_TWO, GENERATION_THREE ); + released.remove( reAcquiredId ); + } + + // THEN + assertEquals( freelistPageId, freelist.acquireNewId( GENERATION_THREE, GENERATION_FOUR ) ); + } + + @Test + public void shouldStayBoundUnderStress() throws Exception + { + // GIVEN + PrimitiveLongSet acquired = Primitive.longSet(); + List acquiredList = new ArrayList<>(); // for quickly finding random to remove + long stableGeneration = GenSafePointer.MIN_GENERATION; + long unstableGeneration = stableGeneration + 1; + int iterations = 100; + + // WHEN + for ( int i = 0; i < iterations; i++ ) + { + for ( int j = 0; j < 10; j++ ) + { + if ( random.nextBoolean() ) + { + // acquire + int count = random.intBetween( 5, 10 ); + for ( int k = 0; k < count; k++ ) + { + long acquiredId = freelist.acquireNewId( stableGeneration, unstableGeneration ); + assertTrue( acquired.add( acquiredId ) ); + acquiredList.add( acquiredId ); + } + } + else + { + // release + int count = random.intBetween( 5, 20 ); + for ( int k = 0; k < count && !acquired.isEmpty(); k++ ) + { + long id = acquiredList.remove( random.nextInt( acquiredList.size() ) ); + assertTrue( acquired.remove( id ) ); + freelist.releaseId( stableGeneration, unstableGeneration, id ); + } + } + } + + for ( long id : acquiredList ) + { + freelist.releaseId( stableGeneration, unstableGeneration, id ); + } + acquiredList.clear(); + acquired.clear(); + + // checkpoint, sort of + stableGeneration = unstableGeneration; + unstableGeneration++; + } + + // THEN + assertTrue( String.valueOf( freelist.lastId() ), freelist.lastId() < 200 ); + } + + private void fillPageWithCrapData( long releasedId ) throws IOException + { + cursor.next( releasedId ); + byte[] crapData = new byte[PAGE_SIZE]; + ThreadLocalRandom.current().nextBytes( crapData ); + cursor.putBytes( crapData ); + } + + private void assertEmpty( PageCursor cursor ) + { + byte[] data = new byte[PAGE_SIZE]; + cursor.getBytes( data ); + for ( byte b : data ) + { + assertEquals( 0, b ); + } + } +} diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/FreelistNodeTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/FreelistNodeTest.java new file mode 100644 index 0000000000000..5aa9a1132cbfc --- /dev/null +++ b/community/index/src/test/java/org/neo4j/index/gbptree/FreelistNodeTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2002-2016 "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.gbptree; + +import org.junit.Test; + +import org.neo4j.io.pagecache.PageCursor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class FreelistNodeTest +{ + private static final int PAGE_SIZE = 128; + + private final PageCursor cursor = ByteArrayPageCursor.wrap( PAGE_SIZE ); + private final FreelistNode freelist = new FreelistNode( PAGE_SIZE ); + private final int maxEntries = freelist.maxEntries(); + + @Test + public void shouldInitializeTreeNode() throws Exception + { + // GIVEN + freelist.initialize( cursor ); + + // WHEN + byte nodeType = TreeNode.nodeType( cursor ); + + // THEN + assertEquals( TreeNode.NODE_TYPE_FREE_LIST_NODE, nodeType ); + } + + @Test + public void shouldNodeOverwriteNodeType() throws Exception + { + // GIVEN + freelist.initialize( cursor ); + byte nodeType = TreeNode.nodeType( cursor ); + assertEquals( TreeNode.NODE_TYPE_FREE_LIST_NODE, nodeType ); + + // WHEN + long someId = 1234; + freelist.setNext( cursor, someId ); + + // THEN + nodeType = TreeNode.nodeType( cursor ); + assertEquals( TreeNode.NODE_TYPE_FREE_LIST_NODE, nodeType ); + } + + @Test + public void shouldSetAndGetNext() throws Exception + { + // GIVEN + long nextId = 12345; + + // WHEN + freelist.setNext( cursor, nextId ); + long readNextId = freelist.next( cursor ); + + // THEN + assertEquals( nextId, readNextId ); + } + + @Test + public void shouldReadAndWriteFreeListEntries() throws Exception + { + // GIVEN + long generationA = 34; + long pointerA = 56; + long generationB = 78; + long pointerB = 90; + + // WHEN + freelist.write( cursor, generationA, pointerA, 0 ); + freelist.write( cursor, generationB, pointerB, 1 ); + long readPointerA = freelist.read( cursor, generationA + 1, 0 ); + long readPointerB = freelist.read( cursor, generationB + 1, 1 ); + + // THEN + assertEquals( pointerA, readPointerA ); + assertEquals( pointerB, readPointerB ); + } + + @Test + public void shouldFailOnWritingBeyondMaxEntries() throws Exception + { + // WHEN + try + { + freelist.write( cursor, 1, 10, maxEntries ); + fail( "Should've failed" ); + } + catch ( IllegalArgumentException e ) + { + // THEN good + } + } + + @Test + public void shouldFailOnWritingTooBigPointer() throws Exception + { + // WHEN + try + { + freelist.write( cursor, 1, PageCursorUtil._6B_MASK + 1, 0 ); + fail( "Should've failed" ); + } + catch ( IllegalArgumentException e ) + { + // THEN good + } + } + + @Test + public void shouldFailOnWritingTooBigGeneration() throws Exception + { + // WHEN + try + { + freelist.write( cursor, GenSafePointer.MAX_GENERATION + 1, 1, 0 ); + fail( "Should've failed" ); + } + catch ( IllegalArgumentException e ) + { + // THEN good + } + } + + @Test + public void shouldReturnNoPageOnUnstableEntry() throws Exception + { + // GIVEN + long stableGeneration = 10; + long unstableGeneration = stableGeneration + 1; + long pageId = 20; + int pos = 2; + freelist.write( cursor, unstableGeneration, pageId, pos ); + + // WHEN + long read = freelist.read( cursor, stableGeneration, pos ); + + // THEN + assertEquals( FreelistNode.NO_PAGE_ID, read ); + } +} diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/GBPTreeTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/GBPTreeTest.java index f3ba85c18c36d..95c1c5593ef81 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/GBPTreeTest.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/GBPTreeTest.java @@ -27,8 +27,10 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.Random; import java.util.TreeMap; @@ -37,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; + import org.neo4j.collection.primitive.Primitive; import org.neo4j.collection.primitive.PrimitiveLongCollections; import org.neo4j.collection.primitive.PrimitiveLongSet; @@ -54,17 +57,15 @@ import org.neo4j.test.Barrier; import org.neo4j.test.rule.RandomRule; +import static java.lang.Integer.max; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; - -import static java.lang.Integer.max; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; - import static org.neo4j.index.IndexWriter.Options.DEFAULTS; import static org.neo4j.index.gbptree.GBPTree.NO_MONITOR; import static org.neo4j.index.gbptree.ThrowingRunnable.throwing; @@ -81,7 +82,7 @@ public class GBPTreeTest private final Layout layout = new SimpleLongLayout(); private GBPTree index; - public GBPTree createIndex( int pageSize ) + private GBPTree createIndex( int pageSize ) throws IOException { return createIndex( pageSize, NO_MONITOR ); @@ -264,7 +265,7 @@ public void shouldFailOnOpenWithDifferentPageSize() throws Exception } @Test - public void shouldFailOnPageSizeLargerThanThatOfPageCache() throws Exception + public void shouldFailOnStartingWithPageSizeLargerThanThatOfPageCache() throws Exception { // WHEN int pageSize = 512; @@ -282,6 +283,96 @@ public void shouldFailOnPageSizeLargerThanThatOfPageCache() throws Exception } } + @Test + public void shouldMapIndexFileWithProvidedPageSizeIfLessThanOrEqualToCachePageSize() throws Exception + { + // WHEN + int pageSize = 1024; + pageCache = new MuninnPageCache( swapperFactory(), 10_000, pageSize, NULL ); + indexFile = new File( folder.getRoot(), "index" ); + try ( Index index = + new GBPTree<>( pageCache, indexFile, layout, pageSize / 2, NO_MONITOR ) ) + { + // Good + } + } + + @Test + public void shouldFailWhenTryingToRemapWithPageSizeLargerThanCachePageSize() throws Exception + { + // WHEN + int pageSize = 1024; + pageCache = new MuninnPageCache( swapperFactory(), 10_000, pageSize, NULL ); + indexFile = new File( folder.getRoot(), "index" ); + try ( Index index = + new GBPTree<>( pageCache, indexFile, layout, pageSize, NO_MONITOR ) ) + { + // Good + } + + pageCache = new MuninnPageCache( swapperFactory(), 10_000, pageSize / 2, NULL ); + try ( GBPTree index = + new GBPTree<>( pageCache, indexFile, layout, pageSize, NO_MONITOR ) ) + { + fail( "Expected to fail" ); + } + catch ( MetadataMismatchException e ) + { + // THEN Good + assertThat( e.getMessage(), containsString( "page size" ) ); + } + } + + @Test + public void shouldRemapFileIfMappedWithPageSizeLargerThanCreationSize() throws Exception + { + // WHEN + int pageSize = 1024; + pageCache = new MuninnPageCache( swapperFactory(), 10_000, pageSize, NULL ); + indexFile = new File( folder.getRoot(), "index" ); + List expectedData = new ArrayList<>(); + for ( long i = 0; i < 100; i++ ) + { + expectedData.add( i ); + } + try ( Index index = + new GBPTree<>( pageCache, indexFile, layout, pageSize / 2, NO_MONITOR ) ) + { + // Insert some data + try ( IndexWriter writer = index.writer( IndexWriter.Options.DEFAULTS ) ) + { + MutableLong key = new MutableLong(); + MutableLong value = new MutableLong(); + + for ( Long insert : expectedData ) + { + key.setValue( insert ); + value.setValue( insert ); + writer.put( key, value ); + } + } + index.checkpoint( IOLimiter.unlimited() ); + } + + // THEN + try ( Index index = new GBPTree<>( pageCache, indexFile, layout, 0, NO_MONITOR ) ) + { + MutableLong fromInclusive = new MutableLong( 0L ); + MutableLong toExclusive = new MutableLong( 200L ); + try ( RawCursor, IOException> seek = index.seek( fromInclusive, toExclusive ) ) + { + int i = 0; + while ( seek.next() ) + { + Hit hit = seek.get(); + assertEquals( hit.key().getValue(), expectedData.get( i ) ); + assertEquals( hit.value().getValue(), expectedData.get( i ) ); + i++; + } + } + } + } + @Test public void shouldReturnNoResultsOnEmptyIndex() throws Exception { @@ -317,6 +408,21 @@ public void shouldNotBeAbleToAcquireModifierTwice() throws Exception writer.close(); } + @Test + public void shouldAllowClosingIndexWriterMultipleTimes() throws Exception + { + // GIVEN + index = createIndex( 256 ); + IndexWriter writer = index.writer( DEFAULTS ); + writer.put( new MutableLong( 0 ), new MutableLong( 1 ) ); + writer.close(); + + // WHEN + writer.close(); + + // THEN that should be OK + } + /* Check-pointing tests */ @Test diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/InternalTreeLogicTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/InternalTreeLogicTest.java index 25514e894ea2f..53bcb50386825 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/InternalTreeLogicTest.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/InternalTreeLogicTest.java @@ -105,7 +105,7 @@ public static Collection generators() public void setUp() throws IOException { id.reset(); - long newId = id.acquireNewId(); + long newId = id.acquireNewId( stableGen, unstableGen ); goTo( cursor, newId ); } @@ -1091,7 +1091,7 @@ private MutableLong key( long key ) private long newRootFromSplit( StructurePropagation split ) throws IOException { assertTrue( split.hasSplit ); - long rootId = id.acquireNewId(); + long rootId = id.acquireNewId( stableGen, unstableGen ); goTo( cursor, rootId ); node.initializeInternal( cursor, stableGen, unstableGen ); node.insertKeyAt( cursor, split.primKey, 0, 0, tmp ); diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/PointerCheckingTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/PointerCheckingTest.java index 1b261aef2b82c..c9e7e4e2d1bdb 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/PointerCheckingTest.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/PointerCheckingTest.java @@ -28,6 +28,7 @@ import static org.neo4j.index.gbptree.GenSafePointerPair.NO_LOGICAL_POS; import static org.neo4j.index.gbptree.GenSafePointerPair.read; import static org.neo4j.index.gbptree.GenSafePointerPair.write; +import static org.neo4j.index.gbptree.PageCursorUtil.put6BLong; public class PointerCheckingTest { @@ -172,7 +173,7 @@ public void checkSiblingShouldThrowOnReadIllegalPointer() throws Exception // Can not use GenSafePointer.write because it will fail on pointer assertion. cursor.putInt( (int) pointer ); - GenSafePointer.put6BLong( cursor, generation ); + put6BLong( cursor, generation ); cursor.putShort( GenSafePointer.checksumOf( generation, pointer ) ); cursor.rewind(); diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/SeekCursorTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/SeekCursorTest.java index 8d349122b03d5..81db8bb67ed87 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/SeekCursorTest.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/SeekCursorTest.java @@ -78,7 +78,7 @@ public long getAsLong() @Before public void setUp() throws IOException { - cursor.next( id.acquireNewId() ); + cursor.next( id.acquireNewId( stableGen, unstableGen ) ); node.initializeLeaf( cursor, stableGen, unstableGen ); } @@ -999,7 +999,7 @@ private void checkpoint() private long newRootFromSplit( StructurePropagation split ) throws IOException { assertTrue( split.hasSplit ); - long rootId = id.acquireNewId(); + long rootId = id.acquireNewId( stableGen, unstableGen ); cursor.next( rootId ); node.initializeInternal( cursor, stableGen, unstableGen ); node.insertKeyAt( cursor, split.primKey, 0, 0, tmp ); diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/SimpleIdProvider.java b/community/index/src/test/java/org/neo4j/index/gbptree/SimpleIdProvider.java index 058ce8113b0d5..daf650eb02a9b 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/SimpleIdProvider.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/SimpleIdProvider.java @@ -19,10 +19,14 @@ */ package org.neo4j.index.gbptree; -import org.neo4j.io.pagecache.PageCursor; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.LinkedList; +import java.util.Queue; class SimpleIdProvider implements IdProvider { + private final Queue> releasedIds = new LinkedList<>(); private long lastId; SimpleIdProvider() @@ -31,12 +35,27 @@ class SimpleIdProvider implements IdProvider } @Override - public long acquireNewId() + public long acquireNewId( long stableGeneration, long unstableGeneration ) { + if ( !releasedIds.isEmpty() ) + { + Pair free = releasedIds.peek(); + if ( free.getLeft() <= stableGeneration ) + { + releasedIds.poll(); + return free.getRight(); + } + } lastId++; return lastId; } + @Override + public void releaseId( long stableGeneration, long unstableGeneration, long id ) + { + releasedIds.add( Pair.of( unstableGeneration, id ) ); + } + long lastId() { return lastId; @@ -44,6 +63,7 @@ long lastId() void reset() { + releasedIds.clear(); lastId = IdSpace.MIN_TREE_NODE_ID - 1; } } diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/TreeNodeTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/TreeNodeTest.java index 519022494423a..7ea094c2e23de 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/TreeNodeTest.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/TreeNodeTest.java @@ -66,6 +66,7 @@ public void shouldInitializeLeaf() throws Exception node.initializeLeaf( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ); // THEN + assertEquals( TreeNode.NODE_TYPE_TREE_NODE, node.nodeType( cursor ) ); assertTrue( node.isLeaf( cursor ) ); assertFalse( node.isInternal( cursor ) ); assertEquals( UNSTABLE_GENERATION, node.gen( cursor ) ); @@ -82,6 +83,7 @@ public void shouldInitializeInternal() throws Exception node.initializeInternal( cursor, STABLE_GENERATION, UNSTABLE_GENERATION ); // THEN + assertEquals( TreeNode.NODE_TYPE_TREE_NODE, node.nodeType( cursor ) ); assertFalse( node.isLeaf( cursor ) ); assertTrue( node.isInternal( cursor ) ); assertEquals( UNSTABLE_GENERATION, node.gen( cursor ) ); diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/TreeStatePairTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/TreeStatePairTest.java index d50d82b1c2ddc..428a5ed070b9a 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/TreeStatePairTest.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/TreeStatePairTest.java @@ -126,7 +126,7 @@ void write( PageCursor cursor ) @Override void write( PageCursor cursor ) throws IOException { - TreeState.write( cursor, 1, 2, 3, 4 ); + TreeState.write( cursor, 1, 2, 3, 4, 5, 6, 7, 8 ); cursor.rewind(); // flip some of the bits as to break the checksum long someOfTheBits = cursor.getLong( cursor.getOffset() ); @@ -138,7 +138,7 @@ void write( PageCursor cursor ) throws IOException @Override void write( PageCursor cursor ) throws IOException { - TreeState.write( cursor, 5, 6, 7, 8 ); + TreeState.write( cursor, 5, 6, 7, 8, 9, 10, 11, 12 ); } }, OLD_VALID @@ -146,7 +146,7 @@ void write( PageCursor cursor ) throws IOException @Override void write( PageCursor cursor ) throws IOException { - TreeState.write( cursor, 2, 3, 4, 5 ); + TreeState.write( cursor, 2, 3, 4, 5, 6, 7, 8, 9 ); } }; diff --git a/community/index/src/test/java/org/neo4j/index/gbptree/TreeStateTest.java b/community/index/src/test/java/org/neo4j/index/gbptree/TreeStateTest.java index 4bfa014b09153..15d225e395e9e 100644 --- a/community/index/src/test/java/org/neo4j/index/gbptree/TreeStateTest.java +++ b/community/index/src/test/java/org/neo4j/index/gbptree/TreeStateTest.java @@ -26,6 +26,7 @@ import org.neo4j.io.pagecache.PageCursor; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -57,21 +58,25 @@ public void readEmptyStateShouldThrow() throws Exception public void shouldReadValidPage() throws Exception { // GIVEN valid state - TreeState.write( cursor, 1, 2, 3, 4 ); + long pageId = cursor.getCurrentPageId(); + TreeState expected = new TreeState( pageId, 1, 2, 3, 4, 5, 6, 7, 8, true ); + write( cursor, expected ); cursor.rewind(); // WHEN - boolean valid = TreeState.read( cursor ).isValid(); + TreeState read = TreeState.read( cursor ); // THEN - assertTrue( valid ); + assertEquals( expected, read ); } @Test public void readBrokenStateShouldFail() throws Exception { // GIVEN broken state - TreeState.write( cursor, 1, 2, 3, 4 ); + long pageId = cursor.getCurrentPageId(); + TreeState expected = new TreeState( pageId, 1, 2, 3, 4, 5, 6, 7, 8, true ); + write( cursor, expected ); cursor.rewind(); assertTrue( TreeState.read( cursor ).isValid() ); cursor.rewind(); @@ -93,7 +98,8 @@ public void shouldNotWriteInvalidStableGeneration() throws Exception // WHEN try { - TreeState.write( cursor, generation, 2, 3, 4 ); + long pageId = cursor.getCurrentPageId(); + write( cursor, new TreeState( pageId, generation, 2, 3, 4, 5, 6, 7, 8, true ) ); fail( "Should have failed" ); } catch ( IllegalArgumentException e ) @@ -111,7 +117,8 @@ public void shouldNotWriteInvalidUnstableGeneration() throws Exception // WHEN try { - TreeState.write( cursor, 1, generation, 3, 4 ); + long pageId = cursor.getCurrentPageId(); + write( cursor, new TreeState( pageId, 1, generation, 3, 4, 5, 6, 7, 8, true ) ); fail( "Should have failed" ); } catch ( IllegalArgumentException e ) @@ -127,4 +134,17 @@ private void breakChecksum( PageCursor cursor ) long existing = cursor.getLong( cursor.getOffset() ); cursor.putLong( cursor.getOffset(), ~existing ); } + + private void write( PageCursor cursor, TreeState origin ) + { + TreeState.write( cursor, + origin.stableGeneration(), + origin.unstableGeneration(), + origin.rootId(), + origin.lastId(), + origin.freeListWritePageId(), + origin.freeListReadPageId(), + origin.freeListWritePos(), + origin.freeListReadPos() ); + } }