From 4de5f1c45161a4e40c8eb6e7dcdfac3948780581 Mon Sep 17 00:00:00 2001 From: Mattias Persson Date: Sun, 21 Jun 2015 23:49:07 +0200 Subject: [PATCH] Implements a record format with very high id limits although iat the same time keeping store size increase to a minimum. Most record sizes are aligned to powers of two to be more page cache and cache line friendly. To support 50-bit IDs these pointers are compressed so that small IDs take less space than big ones in the records, whereas "null" references take only one bit. One "logical" record may span two physical records for records that have most of its IDs very big, such that one record isn't enough to hold all data. The new format is implemented as a separate format, not plugged in by default, but at least tested by RecordFormatTest. This commit also introduces InternalRecordFormatSelector which has a default (the current one) and ability to be configured to use any format by specifying RecordFormats (fully qualified) name pointing to a class implementing RecordFormats. With this a build can be set up to run with a custom format. --- .../java/org/neo4j/helpers/Exceptions.java | 9 +- .../org/neo4j/kernel/NeoStoreDataSource.java | 4 +- .../api/store/StorePropertyPayloadCursor.java | 5 +- .../impl/store/CommonAbstractStore.java | 19 +- .../impl/store/ComposableRecordStore.java | 8 +- .../store/RecordPageLocationCalculator.java | 55 ++ .../neo4j/kernel/impl/store/StoreFactory.java | 4 +- .../format/BaseOneByteHeaderRecordFormat.java | 43 +- .../impl/store/format/BaseRecordFormat.java | 13 +- .../format/InternalRecordFormatSelector.java | 43 ++ .../impl/store/format/RecordFormat.java | 40 +- .../format/lowlimit/DynamicRecordFormat.java | 39 +- .../format/lowlimit/NodeRecordFormat.java | 9 +- .../format/lowlimit/PropertyRecordFormat.java | 5 +- .../RelationshipGroupRecordFormat.java | 9 +- .../lowlimit/RelationshipRecordFormat.java | 9 +- .../format/lowlimit/TokenRecordFormat.java | 5 +- .../impl/store/record/AbstractBaseRecord.java | 30 + .../impl/store/record/DynamicRecord.java | 2 +- .../impl/store/record/PropertyRecord.java | 1 + .../unsafe/batchinsert/BatchInserterImpl.java | 4 +- .../batchimport/store/BatchingIdSequence.java | 14 +- .../neo4j/graphdb/IndexingAcceptanceTest.java | 2 + .../RecordStorageEngineRule.java | 4 +- .../impl/store/LabelTokenStoreTest.java | 4 +- .../kernel/impl/store/PropertyStoreTest.java | 4 +- .../impl/store/TestIdGeneratorRebuilding.java | 7 +- .../kernel/impl/store/UpgradeStoreIT.java | 3 +- .../store/format/FullyCoveringRecordKeys.java | 193 ++++++ .../store/format/LimitedRecordGenerators.java | 189 ++++++ .../format/LowLimitRecordFormatTest.java | 35 ++ .../RecordBoundaryCheckingPagedFile.java | 368 ++++++++++++ .../impl/store/format/RecordFormatTest.java | 564 +++++------------- .../impl/store/format/RecordGenerators.java | 54 ++ .../kernel/impl/store/format/RecordKey.java | 27 + .../kernel/impl/store/format/RecordKeys.java | 48 ++ .../StandaloneDynamicRecordAllocator.java | 42 ++ .../WriteTransactionCommandOrderingTest.java | 9 +- .../format/busted/BaseBustedRecordFormat.java | 244 ++++++++ .../impl/store/format/busted/Busted.java | 98 +++ .../format/busted/DynamicRecordFormat.java | 77 +++ .../store/format/busted/NodeRecordFormat.java | 91 +++ .../format/busted/PropertyRecordFormat.java | 95 +++ .../impl/store/format/busted/Reference.java | 231 +++++++ .../busted/RelationshipGroupRecordFormat.java | 92 +++ .../busted/RelationshipRecordFormat.java | 103 ++++ .../busted/SecondaryPageCursorControl.java | 70 +++ .../SecondaryPageCursorReadDataAdapter.java | 101 ++++ .../SecondaryPageCursorWriteDataAdapter.java | 72 +++ .../store/format/BustedRecordFormatTest.java | 33 + .../store/format/busted/ReferenceTest.java | 94 +++ 51 files changed, 2830 insertions(+), 494 deletions(-) create mode 100644 community/kernel/src/main/java/org/neo4j/kernel/impl/store/RecordPageLocationCalculator.java create mode 100644 community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/InternalRecordFormatSelector.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/FullyCoveringRecordKeys.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LimitedRecordGenerators.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LowLimitRecordFormatTest.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordBoundaryCheckingPagedFile.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordGenerators.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKey.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKeys.java create mode 100644 community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/StandaloneDynamicRecordAllocator.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/BaseBustedRecordFormat.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Busted.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/DynamicRecordFormat.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/NodeRecordFormat.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/PropertyRecordFormat.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Reference.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipGroupRecordFormat.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipRecordFormat.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorControl.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorReadDataAdapter.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorWriteDataAdapter.java create mode 100644 enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/BustedRecordFormatTest.java create mode 100644 enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/busted/ReferenceTest.java diff --git a/community/kernel/src/main/java/org/neo4j/helpers/Exceptions.java b/community/kernel/src/main/java/org/neo4j/helpers/Exceptions.java index 23991f56dd5b0..f4b1694eb68e3 100644 --- a/community/kernel/src/main/java/org/neo4j/helpers/Exceptions.java +++ b/community/kernel/src/main/java/org/neo4j/helpers/Exceptions.java @@ -256,12 +256,11 @@ public static E combine( E first, E second ) } } - public static T withMessage( T cause, String message ) + public static void setMessage( Throwable cause, String message ) { try { THROWABLE_MESSAGE_FIELD.set( cause, message ); - return cause; } catch ( IllegalArgumentException | IllegalAccessException e ) { @@ -269,6 +268,12 @@ public static T withMessage( T cause, String message ) } } + public static T withMessage( T cause, String message ) + { + setMessage( cause, message ); + return cause; + } + @Deprecated public static boolean containsStackTraceElement( Throwable cause, final Predicate predicate ) diff --git a/community/kernel/src/main/java/org/neo4j/kernel/NeoStoreDataSource.java b/community/kernel/src/main/java/org/neo4j/kernel/NeoStoreDataSource.java index b431c46489242..0c2d8f10f3d36 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/NeoStoreDataSource.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/NeoStoreDataSource.java @@ -84,7 +84,7 @@ import org.neo4j.kernel.impl.store.MetaDataStore; import org.neo4j.kernel.impl.store.StoreId; import org.neo4j.kernel.impl.store.UnderlyingStorageException; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; +import org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector; import org.neo4j.kernel.impl.store.id.IdGeneratorFactory; import org.neo4j.kernel.impl.storemigration.DatabaseMigrator; import org.neo4j.kernel.impl.storemigration.monitoring.VisibleMigrationProgressMonitor; @@ -558,7 +558,7 @@ private StorageEngine buildStorageEngine( labelTokens, relationshipTypeTokens, schemaStateChangeCallback, constraintSemantics, scheduler, tokenNameLookup, lockService, schemaIndexProvider, indexingServiceMonitor, databaseHealth, labelScanStore, legacyIndexProviderLookup, indexConfigStore, legacyIndexTransactionOrdering, - LowLimit.RECORD_FORMATS ) ); + InternalRecordFormatSelector.select() ) ); } private TransactionLogModule buildTransactionLogs( diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/api/store/StorePropertyPayloadCursor.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/api/store/StorePropertyPayloadCursor.java index 2e151fae90ad0..424ed70103b29 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/api/store/StorePropertyPayloadCursor.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/api/store/StorePropertyPayloadCursor.java @@ -53,7 +53,7 @@ /** * Cursor that provides a view on property blocks of a particular property record. * This cursor is reusable and can be re-initialized with - * {@link #init(PageCursor)} method and cleaned up using {@link #clear()} method. + * {@link #init(long[], int)} method and cleaned up using {@link #clear()} method. *

* During initialization {@link #MAX_NUMBER_OF_PAYLOAD_LONG_ARRAY} number of longs is read from * the given {@linkplain PageCursor}. This is done eagerly to avoid reading property blocks from different versions @@ -66,7 +66,6 @@ class StorePropertyPayloadCursor { static final int MAX_NUMBER_OF_PAYLOAD_LONG_ARRAY = PropertyRecordFormat.DEFAULT_PAYLOAD_SIZE / 8; - private static final long PROPERTY_KEY_ID_BITMASK = 0xFFFFFFL; private static final int MAX_BYTES_IN_SHORT_STRING_OR_SHORT_ARRAY = 32; private static final int INTERNAL_BYTE_ARRAY_SIZE = 4096; private static final int INITIAL_POSITION = -1; @@ -130,7 +129,7 @@ PropertyType type() int propertyKeyId() { - return (int) (currentHeader() & PROPERTY_KEY_ID_BITMASK); + return PropertyBlock.keyIndexId( currentHeader() ); } boolean booleanValue() diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/CommonAbstractStore.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/CommonAbstractStore.java index 0c8ab175d2625..1a15e9e078622 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/CommonAbstractStore.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/CommonAbstractStore.java @@ -261,12 +261,12 @@ private void extractHeaderRecord() throws IOException protected long pageIdForRecord( long id ) { - return id * getRecordSize() / storeFile.pageSize(); + return RecordPageLocationCalculator.pageIdForRecord( id, storeFile.pageSize(), getRecordSize() ); } protected int offsetForId( long id ) { - return (int) (id * getRecordSize() % storeFile.pageSize()); + return RecordPageLocationCalculator.offsetForId( id, storeFile.pageSize(), getRecordSize() ); } @Override @@ -940,11 +940,13 @@ protected RECORD getRecord( long id, RECORD record, RecordLoad mode, PageCursor protected void readRecordWithRetry( PageCursor cursor, long id, RECORD record, RecordLoad mode, int offset ) throws IOException { + // Mark the record with this id regardless of whether or not we load the contents of it. + // This is done in this method since there are multiple call sites and they all want the id + // on that record, so it's to ensure it isn't forgotten. + record.setId( id ); + do { - // Mark the record with this id regardless of whether or not we load the contents of it. - record.setId( id ); - // Mark this record as unused. This to simplify implementations of readRecord. // readRecord can behave differently depending on RecordLoad argument and so it may be that // contents of a record may be loaded even if that record is unused, where the contents @@ -963,8 +965,9 @@ protected void readRecordWithRetry( PageCursor cursor, long id, RECORD record, R /** * Reads data from {@link PageCursor} into the record. + * @throws IOException on error reading. */ - protected abstract void readRecord( PageCursor cursor, RECORD record, RecordLoad mode ); + protected abstract void readRecord( PageCursor cursor, RECORD record, RecordLoad mode ) throws IOException; @Override public void updateRecord( RECORD record ) @@ -994,13 +997,15 @@ public void updateRecord( RECORD record ) } } - protected abstract void writeRecord( PageCursor cursor, RECORD record ); + protected abstract void writeRecord( PageCursor cursor, RECORD record ) throws IOException; /** * Scan the given range of records both inclusive, and pass all the in-use ones to the given processor, one by one. * * The record passed to the NodeRecordScanner is reused instead of reallocated for every record, so it must be * cloned if you want to save it for later. + * @param visitor {@link Visitor} notified about all records. + * @throws IOException on error reading from store. */ public void scanAllRecords( Visitor visitor ) throws IOException { diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/ComposableRecordStore.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/ComposableRecordStore.java index 4d3f8d338490c..0675362ae95df 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/ComposableRecordStore.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/ComposableRecordStore.java @@ -70,15 +70,15 @@ public int getRecordDataSize() } @Override - protected void readRecord( PageCursor cursor, RECORD record, RecordLoad mode ) + protected void readRecord( PageCursor cursor, RECORD record, RecordLoad mode ) throws IOException { - recordFormat.read( record, cursor, mode, recordSize ); + recordFormat.read( record, cursor, mode, recordSize, storeFile ); } @Override - protected void writeRecord( PageCursor cursor, RECORD record ) + protected void writeRecord( PageCursor cursor, RECORD record ) throws IOException { - recordFormat.write( record, cursor ); + recordFormat.write( record, cursor, recordSize, storeFile ); } @Override diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/RecordPageLocationCalculator.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/RecordPageLocationCalculator.java new file mode 100644 index 0000000000000..48d8f019981c3 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/RecordPageLocationCalculator.java @@ -0,0 +1,55 @@ +/* + * 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.kernel.impl.store; + +/** + * Calculates page ids and offset based on record ids. + */ +public class RecordPageLocationCalculator +{ + /** + * Calculates which page a record with the given {@code id} should go into. + * + * @param id record id + * @param pageSize size of each page + * @param recordSize size of each record + * @return which page the record with the given {@code id} should go into, given the + * {@code pageSize} and {@code recordSize}. + */ + public static long pageIdForRecord( long id, int pageSize, int recordSize ) + { + return id * recordSize / pageSize; + } + + /** + * Calculates which offset into the right page (had by {@link #pageIdForRecord(long, int, int)}) + * the given {@code id} lives at. + * + * @param id record id + * @param pageSize size of each page + * @param recordSize size of each record + * @return which offset into the right page the given {@code id} lives at, given the + * {@code pageSize} and {@code recordSize}. + */ + public static int offsetForId( long id, int pageSize, int recordSize ) + { + return (int) (id * recordSize % pageSize); + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/StoreFactory.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/StoreFactory.java index 1efb8e3cc3b96..029e991453de7 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/StoreFactory.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/StoreFactory.java @@ -29,8 +29,8 @@ import org.neo4j.kernel.impl.store.id.DefaultIdGeneratorFactory; import org.neo4j.kernel.impl.store.id.IdGeneratorFactory; import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector; import org.neo4j.kernel.impl.store.format.RecordFormats; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; import org.neo4j.logging.LogProvider; /** @@ -84,7 +84,7 @@ public StoreFactory( File storeDir, Config config, FileSystemAbstraction fileSystemAbstraction, LogProvider logProvider ) { this( storeDir, config, idGeneratorFactory, pageCache, fileSystemAbstraction, logProvider, - LowLimit.RECORD_FORMATS ); + InternalRecordFormatSelector.select() ); } public StoreFactory( File storeDir, Config config, diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseOneByteHeaderRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseOneByteHeaderRecordFormat.java index 4260d75f0d64b..4adc8a339afd4 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseOneByteHeaderRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseOneByteHeaderRecordFormat.java @@ -19,9 +19,11 @@ */ package org.neo4j.kernel.impl.store.format; +import java.io.IOException; import java.util.function.Function; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.RecordStore; import org.neo4j.kernel.impl.store.StoreHeader; import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; @@ -30,7 +32,7 @@ /** * Implementation of a very common type of format where the first byte, at least one bit in it, * say whether or not the record is in use. That can be used to let sub classes have simpler - * read/write implementations. + * read/write implementations. The rest of the 7 bits in that header byte are free to use by subclasses. * * @param type of record. */ @@ -43,19 +45,20 @@ protected BaseOneByteHeaderRecordFormat( Function recordSiz } @Override - public final void read( RECORD record, PageCursor cursor, RecordLoad mode, int recordSize ) + public final void read( RECORD record, PageCursor cursor, RecordLoad mode, int recordSize, PagedFile storeFile ) + throws IOException { - byte inUseByte = cursor.getByte(); - boolean inUse = isInUse( inUseByte ); + byte headerByte = cursor.getByte(); + boolean inUse = isInUse( headerByte ); if ( mode.shouldLoad( inUse ) ) { - doRead( record, cursor, recordSize, inUseByte, inUse ); + doRead( record, cursor, recordSize, storeFile, headerByte, inUse ); } } /** * Reads contents at {@code cursor} into the given record. This method is only called if the {@link RecordLoad} - * mode in {@link #read(AbstractBaseRecord, PageCursor, RecordLoad, int)} thinks it's OK to load the record, + * mode in {@link #read(AbstractBaseRecord, PageCursor, RecordLoad, int, PagedFile)} thinks it's OK to load the record, * given its inUse status. * * @param record to put read data into, replacing any existing data in that record object. @@ -63,18 +66,22 @@ public final void read( RECORD record, PageCursor cursor, RecordLoad mode, int r * See {@link RecordStore#getRecord(long, AbstractBaseRecord, RecordLoad)} for more information. * @param recordSize size of records of this format. This is passed in like this since not all formats * know the record size in advance, but may be read from store header when opening the store. - * @param inUseByte the first byte read, in order to determine inUse status. + * @param storeFile {@link PagedFile} to get additional {@link PageCursor} from, if need be. + * @param headerByte the first byte read, in order to determine inUse status. * @param inUse whether or not the record is in use. Keep in mind that this method may be called * even on an unused record, depending on {@link RecordLoad} mode. + * @throws IOException on error reading. */ - protected abstract void doRead( RECORD record, PageCursor cursor, int recordSize, long inUseByte, boolean inUse ); + protected abstract void doRead( RECORD record, PageCursor cursor, int recordSize, PagedFile storeFile, + long headerByte, boolean inUse ) throws IOException; @Override - public final void write( RECORD record, PageCursor cursor ) + public final void write( RECORD record, PageCursor cursor, int recordSize, PagedFile storeFile ) + throws IOException { if ( record.inUse() ) { - doWrite( record, cursor ); + doWrite( record, cursor, recordSize, storeFile ); } else { @@ -91,6 +98,20 @@ public final void write( RECORD record, PageCursor cursor ) * * @param record containing data to write. * @param cursor {@link PageCursor} to write the record data into. + * @param recordSize size of records of this format. This is passed in like this since not all formats + * know the record size in advance, but may be read from store header when opening the store. + * @throws IOException on error writing. */ - protected abstract void doWrite( RECORD record, PageCursor cursor ); + protected abstract void doWrite( RECORD record, PageCursor cursor, int recordSize, PagedFile storeFile ) + throws IOException; + + protected static boolean has( long headerByte, int bitMask ) + { + return (headerByte & bitMask) != 0; + } + + protected static byte set( byte header, int bitMask, boolean value ) + { + return (byte) (value ? header | bitMask : header); + } } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseRecordFormat.java index bffe52a3c1870..073fedbb395c7 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/BaseRecordFormat.java @@ -22,22 +22,24 @@ import java.util.function.Function; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.IntStoreHeader; import org.neo4j.kernel.impl.store.StoreHeader; import org.neo4j.kernel.impl.store.id.IdGeneratorImpl; +import org.neo4j.kernel.impl.store.id.IdSequence; import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; import org.neo4j.kernel.impl.store.record.Record; /** * Basic abstract implementation of a {@link RecordFormat} implementing most functionality except - * {@link #read(AbstractBaseRecord, PageCursor, org.neo4j.kernel.impl.store.record.RecordLoad, int)} and - * {@link #write(AbstractBaseRecord, PageCursor)}. + * {@link #read(AbstractBaseRecord, PageCursor, org.neo4j.kernel.impl.store.record.RecordLoad, int, PagedFile)} and + * {@link #write(AbstractBaseRecord, PageCursor, int, PagedFile)}. * * @param type of record. */ public abstract class BaseRecordFormat implements RecordFormat { - public static final int IN_USE_BIT = 0x1; + public static final int IN_USE_BIT = 0b0000_0001; public static final Function INT_STORE_HEADER_READER = (header) -> ((IntStoreHeader)header).value(); @@ -91,4 +93,9 @@ public static long longFromIntAndMod( long base, long modifier ) { return modifier == 0 && base == IdGeneratorImpl.INTEGER_MINUS_ONE ? -1 : base | modifier; } + + @Override + public void prepare( RECORD record, int recordSize, IdSequence idSequence ) + { // Do nothing by default + } } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/InternalRecordFormatSelector.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/InternalRecordFormatSelector.java new file mode 100644 index 0000000000000..1000cfe637bbf --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/InternalRecordFormatSelector.java @@ -0,0 +1,43 @@ +/* + * 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.kernel.impl.store.format; + +import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; + +/** + * Selects format to use for databases in this JVM, using a system property. By default uses the safest + * and established format. During development this may be switched in builds to experimental formats + * to gain more testing there. + */ +public class InternalRecordFormatSelector +{ + public static RecordFormats select() + { + String formatsClassName = System.getProperty( RecordFormats.class.getName(), LowLimit.class.getName() ); + try + { + return Class.forName( formatsClassName ).asSubclass( RecordFormats.class ).newInstance(); + } + catch ( Exception e ) + { + throw new Error( "Couldn't load specified record format class '" + formatsClassName + "'", e ); + } + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/RecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/RecordFormat.java index 8e20b8225cbe5..07ca0c6d45e31 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/RecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/RecordFormat.java @@ -19,9 +19,13 @@ */ package org.neo4j.kernel.impl.store.format; +import java.io.IOException; + import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.RecordStore; import org.neo4j.kernel.impl.store.StoreHeader; +import org.neo4j.kernel.impl.store.id.IdSequence; import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; import org.neo4j.kernel.impl.store.record.DynamicRecord; import org.neo4j.kernel.impl.store.record.RecordLoad; @@ -35,12 +39,12 @@ public interface RecordFormat { /** - * Instantiates a new record to use in {@link #read(AbstractBaseRecord, PageCursor, RecordLoad, int)} - * and {@link #write(AbstractBaseRecord, PageCursor)}. Records may be reused, which is why the instantiation + * Instantiates a new record to use in {@link #read(AbstractBaseRecord, PageCursor, RecordLoad, int, PagedFile)} + * and {@link #write(AbstractBaseRecord, PageCursor, int, PagedFile)}. Records may be reused, which is why the instantiation * is separated from reading and writing. * - * @return a new record instance, usable in {@link #read(AbstractBaseRecord, PageCursor, RecordLoad, int)} - * and {@link #write(AbstractBaseRecord, PageCursor)}. + * @return a new record instance, usable in {@link #read(AbstractBaseRecord, PageCursor, RecordLoad, int, PagedFile)} + * and {@link #write(AbstractBaseRecord, PageCursor, int, PagedFile)}. */ RECORD newRecord(); @@ -82,16 +86,40 @@ public interface RecordFormat * See {@link RecordStore#getRecord(long, AbstractBaseRecord, RecordLoad)} for more information. * @param recordSize size of records of this format. This is passed in like this since not all formats * know the record size in advance, but may be read from store header when opening the store. + * @param storeFile {@link PagedFile} to get additional {@link PageCursor} from if needed. + * @throws IOException on error reading. */ - void read( RECORD record, PageCursor cursor, RecordLoad mode, int recordSize ); + void read( RECORD record, PageCursor cursor, RecordLoad mode, int recordSize, PagedFile storeFile ) + throws IOException; + + /** + * For lack of better term this is to be called when all changes about a record has been gathered + * and before it's time to convert into a command. The original reason for introducing this is the + * thing with record units, where we need to know whether or not a record will span two units + * before even writing to the log as a command. The format is the pluggable entity which knows + * about the format and therefore the potential length of it and can update the given record with + * additional information which needs to be written to the command, carried back inside the record + * itself. + * + * @param record record to prepare, potentially updating it with more information before converting + * into a command. + * @param recordSize size of each record. + * @param idSequence source of new ids if such are required be generated. + */ + void prepare( RECORD record, int recordSize, IdSequence idSequence ); /** * Writes record contents to the {@code cursor} in the format specified by this implementation. * * @param record containing data to write. * @param cursor {@link PageCursor} to write the record data into. + * @param recordSize size of records of this format. This is passed in like this since not all formats + * know the record size in advance, but may be read from store header when opening the store. + * @param storeFile {@link PagedFile} to get additional {@link PageCursor} from if needed. + * @throws IOException on error writing. */ - void write( RECORD record, PageCursor cursor ); + void write( RECORD record, PageCursor cursor, int recordSize, PagedFile storeFile ) + throws IOException; /** * @param record to obtain "next" reference from. diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/DynamicRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/DynamicRecordFormat.java index 7a08ccd9bc561..3d23373514917 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/DynamicRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/DynamicRecordFormat.java @@ -20,6 +20,7 @@ package org.neo4j.kernel.impl.store.format.lowlimit; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.format.BaseRecordFormat; import org.neo4j.kernel.impl.store.record.DynamicRecord; import org.neo4j.kernel.impl.store.record.Record; @@ -27,9 +28,10 @@ import static java.lang.String.format; +import static org.neo4j.kernel.impl.store.record.DynamicRecord.NO_DATA; + public class DynamicRecordFormat extends BaseRecordFormat { - public static final byte[] NO_DATA = new byte[0]; // (in_use+next high)(1 byte)+nr_of_bytes(3 bytes)+next_block(int) public static final int RECORD_HEADER_SIZE = 1 + 3 + 4; // = 8 @@ -45,7 +47,7 @@ public DynamicRecord newRecord() } @Override - public void read( DynamicRecord record, PageCursor cursor, RecordLoad mode, int recordSize ) + public void read( DynamicRecord record, PageCursor cursor, RecordLoad mode, int recordSize, PagedFile storeFile ) { /* * First 4b @@ -78,25 +80,30 @@ public void read( DynamicRecord record, PageCursor cursor, RecordLoad mode, int record.getNextBlock(), record.getLength(), dataSize ) ); } - if ( record.getLength() == 0 ) // don't go though the trouble of acquiring the window if we would read nothing - { - record.setData( NO_DATA ); - return; - } + readData( record, cursor ); + } + } - int len = record.getLength(); - byte[] data = record.getData(); - if ( data == null || data.length != len ) - { - data = new byte[len]; - } - cursor.getBytes( data ); - record.setData( data ); + public static void readData( DynamicRecord record, PageCursor cursor ) + { + if ( record.getLength() == 0 ) // don't go though the trouble of acquiring the window if we would read nothing + { + record.setData( NO_DATA ); + return; + } + + int len = record.getLength(); + byte[] data = record.getData(); + if ( data == null || data.length != len ) + { + data = new byte[len]; } + cursor.getBytes( data ); + record.setData( data ); } @Override - public void write( DynamicRecord record, PageCursor cursor ) + public void write( DynamicRecord record, PageCursor cursor, int recordSize, PagedFile storeFile ) { if ( record.inUse() ) { diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/NodeRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/NodeRecordFormat.java index f5922500b9c0d..2cbdc3be87758 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/NodeRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/NodeRecordFormat.java @@ -20,6 +20,7 @@ package org.neo4j.kernel.impl.store.format.lowlimit; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.format.BaseOneByteHeaderRecordFormat; import org.neo4j.kernel.impl.store.format.BaseRecordFormat; import org.neo4j.kernel.impl.store.record.NodeRecord; @@ -42,13 +43,13 @@ public NodeRecord newRecord() } @Override - public void doRead( NodeRecord record, PageCursor cursor, int recordSize, long inUseByte, boolean inUse ) + public void doRead( NodeRecord record, PageCursor cursor, int recordSize, PagedFile storeFile, long headerByte, boolean inUse ) { long nextRel = cursor.getUnsignedInt(); long nextProp = cursor.getUnsignedInt(); - long relModifier = (inUseByte & 0xEL) << 31; - long propModifier = (inUseByte & 0xF0L) << 28; + long relModifier = (headerByte & 0xEL) << 31; + long propModifier = (headerByte & 0xF0L) << 28; long lsbLabels = cursor.getUnsignedInt(); long hsbLabels = cursor.getByte() & 0xFF; // so that a negative byte won't fill the "extended" bits with ones. @@ -62,7 +63,7 @@ public void doRead( NodeRecord record, PageCursor cursor, int recordSize, long i } @Override - public void doWrite( NodeRecord record, PageCursor cursor ) + public void doWrite( NodeRecord record, PageCursor cursor, int recordSize, PagedFile storeFile ) { long nextRel = record.getNextRel(); long nextProp = record.getNextProp(); diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/PropertyRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/PropertyRecordFormat.java index 7930d88aa85b1..714bfa75ca55e 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/PropertyRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/PropertyRecordFormat.java @@ -20,6 +20,7 @@ package org.neo4j.kernel.impl.store.format.lowlimit; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.PropertyType; import org.neo4j.kernel.impl.store.format.BaseRecordFormat; import org.neo4j.kernel.impl.store.record.PropertyBlock; @@ -50,7 +51,7 @@ public PropertyRecord newRecord() } @Override - public void read( PropertyRecord record, PageCursor cursor, RecordLoad mode, int recordSize ) + public void read( PropertyRecord record, PageCursor cursor, RecordLoad mode, int recordSize, PagedFile storeFile ) { int offsetAtBeginning = cursor.getOffset(); @@ -86,7 +87,7 @@ public void read( PropertyRecord record, PageCursor cursor, RecordLoad mode, int } @Override - public void write( PropertyRecord record, PageCursor cursor ) + public void write( PropertyRecord record, PageCursor cursor, int recordSize, PagedFile storeFile ) { if ( record.inUse() ) { diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipGroupRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipGroupRecordFormat.java index 19acc6a8ca3a3..4adc1b473b9e0 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipGroupRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipGroupRecordFormat.java @@ -20,6 +20,7 @@ package org.neo4j.kernel.impl.store.format.lowlimit; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.format.BaseOneByteHeaderRecordFormat; import org.neo4j.kernel.impl.store.format.BaseRecordFormat; import org.neo4j.kernel.impl.store.record.Record; @@ -42,7 +43,7 @@ public RelationshipGroupRecordFormat() } @Override - public void doRead( RelationshipGroupRecord record, PageCursor cursor, int recordSize, long inUseByte, boolean inUse ) + public void doRead( RelationshipGroupRecord record, PageCursor cursor, int recordSize, PagedFile storeFile, long headerByte, boolean inUse ) { // [ , x] in use // [ ,xxx ] high next id bits @@ -58,8 +59,8 @@ public void doRead( RelationshipGroupRecord record, PageCursor cursor, int recor long nextLoopLowBits = cursor.getUnsignedInt(); long owningNode = cursor.getUnsignedInt() | (((long)cursor.getByte()) << 32); - long nextMod = (inUseByte & 0xE) << 31; - long nextOutMod = (inUseByte & 0x70) << 28; + long nextMod = (headerByte & 0xE) << 31; + long nextOutMod = (headerByte & 0x70) << 28; long nextInMod = (highByte & 0xE) << 31; long nextLoopMod = (highByte & 0x70) << 28; @@ -72,7 +73,7 @@ public void doRead( RelationshipGroupRecord record, PageCursor cursor, int recor } @Override - public void doWrite( RelationshipGroupRecord record, PageCursor cursor ) + public void doWrite( RelationshipGroupRecord record, PageCursor cursor, int recordSize, PagedFile storeFile ) { long nextMod = record.getNext() == Record.NO_NEXT_RELATIONSHIP.intValue() ? 0 : (record.getNext() & 0x700000000L) >> 31; long nextOutMod = record.getFirstOut() == Record.NO_NEXT_RELATIONSHIP.intValue() ? 0 : (record.getFirstOut() & 0x700000000L) >> 28; diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipRecordFormat.java index 73d63e86bc0e2..9dc3f2a13e271 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/RelationshipRecordFormat.java @@ -20,6 +20,7 @@ package org.neo4j.kernel.impl.store.format.lowlimit; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.format.BaseOneByteHeaderRecordFormat; import org.neo4j.kernel.impl.store.format.BaseRecordFormat; import org.neo4j.kernel.impl.store.record.Record; @@ -45,13 +46,13 @@ public RelationshipRecord newRecord() } @Override - public void doRead( RelationshipRecord record, PageCursor cursor, int recordSize, long inUseByte, boolean inUse ) + public void doRead( RelationshipRecord record, PageCursor cursor, int recordSize, PagedFile storeFile, long headerByte, boolean inUse ) { // [ , x] in use flag // [ ,xxx ] first node high order bits // [xxxx, ] next prop high order bits long firstNode = cursor.getUnsignedInt(); - long firstNodeMod = (inUseByte & 0xEL) << 31; + long firstNodeMod = (headerByte & 0xEL) << 31; long secondNode = cursor.getUnsignedInt(); @@ -78,7 +79,7 @@ public void doRead( RelationshipRecord record, PageCursor cursor, int recordSize long secondNextRelMod = (typeInt & 0x70000L) << 16; long nextProp = cursor.getUnsignedInt(); - long nextPropMod = (inUseByte & 0xF0L) << 28; + long nextPropMod = (headerByte & 0xF0L) << 28; byte extraByte = cursor.getByte(); @@ -96,7 +97,7 @@ public void doRead( RelationshipRecord record, PageCursor cursor, int recordSize } @Override - public void doWrite( RelationshipRecord record, PageCursor cursor ) + public void doWrite( RelationshipRecord record, PageCursor cursor, int recordSize, PagedFile storeFile ) { long firstNode = record.getFirstNode(); short firstNodeMod = (short)((firstNode & 0x700000000L) >> 31); diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/TokenRecordFormat.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/TokenRecordFormat.java index cb571206b5164..857f7f765161e 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/TokenRecordFormat.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/format/lowlimit/TokenRecordFormat.java @@ -20,6 +20,7 @@ package org.neo4j.kernel.impl.store.format.lowlimit; import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.format.BaseRecordFormat; import org.neo4j.kernel.impl.store.record.Record; import org.neo4j.kernel.impl.store.record.RecordLoad; @@ -35,7 +36,7 @@ protected TokenRecordFormat( int recordSize ) } @Override - public void read( RECORD record, PageCursor cursor, RecordLoad mode, int recordSize ) + public void read( RECORD record, PageCursor cursor, RecordLoad mode, int recordSize, PagedFile storeFile ) { byte inUseByte = cursor.getByte(); boolean inUse = isInUse( inUseByte ); @@ -51,7 +52,7 @@ protected void readRecordData( PageCursor cursor, RECORD record, boolean inUse ) } @Override - public void write( RECORD record, PageCursor cursor ) + public void write( RECORD record, PageCursor cursor, int recordSize, PagedFile storeFile ) { if ( record.inUse() ) { diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/AbstractBaseRecord.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/AbstractBaseRecord.java index 338512df3f4e8..384d29f76ba74 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/AbstractBaseRecord.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/AbstractBaseRecord.java @@ -31,6 +31,9 @@ public abstract class AbstractBaseRecord implements CloneableInPublic { private long id; + // Used for the "record unit" feature where one logical record may span two physical records, + // as to still keep low and fixed record size, but support occasionally bigger records. + private long secondaryId; private boolean inUse; private boolean created; @@ -44,6 +47,7 @@ protected AbstractBaseRecord initialize( boolean inUse ) { this.inUse = inUse; this.created = false; + this.secondaryId = -1; return this; } @@ -57,6 +61,7 @@ public void clear() { inUse = false; created = false; + secondaryId = -1; } public long getId() @@ -74,6 +79,31 @@ public void setId( long id ) this.id = id; } + /** + * Sets a secondary record unit ID for this record. If this is set to something other than {@code -1} + * then {@link #requiresTwoUnits()} will return {@code true}. + */ + public void setSecondaryId( long id ) + { + this.secondaryId = id; + } + + /** + * @return secondary record unit ID set by {@link #setSecondaryId(long)}. + */ + public long getSecondaryId() + { + return this.secondaryId; + } + + /** + * @return whether or not a secondary record unit ID has been assigned. + */ + public boolean requiresTwoUnits() + { + return this.secondaryId != -1; + } + public final boolean inUse() { return inUse; diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/DynamicRecord.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/DynamicRecord.java index 02ef21321f5f1..6630131ed96f7 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/DynamicRecord.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/DynamicRecord.java @@ -24,7 +24,7 @@ public class DynamicRecord extends AbstractBaseRecord { - private static final byte[] NO_DATA = new byte[0]; + public static final byte[] NO_DATA = new byte[0]; private static final int MAX_BYTES_IN_TO_STRING = 8, MAX_CHARS_IN_TO_STRING = 16; private byte[] data; diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/PropertyRecord.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/PropertyRecord.java index 7b9cf7fe6ac09..3ca65003c1682 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/PropertyRecord.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/store/record/PropertyRecord.java @@ -160,6 +160,7 @@ public int size() public int numberOfProperties() { + ensureBlocksLoaded(); return blockRecordsCursor; } diff --git a/community/kernel/src/main/java/org/neo4j/unsafe/batchinsert/BatchInserterImpl.java b/community/kernel/src/main/java/org/neo4j/unsafe/batchinsert/BatchInserterImpl.java index 458b83a3ec18c..da1674c435970 100644 --- a/community/kernel/src/main/java/org/neo4j/unsafe/batchinsert/BatchInserterImpl.java +++ b/community/kernel/src/main/java/org/neo4j/unsafe/batchinsert/BatchInserterImpl.java @@ -112,7 +112,7 @@ import org.neo4j.kernel.impl.store.StoreFactory; import org.neo4j.kernel.impl.store.UnderlyingStorageException; import org.neo4j.kernel.impl.store.counts.CountsTracker; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; +import org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector; import org.neo4j.kernel.impl.store.id.DefaultIdGeneratorFactory; import org.neo4j.kernel.impl.store.id.IdGeneratorFactory; import org.neo4j.kernel.impl.store.id.IdGeneratorImpl; @@ -271,7 +271,7 @@ public Label apply( long from ) this.idGeneratorFactory = new DefaultIdGeneratorFactory( fileSystem ); StoreFactory sf = new StoreFactory( this.storeDir, config, idGeneratorFactory, pageCache, fileSystem, - logService.getInternalLogProvider(), LowLimit.RECORD_FORMATS ); + logService.getInternalLogProvider(), InternalRecordFormatSelector.select() ); if ( dump ) { diff --git a/community/kernel/src/main/java/org/neo4j/unsafe/impl/batchimport/store/BatchingIdSequence.java b/community/kernel/src/main/java/org/neo4j/unsafe/impl/batchimport/store/BatchingIdSequence.java index f137c06bb1a34..c1e0ce35aeb37 100644 --- a/community/kernel/src/main/java/org/neo4j/unsafe/impl/batchimport/store/BatchingIdSequence.java +++ b/community/kernel/src/main/java/org/neo4j/unsafe/impl/batchimport/store/BatchingIdSequence.java @@ -27,8 +27,20 @@ */ public class BatchingIdSequence implements IdSequence { + private final long startId; private long nextId = 0; + public BatchingIdSequence() + { + this( 0 ); + } + + public BatchingIdSequence( long startId ) + { + this.startId = startId; + this.nextId = startId; + } + @Override public long nextId() { @@ -42,6 +54,6 @@ public long nextId() public void reset() { - nextId = 0; + nextId = startId; } } diff --git a/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java b/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java index 0fe2abec47662..e5389b83b8046 100644 --- a/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java +++ b/community/kernel/src/test/java/org/neo4j/graphdb/IndexingAcceptanceTest.java @@ -21,6 +21,7 @@ import org.junit.Rule; import org.junit.Test; + import java.util.Map; import java.util.concurrent.TimeUnit; @@ -41,6 +42,7 @@ import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; + import static org.neo4j.graphdb.Neo4jMatchers.containsOnly; import static org.neo4j.graphdb.Neo4jMatchers.findNodesByLabelAndProperty; import static org.neo4j.graphdb.Neo4jMatchers.hasProperty; diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngineRule.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngineRule.java index 42d3a9cfd696f..70ffbe264912d 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngineRule.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngineRule.java @@ -24,7 +24,7 @@ import org.neo4j.helpers.collection.Iterables; import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.io.pagecache.PageCache; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; +import org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector; import org.neo4j.kernel.impl.store.id.IdGeneratorFactory; import org.neo4j.kernel.KernelEventHandlers; import org.neo4j.kernel.api.TokenNameLookup; @@ -101,7 +101,7 @@ private RecordStorageEngine get( FileSystemAbstraction fs, PageCache pageCache, scheduler, mock( TokenNameLookup.class ), new ReentrantLockService(), schemaIndexProvider, IndexingService.NO_MONITOR, databaseHealth, labelScanStoreProvider, legacyIndexProviderLookup, indexConfigStore, - new SynchronizedArrayIdOrderingQueue( 20 ), LowLimit.RECORD_FORMATS ) ); + new SynchronizedArrayIdOrderingQueue( 20 ), InternalRecordFormatSelector.select() ) ); } @Override diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/LabelTokenStoreTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/LabelTokenStoreTest.java index c3ce15bb64845..eb4a671b83636 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/LabelTokenStoreTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/LabelTokenStoreTest.java @@ -27,7 +27,6 @@ import org.neo4j.io.pagecache.PageCache; import org.neo4j.io.pagecache.PageCursor; import org.neo4j.io.pagecache.PagedFile; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; import org.neo4j.kernel.impl.store.id.IdGeneratorFactory; import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.impl.store.record.LabelTokenRecord; @@ -39,6 +38,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector.select; import static org.neo4j.kernel.impl.store.record.RecordLoad.FORCE; import static org.neo4j.kernel.impl.store.record.RecordLoad.NORMAL; @@ -74,7 +74,7 @@ class UnusedLabelTokenStore extends LabelTokenStore public UnusedLabelTokenStore() throws IOException { super( file, config, generatorFactory, cache, logProvider, dynamicStringStore, - LowLimit.RECORD_FORMATS.labelToken(), LowLimit.STORE_VERSION ); + select().labelToken(), select().storeVersion() ); storeFile = mock( PagedFile.class ); when( storeFile.io( any( Long.class ), any( Integer.class ) ) ).thenReturn( pageCursor ); diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/PropertyStoreTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/PropertyStoreTest.java index f2d91e2a2cda2..9f3e98c0a8ecc 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/PropertyStoreTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/PropertyStoreTest.java @@ -33,7 +33,6 @@ import org.neo4j.io.pagecache.PageCache; import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.impl.core.JumpingIdGeneratorFactory; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; import org.neo4j.kernel.impl.store.record.DynamicRecord; import org.neo4j.kernel.impl.store.record.PropertyBlock; import org.neo4j.kernel.impl.store.record.PropertyKeyTokenRecord; @@ -49,6 +48,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector.select; import static org.neo4j.kernel.impl.store.record.RecordLoad.FORCE; public class PropertyStoreTest @@ -83,7 +83,7 @@ public void shouldWriteOutTheDynamicChainBeforeUpdatingThePropertyRecord() throw final PropertyStore store = new PropertyStore( path, config, new JumpingIdGeneratorFactory( 1 ), pageCache, NullLogProvider.getInstance(), stringPropertyStore, mock( PropertyKeyTokenStore.class ), mock( DynamicArrayStore.class ), - LowLimit.RECORD_FORMATS.property(), LowLimit.STORE_VERSION ); + select().property(), select().storeVersion() ); store.initialise( true ); try diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/TestIdGeneratorRebuilding.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/TestIdGeneratorRebuilding.java index d94bdfd534012..c48c0ffc705a6 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/TestIdGeneratorRebuilding.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/TestIdGeneratorRebuilding.java @@ -32,7 +32,6 @@ import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.graphdb.mockfs.EphemeralFileSystemAbstraction; import org.neo4j.helpers.collection.MapUtil; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; import org.neo4j.kernel.impl.store.id.DefaultIdGeneratorFactory; import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.impl.AbstractNeo4jTestCase; @@ -47,6 +46,8 @@ import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; +import static org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector.select; + public class TestIdGeneratorRebuilding { @ClassRule @@ -80,7 +81,7 @@ public void verifyFixedSizeStoresCanRebuildIdGeneratorSlowly() throws IOExceptio DynamicArrayStore labelStore = mock( DynamicArrayStore.class ); NodeStore store = new NodeStore( storeFile, config, new DefaultIdGeneratorFactory( fs ), pageCacheRule.getPageCache( fs ), NullLogProvider.getInstance(), labelStore, - LowLimit.RECORD_FORMATS.node(), LowLimit.STORE_VERSION ); + select().node(), select().storeVersion() ); store.initialise( true ); store.makeStoreOk(); @@ -185,7 +186,7 @@ public void rebuildingIdGeneratorMustNotMissOutOnFreeRecordsAtEndOfFilePage() th DynamicArrayStore labelStore = mock( DynamicArrayStore.class ); NodeStore store = new NodeStore( storeFile, config, new DefaultIdGeneratorFactory( fs ), pageCacheRule.getPageCache( fs ), NullLogProvider.getInstance(), labelStore, - LowLimit.RECORD_FORMATS.node(), LowLimit.STORE_VERSION ); + select().node(), select().storeVersion() ); store.initialise( true ); store.makeStoreOk(); diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/UpgradeStoreIT.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/UpgradeStoreIT.java index d35d40767d7c2..85066f1575904 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/UpgradeStoreIT.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/UpgradeStoreIT.java @@ -65,6 +65,7 @@ import static org.neo4j.helpers.collection.IteratorUtil.first; import static org.neo4j.helpers.collection.MapUtil.stringMap; import static org.neo4j.kernel.impl.AbstractNeo4jTestCase.deleteFileOrDirectory; +import static org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector.select; @Ignore public class UpgradeStoreIT @@ -380,7 +381,7 @@ public RelationshipTypeTokenStoreWithOneOlderVersion( PageCache pageCache ) { super( fileName, config, new NoLimitIdGeneratorFactory( fs ), pageCache, NullLogProvider.getInstance(), - stringStore, LowLimit.RECORD_FORMATS.relationshipTypeToken(), LowLimit.STORE_VERSION ); + stringStore, select().relationshipTypeToken(), select().storeVersion() ); } @Override diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/FullyCoveringRecordKeys.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/FullyCoveringRecordKeys.java new file mode 100644 index 0000000000000..da00c53ad569a --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/FullyCoveringRecordKeys.java @@ -0,0 +1,193 @@ +/* + * 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.kernel.impl.store.format; + +import java.util.Iterator; + +import org.neo4j.kernel.impl.store.record.DynamicRecord; +import org.neo4j.kernel.impl.store.record.LabelTokenRecord; +import org.neo4j.kernel.impl.store.record.NodeRecord; +import org.neo4j.kernel.impl.store.record.PropertyBlock; +import org.neo4j.kernel.impl.store.record.PropertyKeyTokenRecord; +import org.neo4j.kernel.impl.store.record.PropertyRecord; +import org.neo4j.kernel.impl.store.record.RelationshipGroupRecord; +import org.neo4j.kernel.impl.store.record.RelationshipRecord; +import org.neo4j.kernel.impl.store.record.RelationshipTypeTokenRecord; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +class FullyCoveringRecordKeys implements RecordKeys +{ + public static final RecordKeys INSTANCE = new FullyCoveringRecordKeys(); + + @Override + public RecordKey node() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( NodeRecord written, NodeRecord read ) + { + assertEquals( written.getNextProp(), read.getNextProp() ); + assertEquals( written.getNextRel(), read.getNextRel() ); + assertEquals( written.getLabelField(), read.getLabelField() ); + assertEquals( written.isDense(), read.isDense() ); + } + }; + } + + @Override + public RecordKey relationship() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( RelationshipRecord written, RelationshipRecord read ) + { + assertEquals( written.getNextProp(), read.getNextProp() ); + assertEquals( written.getFirstNode(), read.getFirstNode() ); + assertEquals( written.getSecondNode(), read.getSecondNode() ); + assertEquals( written.getType(), read.getType() ); + assertEquals( written.getFirstPrevRel(), read.getFirstPrevRel() ); + assertEquals( written.getFirstNextRel(), read.getFirstNextRel() ); + assertEquals( written.getSecondPrevRel(), read.getSecondPrevRel() ); + assertEquals( written.getSecondNextRel(), read.getSecondNextRel() ); + } + }; + } + + @Override + public RecordKey property() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( PropertyRecord written, PropertyRecord read ) + { + assertEquals( written.getPrevProp(), read.getPrevProp() ); + assertEquals( written.getNextProp(), read.getNextProp() ); + assertEquals( written.isNodeSet(), read.isNodeSet() ); + if ( written.isNodeSet() ) + { + assertEquals( written.getNodeId(), read.getNodeId() ); + } + else + { + assertEquals( written.getRelId(), read.getRelId() ); + } + assertEquals( written.numberOfProperties(), read.numberOfProperties() ); + Iterator writtenBlocks = written.iterator(); + Iterator readBlocks = read.iterator(); + while ( writtenBlocks.hasNext() ) + { + assertTrue( readBlocks.hasNext() ); + assertBlocksEquals( writtenBlocks.next(), readBlocks.next() ); + } + } + + private void assertBlocksEquals( PropertyBlock written, PropertyBlock read ) + { + assertEquals( written.getKeyIndexId(), read.getKeyIndexId() ); + assertEquals( written.getSize(), read.getSize() ); + assertTrue( written.hasSameContentsAs( read ) ); + assertArrayEquals( written.getValueBlocks(), read.getValueBlocks() ); + } + }; + } + + @Override + public RecordKey relationshipGroup() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( RelationshipGroupRecord written, RelationshipGroupRecord read ) + { + assertEquals( written.getType(), read.getType() ); + assertEquals( written.getFirstOut(), read.getFirstOut() ); + assertEquals( written.getFirstIn(), read.getFirstIn() ); + assertEquals( written.getFirstLoop(), read.getFirstLoop() ); + assertEquals( written.getNext(), read.getNext() ); + assertEquals( written.getOwningNode(), read.getOwningNode() ); + } + }; + } + + @Override + public RecordKey relationshipTypeToken() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( RelationshipTypeTokenRecord written, RelationshipTypeTokenRecord read ) + { + assertEquals( written.getNameId(), read.getNameId() ); + } + }; + } + + @Override + public RecordKey propertyKeyToken() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( PropertyKeyTokenRecord written, PropertyKeyTokenRecord read ) + { + assertEquals( written.getNameId(), read.getNameId() ); + assertEquals( written.getPropertyCount(), read.getPropertyCount() ); + } + }; + } + + @Override + public RecordKey labelToken() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( LabelTokenRecord written, LabelTokenRecord read ) + { + assertEquals( written.getNameId(), read.getNameId() ); + } + }; + } + + @Override + public RecordKey dynamic() + { + return new RecordKey() + { + @Override + public void assertRecordsEquals( DynamicRecord written, DynamicRecord read ) + { + // Don't assert type, since that's read from the data, and the data in this test + // is randomly generated. Since we assert that the data is the same then the type + // is also correct. + assertEquals( written.getLength(), read.getLength() ); + assertEquals( written.getNextBlock(), read.getNextBlock() ); + assertArrayEquals( written.getData(), read.getData() ); + assertEquals( written.isStartRecord(), read.isStartRecord() ); + } + }; + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LimitedRecordGenerators.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LimitedRecordGenerators.java new file mode 100644 index 0000000000000..1befa2d72b0e6 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LimitedRecordGenerators.java @@ -0,0 +1,189 @@ +/* + * 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.kernel.impl.store.format; + +import org.neo4j.kernel.impl.store.PropertyStore; +import org.neo4j.kernel.impl.store.PropertyType; +import org.neo4j.kernel.impl.store.record.DynamicRecord; +import org.neo4j.kernel.impl.store.record.LabelTokenRecord; +import org.neo4j.kernel.impl.store.record.NodeRecord; +import org.neo4j.kernel.impl.store.record.PropertyBlock; +import org.neo4j.kernel.impl.store.record.PropertyKeyTokenRecord; +import org.neo4j.kernel.impl.store.record.PropertyRecord; +import org.neo4j.kernel.impl.store.record.RelationshipGroupRecord; +import org.neo4j.kernel.impl.store.record.RelationshipRecord; +import org.neo4j.kernel.impl.store.record.RelationshipTypeTokenRecord; +import org.neo4j.test.RandomRule; + +import static java.lang.Math.abs; + +class LimitedRecordGenerators implements RecordGenerators +{ + static final long NULL = -1; + + private final RandomRule random; + private final int entityBits; + private final int propertyBits; + private final int nodeLabelBits; + private final int tokenBits; + private final long nullValue; + private final float fractionNullValues; + + public LimitedRecordGenerators( RandomRule random, int entityBits, int propertyBits, int nodeLabelBits, + int tokenBits, long nullValue ) + { + this( random, entityBits, propertyBits, nodeLabelBits, tokenBits, nullValue, 0.2f ); + } + + public LimitedRecordGenerators( RandomRule random, int entityBits, int propertyBits, int nodeLabelBits, + int tokenBits, long nullValue, float fractionNullValues ) + { + this.random = random; + this.entityBits = entityBits; + this.propertyBits = propertyBits; + this.nodeLabelBits = nodeLabelBits; + this.tokenBits = tokenBits; + this.nullValue = nullValue; + this.fractionNullValues = fractionNullValues; + } + + @Override + public Generator relationshipTypeToken() + { + return (recordSize, format) -> new RelationshipTypeTokenRecord( 0 ).initialize( random.nextBoolean(), + randomInt( tokenBits ) ); + } + + @Override + public Generator relationshipGroup() + { + return (recordSize, format) -> new RelationshipGroupRecord( 0 ).initialize( random.nextBoolean(), + randomInt( tokenBits ), + randomLongOrOccasionallyNull( entityBits ), + randomLongOrOccasionallyNull( entityBits ), + randomLongOrOccasionallyNull( entityBits ), + randomLongOrOccasionallyNull( entityBits ), + randomLongOrOccasionallyNull( entityBits ) ); + } + + @Override + public Generator relationship() + { + return (recordSize, format) -> new RelationshipRecord( 0 ).initialize( random.nextBoolean(), + randomLongOrOccasionallyNull( propertyBits ), + random.nextLong( entityBits ), random.nextLong( entityBits ), randomInt( tokenBits ), + randomLongOrOccasionallyNull( entityBits ), randomLongOrOccasionallyNull( entityBits ), + randomLongOrOccasionallyNull( entityBits ), randomLongOrOccasionallyNull( entityBits ), + random.nextBoolean(), random.nextBoolean() ); + } + + @Override + public Generator propertyKeyToken() + { + return (recordSize, format) -> new PropertyKeyTokenRecord( 0 ).initialize( random.nextBoolean(), + random.nextInt( tokenBits ), abs( random.nextInt() ) ); + } + + @Override + public Generator property() + { + return (recordSize, format) -> { + PropertyRecord record = new PropertyRecord( 0 ); + int maxProperties = random.intBetween( 1, 4 ); + StandaloneDynamicRecordAllocator stringAllocator = new StandaloneDynamicRecordAllocator(); + StandaloneDynamicRecordAllocator arrayAllocator = new StandaloneDynamicRecordAllocator(); + record.setInUse( true ); + int blocksOccupied = 0; + for ( int i = 0; i < maxProperties && blocksOccupied < 4; ) + { + PropertyBlock block = new PropertyBlock(); + // Dynamic records will not be written and read by the property record format, + // that happens in the store where it delegates to a "sub" store. + PropertyStore.encodeValue( block, random.nextInt( tokenBits ), random.propertyValue(), + stringAllocator, arrayAllocator ); + int tentativeBlocksWithThisOne = blocksOccupied + block.getValueBlocks().length; + if ( tentativeBlocksWithThisOne <= 4 ) + { + record.addPropertyBlock( block ); + blocksOccupied = tentativeBlocksWithThisOne; + } + } + record.setPrevProp( randomLongOrOccasionallyNull( propertyBits ) ); + record.setNextProp( randomLongOrOccasionallyNull( propertyBits ) ); + return record; + }; + } + + @Override + public Generator node() + { + return (recordSize, format) -> new NodeRecord( 0 ).initialize( + random.nextBoolean(), randomLongOrOccasionallyNull( propertyBits ), random.nextBoolean(), + randomLongOrOccasionallyNull( entityBits ), + randomLongOrOccasionallyNull( nodeLabelBits, 0 ) ); + } + + @Override + public Generator labelToken() + { + return (recordSize, format) -> new LabelTokenRecord( 0 ).initialize( + random.nextBoolean(), random.nextInt( tokenBits ) ); + } + + @Override + public Generator dynamic() + { + return (recordSize, format) -> { + int dataSize = recordSize - format.getRecordHeaderSize(); + int length = random.nextBoolean() ? dataSize : random.nextInt( dataSize ); + long next = length == dataSize ? randomLong( propertyBits ) : nullValue; + DynamicRecord record = new DynamicRecord( 1 ).initialize( random.nextBoolean(), + random.nextBoolean(), next, random.nextInt( PropertyType.values().length ), length ); + byte[] data = new byte[record.getLength()]; + random.nextBytes( data ); + record.setData( data ); + return record; + }; + } + + private int randomInt( int maxBits ) + { + int bits = random.nextInt( maxBits + 1 ); + int max = 1 << bits; + return random.nextInt( max ); + } + + private long randomLong( int maxBits ) + { + int bits = random.nextInt( maxBits + 1 ); + long max = 1L << bits; + return random.nextLong( max ); + } + + private long randomLongOrOccasionallyNull( int maxBits ) + { + return randomLongOrOccasionallyNull( maxBits, NULL ); + } + + private long randomLongOrOccasionallyNull( int maxBits, long nullValue ) + { + return random.nextFloat() < fractionNullValues ? nullValue : randomLong( maxBits ); + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LowLimitRecordFormatTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LowLimitRecordFormatTest.java new file mode 100644 index 0000000000000..1e5795eebcedb --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/LowLimitRecordFormatTest.java @@ -0,0 +1,35 @@ +/* + * 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.kernel.impl.store.format; + +import org.neo4j.kernel.impl.store.format.LimitedRecordGenerators; +import org.neo4j.kernel.impl.store.format.RecordFormatTest; +import org.neo4j.kernel.impl.store.format.RecordGenerators; +import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; + +public class LowLimitRecordFormatTest extends RecordFormatTest +{ + private static final RecordGenerators LOW_LIMITS = new LimitedRecordGenerators( random, 35, 36, 40, 16, NULL ); + + public LowLimitRecordFormatTest() + { + super( LowLimit.RECORD_FORMATS, LOW_LIMITS ); + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordBoundaryCheckingPagedFile.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordBoundaryCheckingPagedFile.java new file mode 100644 index 0000000000000..3bd7e7593e091 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordBoundaryCheckingPagedFile.java @@ -0,0 +1,368 @@ +/* + * 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.kernel.impl.store.format; + +import java.io.File; +import java.io.IOException; + +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; + +public class RecordBoundaryCheckingPagedFile implements PagedFile +{ + private final PagedFile actual; + private final int recordSize; + private int ioCalls; + private int nextCalls; + private int setOffsetCalls; + private int unusedBytes; + private int retries; + + public RecordBoundaryCheckingPagedFile( PagedFile actual, int enforcedRecordSize ) + { + this.actual = actual; + this.recordSize = enforcedRecordSize; + } + + @Override + public int pageSize() + { + return actual.pageSize(); + } + + @Override + public PageCursor io( long pageId, int pf_flags ) throws IOException + { + ioCalls++; + return new RecordBoundaryCheckingPageCursor( actual.io( pageId, pf_flags ) ); + } + + @Override + public long getLastPageId() throws IOException + { + return actual.getLastPageId(); + } + + @Override + public void flushAndForce() throws IOException + { + actual.flushAndForce(); + } + + @Override + public void close() throws IOException + { + actual.close(); + } + + public int ioCalls() + { + return ioCalls; + } + + public int nextCalls() + { + return nextCalls; + } + + public int unusedBytes() + { + return unusedBytes; + } + + public void resetMeasurements() + { + ioCalls = unusedBytes = nextCalls = 0; + } + + class RecordBoundaryCheckingPageCursor implements PageCursor + { + private final PageCursor actual; + private int start = -10_000; + private boolean shouldReport; + + RecordBoundaryCheckingPageCursor( PageCursor actual ) + { + this.actual = actual; + } + + private void checkBoundary( int size ) + { + shouldReport = true; // since the cursor is moving + if ( size > recordSize ) + { + throw new IllegalStateException( "Tried to go beyond record boundaries. We seem to be on the " + + (nextCalls == 1 ? "first" : "second") + " page start offset:" + start + " record size:" + + recordSize + " and tried to go to " + size ); + } + } + + private void checkRelativeBoundary( int add ) + { + checkBoundary( getOffset() - start + add ); + } + + private void checkAbsoluteBoundary( int offset ) + { + checkBoundary( offset - start ); + } + + @Override + public byte getByte() + { + checkRelativeBoundary( Byte.BYTES ); + return actual.getByte(); + } + + @Override + public byte getByte( int offset ) + { + checkAbsoluteBoundary( Byte.BYTES ); + return actual.getByte( offset ); + } + + @Override + public void putByte( byte value ) + { + checkRelativeBoundary( Byte.BYTES ); + actual.putByte( value ); + } + + @Override + public void putByte( int offset, byte value ) + { + checkAbsoluteBoundary( Byte.BYTES ); + actual.putByte( offset, value ); + } + + @Override + public long getLong() + { + checkRelativeBoundary( Long.BYTES ); + return actual.getLong(); + } + + @Override + public long getLong( int offset ) + { + checkAbsoluteBoundary( Long.BYTES ); + return actual.getLong( offset ); + } + + @Override + public void putLong( long value ) + { + checkRelativeBoundary( Long.BYTES ); + actual.putLong( value ); + } + + @Override + public void putLong( int offset, long value ) + { + checkAbsoluteBoundary( Long.BYTES ); + actual.putLong( offset, value ); + } + + @Override + public int getInt() + { + checkRelativeBoundary( Integer.BYTES ); + return actual.getInt(); + } + + @Override + public int getInt( int offset ) + { + checkAbsoluteBoundary( Integer.BYTES ); + return actual.getInt( offset ); + } + + @Override + public void putInt( int value ) + { + checkRelativeBoundary( Integer.BYTES ); + actual.putInt( value ); + } + + @Override + public void putInt( int offset, int value ) + { + checkAbsoluteBoundary( Integer.BYTES ); + actual.putInt( offset, value ); + } + + @Override + public long getUnsignedInt() + { + checkRelativeBoundary( Integer.BYTES ); + return actual.getUnsignedInt(); + } + + @Override + public long getUnsignedInt( int offset ) + { + checkAbsoluteBoundary( Integer.BYTES ); + return actual.getUnsignedInt( offset ); + } + + @Override + public void getBytes( byte[] data ) + { + checkRelativeBoundary( data.length ); + actual.getBytes( data ); + } + + @Override + public void getBytes( byte[] data, int arrayOffset, int length ) + { + checkRelativeBoundary( length ); + actual.getBytes( data, arrayOffset, length ); + } + + @Override + public void putBytes( byte[] data ) + { + checkRelativeBoundary( data.length ); + actual.putBytes( data ); + } + + @Override + public void putBytes( byte[] data, int arrayOffset, int length ) + { + checkRelativeBoundary( length ); + actual.putBytes( data, arrayOffset, length ); + } + + @Override + public short getShort() + { + checkRelativeBoundary( Short.BYTES ); + return actual.getShort(); + } + + @Override + public short getShort( int offset ) + { + checkAbsoluteBoundary( Short.BYTES ); + return actual.getShort( offset ); + } + + @Override + public void putShort( short value ) + { + checkRelativeBoundary( Short.BYTES ); + actual.putShort( value ); + } + + @Override + public void putShort( int offset, short value ) + { + checkAbsoluteBoundary( Short.BYTES ); + actual.putShort( offset, value ); + } + + @Override + public void setOffset( int offset ) + { + if ( offset < start || offset >= start + recordSize ) + { + reportBeforeLeavingRecord(); + start = offset; + } + setOffsetCalls++; + actual.setOffset( offset ); + } + + private void reportBeforeLeavingRecord() + { + if ( shouldReport ) + { + int currentUnused = recordSize - (getOffset() - start); + unusedBytes += currentUnused; + shouldReport = false; + } + } + + @Override + public int getOffset() + { + return actual.getOffset(); + } + + @Override + public long getCurrentPageId() + { + return actual.getCurrentPageId(); + } + + @Override + public int getCurrentPageSize() + { + return actual.getCurrentPageSize(); + } + + @Override + public File getCurrentFile() + { + return actual.getCurrentFile(); + } + + @Override + public void rewind() + { + actual.rewind(); + start = getOffset(); + } + + @Override + public boolean next() throws IOException + { + reportBeforeLeavingRecord(); + nextCalls++; + return actual.next(); + } + + @Override + public boolean next( long pageId ) throws IOException + { + reportBeforeLeavingRecord(); + nextCalls++; + return actual.next( pageId ); + } + + @Override + public void close() + { + reportBeforeLeavingRecord(); + actual.close(); + } + + @Override + public boolean shouldRetry() throws IOException + { + boolean result = actual.shouldRetry(); + if ( result ) + { + retries++; + } + return result; + } + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordFormatTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordFormatTest.java index 6f85777893aec..c5c53e7172a34 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordFormatTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordFormatTest.java @@ -20,490 +20,246 @@ package org.neo4j.kernel.impl.store.format; import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; +import java.io.File; +import java.io.IOException; import java.util.function.Supplier; +import org.neo4j.helpers.Exceptions; +import org.neo4j.io.pagecache.PageCache; import org.neo4j.io.pagecache.PageCursor; -import org.neo4j.io.pagecache.StubPageCursor; -import org.neo4j.kernel.impl.store.DynamicRecordAllocator; +import org.neo4j.io.pagecache.PagedFile; import org.neo4j.kernel.impl.store.IntStoreHeader; -import org.neo4j.kernel.impl.store.PropertyStore; -import org.neo4j.kernel.impl.store.PropertyType; -import org.neo4j.kernel.impl.store.StoreHeader; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; +import org.neo4j.kernel.impl.store.format.RecordGenerators.Generator; import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; -import org.neo4j.kernel.impl.store.record.DynamicRecord; -import org.neo4j.kernel.impl.store.record.LabelTokenRecord; -import org.neo4j.kernel.impl.store.record.NodeRecord; -import org.neo4j.kernel.impl.store.record.PropertyBlock; -import org.neo4j.kernel.impl.store.record.PropertyKeyTokenRecord; -import org.neo4j.kernel.impl.store.record.PropertyRecord; -import org.neo4j.kernel.impl.store.record.RecordLoad; -import org.neo4j.kernel.impl.store.record.RelationshipGroupRecord; -import org.neo4j.kernel.impl.store.record.RelationshipRecord; -import org.neo4j.kernel.impl.store.record.RelationshipTypeTokenRecord; +import org.neo4j.kernel.impl.store.record.Record; +import org.neo4j.test.EphemeralFileSystemRule; +import org.neo4j.test.PageCacheRule; import org.neo4j.test.RandomRule; +import org.neo4j.unsafe.impl.batchimport.store.BatchingIdSequence; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.neo4j.helpers.ArrayUtil.array; -import static org.neo4j.kernel.impl.store.NoStoreHeader.NO_STORE_HEADER; -import static org.neo4j.kernel.impl.store.format.lowlimit.DynamicRecordFormat.RECORD_HEADER_SIZE; +import static java.lang.System.currentTimeMillis; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.util.concurrent.TimeUnit.SECONDS; -@RunWith( Parameterized.class ) -public class RecordFormatTest -{ - //========================================================================== - //========= RULES AND CONSTANTS ============================================ - //========================================================================== +import static org.neo4j.kernel.impl.store.record.RecordLoad.NORMAL; - private static final int TEST_ITERATIONS = 20_000; - private static final int _16B = 1 << 16; - private static final int _32B = 1 << 32; - private static final long _35B = 1L << 35; - private static final long _36B = 1L << 36; - private static final long _40B = 1L << 40; - private static final long NULL = -1; - private static final int BLOCK_SIZE = 120; - // This relies on record header size of a particular format, works for now, but should be changed. - private static final int DATA_SIZE = BLOCK_SIZE - RECORD_HEADER_SIZE; +@Ignore( "Not a test, a base class for testing formats" ) +public abstract class RecordFormatTest +{ + private static final int PAGE_SIZE = 1_024; - @Parameters( name = "{0}" ) - public static Collection data() - { - Collection data = new ArrayList<>(); - data.add( array( LowLimit.RECORD_FORMATS, LOW_LIMITS ) ); - return data; - } + // Whoever is hit first + private static final long TEST_ITERATIONS = 20_000; + private static final long TEST_TIME = 500; + private static final long PRINT_RESULTS_THRESHOLD = SECONDS.toMillis( 1 ); + private static final int DATA_SIZE = 100; + protected static final long NULL = Record.NULL_REFERENCE.intValue(); @ClassRule public static final RandomRule random = new RandomRule(); - @Parameter( 0 ) - public RecordFormats formats; - @Parameter( 1 ) - public RecordKeys keyFactory; - //========================================================================== - //========= FORMATS THAT MAKES UP THE TEST SPECS =========================== - //========================================================================== + @Rule + public final EphemeralFileSystemRule fsRule = new EphemeralFileSystemRule(); + @Rule + public final PageCacheRule pageCacheRule = new PageCacheRule( false /*true here later*/ ); + public RecordKeys keys = FullyCoveringRecordKeys.INSTANCE; - private static long randomLong( long max ) - { - return randomLong( max, NULL ); - } + private final RecordFormats formats; + private final RecordGenerators generators; - private static long randomLong( long max, long nullValue ) + protected RecordFormatTest( RecordFormats formats, RecordGenerators generators ) { - return random.nextFloat() < 0.2 ? nullValue : random.nextLong( max ); + this.formats = formats; + this.generators = generators; } - private static final RecordKeys LOW_LIMITS = new RecordKeys() - { - @Override - public RecordKey node() - { - return new AbstractRecordKey() - { - @Override - public NodeRecord get() - { - return new NodeRecord( 0 ).initialize( - random.nextBoolean(), randomLong( _35B ), random.nextBoolean(), randomLong( _35B ), - randomLong( _40B, 0 ) ); - } - - @Override - public void assertRecordsEquals( NodeRecord written, NodeRecord read ) - { - assertEquals( written.getNextProp(), read.getNextProp() ); - assertEquals( written.getNextRel(), read.getNextRel() ); - assertEquals( written.getLabelField(), read.getLabelField() ); - assertEquals( written.isDense(), read.isDense() ); - } - }; - } - - @Override - public RecordKey relationship() - { - return new AbstractRecordKey() - { - @Override - public RelationshipRecord get() - { - return new RelationshipRecord( 0 ).initialize( random.nextBoolean(), randomLong( _36B ), - random.nextLong( _35B ), random.nextLong( _35B ), random.nextInt( _16B ), - randomLong( _35B ), randomLong( _35B ), - randomLong( _35B ), randomLong( _35B ), - random.nextBoolean(), random.nextBoolean() ); - } - - @Override - public void assertRecordsEquals( RelationshipRecord written, RelationshipRecord read ) - { - assertEquals( written.getNextProp(), read.getNextProp() ); - assertEquals( written.getFirstNode(), read.getFirstNode() ); - assertEquals( written.getSecondNode(), read.getSecondNode() ); - assertEquals( written.getType(), read.getType() ); - assertEquals( written.getFirstPrevRel(), read.getFirstPrevRel() ); - assertEquals( written.getFirstNextRel(), read.getFirstNextRel() ); - assertEquals( written.getSecondPrevRel(), read.getSecondPrevRel() ); - assertEquals( written.getSecondNextRel(), read.getSecondNextRel() ); - } - }; - } - - @Override - public RecordKey property() - { - return new AbstractRecordKey() - { - @Override - public PropertyRecord get() - { - PropertyRecord record = new PropertyRecord( 0 ); - int blocks = random.intBetween( 1, 4 ); - MyDynamicRecordAllocator stringAllocator = new MyDynamicRecordAllocator(); - MyDynamicRecordAllocator arrayAllocator = new MyDynamicRecordAllocator(); - for ( int i = 0; i < blocks; i++ ) - { - PropertyBlock block = new PropertyBlock(); - // Dynamic records will not be written and read by the property record format, - // that happens in the store where it delegates to a "sub" store. - PropertyStore.encodeValue( block, random.nextInt( _16B ), random.propertyValue(), - stringAllocator, arrayAllocator ); - } - return record; - } - - @Override - public void assertRecordsEquals( PropertyRecord written, PropertyRecord read ) - { - assertEquals( written.getNextProp(), read.getNextProp() ); - assertEquals( written.isNodeSet(), read.isNodeSet() ); - if ( written.isNodeSet() ) - { - assertEquals( written.getNodeId(), read.getNodeId() ); - } - else - { - assertEquals( written.getRelId(), read.getRelId() ); - } - assertEquals( written.numberOfProperties(), read.numberOfProperties() ); - Iterator writtenBlocks = written.iterator(); - Iterator readBlocks = read.iterator(); - while ( writtenBlocks.hasNext() ) - { - assertTrue( readBlocks.hasNext() ); - assertBlocksEquals( writtenBlocks.next(), readBlocks.next() ); - } - } - - private void assertBlocksEquals( PropertyBlock written, PropertyBlock read ) - { - assertEquals( written.getKeyIndexId(), read.getKeyIndexId() ); - assertEquals( written.getSize(), read.getSize() ); - assertTrue( written.hasSameContentsAs( read ) ); - assertArrayEquals( written.getValueBlocks(), read.getValueBlocks() ); - } - }; - } - - @Override - public RecordKey relationshipGroup() - { - return new AbstractRecordKey() - { - @Override - public RelationshipGroupRecord get() - { - return new RelationshipGroupRecord( 0 ).initialize( random.nextBoolean(), - random.nextInt( _16B ), randomLong( _35B ), randomLong( _35B ), - randomLong( _35B ), randomLong( _35B ), randomLong( _35B ) ); - } - - @Override - public void assertRecordsEquals( RelationshipGroupRecord written, RelationshipGroupRecord read ) - { - assertEquals( written.getType(), read.getType() ); - assertEquals( written.getFirstOut(), read.getFirstOut() ); - assertEquals( written.getFirstIn(), read.getFirstIn() ); - assertEquals( written.getFirstLoop(), read.getFirstLoop() ); - assertEquals( written.getNext(), read.getNext() ); - assertEquals( written.getOwningNode(), read.getOwningNode() ); - } - }; - } - - @Override - public RecordKey relationshipTypeToken() - { - return new AbstractRecordKey() - { - @Override - public RelationshipTypeTokenRecord get() - { - return new RelationshipTypeTokenRecord( 0 ).initialize( random.nextBoolean(), - random.nextInt( _16B ) ); - } - - @Override - public void assertRecordsEquals( RelationshipTypeTokenRecord written, RelationshipTypeTokenRecord read ) - { - assertEquals( written.getNameId(), read.getNameId() ); - } - }; - } - - @Override - public RecordKey propertyKeyToken() - { - return new AbstractRecordKey() - { - @Override - public PropertyKeyTokenRecord get() - { - return new PropertyKeyTokenRecord( 0 ).initialize( random.nextBoolean(), - random.nextInt( _16B ), random.nextInt( _32B ) ); - } - - @Override - public void assertRecordsEquals( PropertyKeyTokenRecord written, PropertyKeyTokenRecord read ) - { - assertEquals( written.getNameId(), read.getNameId() ); - assertEquals( written.getPropertyCount(), read.getPropertyCount() ); - } - }; - } - - @Override - public RecordKey labelToken() - { - return new AbstractRecordKey() - { - @Override - public LabelTokenRecord get() - { - return new LabelTokenRecord( 0 ).initialize( random.nextBoolean(), - random.nextInt( _16B ) ); - } - - @Override - public void assertRecordsEquals( LabelTokenRecord written, LabelTokenRecord read ) - { - assertEquals( written.getNameId(), read.getNameId() ); - } - }; - } - - @Override - public RecordKey dynamic() - { - return new RecordKey() - { - @Override - public DynamicRecord get() - { - int length = random.nextBoolean() ? DATA_SIZE : random.nextInt( DATA_SIZE ); - long next = length == DATA_SIZE ? random.nextLong( _36B ) : NULL; - DynamicRecord record = new DynamicRecord( 1 ).initialize( random.nextBoolean(), - random.nextBoolean(), next, random.nextInt( PropertyType.values().length ), length ); - byte[] data = new byte[record.getLength()]; - random.nextBytes( data ); - record.setData( data ); - return record; - } - - @Override - public void assertRecordsEquals( DynamicRecord written, DynamicRecord read ) - { - // Don't assert type, since that's read from the data, and the data in this test - // is randomly generated. Since we assert that the data is the same then the type - // is also correct. - assertEquals( written.getLength(), read.getLength() ); - assertEquals( written.getNextBlock(), read.getNextBlock() ); - assertArrayEquals( written.getData(), read.getData() ); - assertEquals( written.isStartRecord(), read.isStartRecord() ); - } - - @Override - public StoreHeader storeHeader() - { - return new IntStoreHeader( BLOCK_SIZE ); - } - }; - } - }; - - //========================================================================== - //========= THE ACTUAL TESTS =============================================== - //========================================================================== - @Test public void node() throws Exception { - verifyWriteAndRead( formats::node, keyFactory::node ); + verifyWriteAndRead( formats::node, generators::node, keys::node ); } @Test public void relationship() throws Exception { - verifyWriteAndRead( formats::relationship, keyFactory::relationship ); + verifyWriteAndRead( formats::relationship, generators::relationship, keys::relationship ); } @Test public void property() throws Exception { - verifyWriteAndRead( formats::property, keyFactory::property ); + verifyWriteAndRead( formats::property, generators::property, keys::property ); } @Test public void relationshipGroup() throws Exception { - verifyWriteAndRead( formats::relationshipGroup, keyFactory::relationshipGroup ); + verifyWriteAndRead( formats::relationshipGroup, generators::relationshipGroup, keys::relationshipGroup ); } @Test public void relationshipTypeToken() throws Exception { - verifyWriteAndRead( formats::relationshipTypeToken, keyFactory::relationshipTypeToken ); + verifyWriteAndRead( formats::relationshipTypeToken, generators::relationshipTypeToken, + keys::relationshipTypeToken ); } @Test public void propertyKeyToken() throws Exception { - verifyWriteAndRead( formats::propertyKeyToken, keyFactory::propertyKeyToken ); + verifyWriteAndRead( formats::propertyKeyToken, generators::propertyKeyToken, keys::propertyKeyToken ); } @Test public void labelToken() throws Exception { - verifyWriteAndRead( formats::labelToken, keyFactory::labelToken ); + verifyWriteAndRead( formats::labelToken, generators::labelToken, keys::labelToken ); } @Test public void dynamic() throws Exception { - verifyWriteAndRead( formats::dynamic, keyFactory::dynamic ); + verifyWriteAndRead( formats::dynamic, generators::dynamic, keys::dynamic ); } - private void verifyWriteAndRead( Supplier> formatSupplier, - Supplier> keyFactory ) + private void verifyWriteAndRead( + Supplier> formatSupplier, + Supplier> generatorSupplier, + Supplier> keySupplier ) throws IOException { // GIVEN - RecordFormat format = formatSupplier.get(); - PageCursor cursor = new StubPageCursor( 0, 1_000 ); - RecordKey key = keyFactory.get(); - int recordSize = format.getRecordSize( key.storeHeader() ); - - // WHEN - for ( int i = 0; i < TEST_ITERATIONS; i++ ) + PageCache pageCache = pageCacheRule.getPageCache( fsRule.get() ); + try ( PagedFile dontUseStoreFile = pageCache.map( new File( "store" ), PAGE_SIZE, CREATE ) ) { - R written = key.get(); + long totalUnusedBytesPrimary = 0; + long totalUnusedBytesSecondary = 0; + long totalRecordsRequiringSecondUnit = 0; + RecordFormat format = formatSupplier.get(); + RecordKey key = keySupplier.get(); + Generator generator = generatorSupplier.get(); + int recordSize = format.getRecordSize( new IntStoreHeader( DATA_SIZE ) ); + RecordBoundaryCheckingPagedFile storeFile = + new RecordBoundaryCheckingPagedFile( dontUseStoreFile, recordSize ); + BatchingIdSequence idSequence = new BatchingIdSequence( random.nextBoolean() ? + idSureToBeOnTheNextPage( PAGE_SIZE, recordSize ) : 10 ); + long smallestUnusedBytesPrimary = recordSize; + long smallestUnusedBytesSecondary = recordSize; + + // WHEN + long time = currentTimeMillis(); + long endTime = time + TEST_TIME; + long i = 0; + for ( ; i < TEST_ITERATIONS && currentTimeMillis() < endTime; i++ ) + { + R written = generator.get( recordSize, format ); + try + { + // write + try ( PageCursor cursor = storeFile.io( 0, PagedFile.PF_SHARED_WRITE_LOCK ) ) + { + assertedNext( cursor ); + if ( written.inUse() ) + { + format.prepare( written, recordSize, idSequence ); + } + + int offset = Math.toIntExact( written.getId() * recordSize ); + cursor.setOffset( offset ); + format.write( written, cursor, recordSize, storeFile ); + } + long recordsUsedForWriting = storeFile.nextCalls(); + long unusedBytes = storeFile.unusedBytes(); + storeFile.resetMeasurements(); - // write - int offset = Math.toIntExact( written.getId() * recordSize ); - cursor.setOffset( offset ); - format.write( written, cursor ); + // read + try ( PageCursor cursor = storeFile.io( 0, PagedFile.PF_SHARED_READ_LOCK ) ) + { + assertedNext( cursor ); + int offset = Math.toIntExact( written.getId() * recordSize ); + cursor.setOffset( offset ); + @SuppressWarnings( "unchecked" ) + R read = (R) written.clone(); // just to get a new instance + format.read( read, cursor, NORMAL, recordSize, storeFile ); + + // THEN + if ( written.inUse() ) + { + assertEquals( written.inUse(), read.inUse() ); + assertEquals( written.getId(), read.getId() ); + assertEquals( written.getSecondaryId(), read.getSecondaryId() ); + key.assertRecordsEquals( written, read ); + } + else + { + assertEquals( written.inUse(), read.inUse() ); + } + } - // read - cursor.setOffset( offset ); - @SuppressWarnings( "unchecked" ) - R read = (R) written.clone(); // just to get a new instance - format.read( read, cursor, RecordLoad.NORMAL, recordSize ); + if ( written.inUse() ) + { + assertEquals( recordsUsedForWriting, storeFile.ioCalls() ); + assertEquals( recordsUsedForWriting, storeFile.nextCalls() ); + assertEquals( unusedBytes, storeFile.unusedBytes() ); + + // unused access don't really count for "wasted space" + if ( recordsUsedForWriting == 1 ) + { + totalUnusedBytesPrimary += unusedBytes; + smallestUnusedBytesPrimary = Math.min( smallestUnusedBytesPrimary, unusedBytes ); + } + else + { + totalUnusedBytesSecondary += unusedBytes; + smallestUnusedBytesSecondary = Math.min( smallestUnusedBytesSecondary, unusedBytes ); + } + totalRecordsRequiringSecondUnit += (recordsUsedForWriting > 1 ? 1 : 0); + } - // THEN - if ( written.inUse() ) - { - key.assertRecordsEquals( written, read ); + storeFile.resetMeasurements(); + idSequence.reset(); + } + catch ( Throwable t ) + { + Exceptions.setMessage( t, t.getMessage() + " : " + written ); + throw t; + } } - else + time = currentTimeMillis() - time; + if ( time >= PRINT_RESULTS_THRESHOLD ) { - assertEquals( written.inUse(), read.inUse() ); + System.out.printf( "%s%n %.2f write-read ops/ms%n %.2f%% required secondary unit%n" + + " %.2f%% wasted primary record space%n" + + " %.2f%% wasted secondary record space%n" + + " %.2f%% wasted total record space%n" + + " %dB smallest primary waste%n" + + " %dB smallest secondary waste%n", + format, ((double)i/time), percent( totalRecordsRequiringSecondUnit, i ), + percent( totalUnusedBytesPrimary, i * recordSize ), + percent( totalUnusedBytesSecondary, i * recordSize ), + percent( totalUnusedBytesPrimary + totalUnusedBytesSecondary, i * recordSize ), + smallestUnusedBytesPrimary, smallestUnusedBytesPrimary); } } } - //========================================================================== - //========= UTILITIES TO AID THE TESTING =================================== - //========================================================================== - - interface RecordKeys - { - RecordKey node(); - - RecordKey relationship(); - - RecordKey property(); - - RecordKey relationshipGroup(); - - RecordKey relationshipTypeToken(); - - RecordKey propertyKeyToken(); - - RecordKey labelToken(); - - RecordKey dynamic(); - } - - interface RecordKey extends Supplier - { - void assertRecordsEquals( RECORD written, RECORD read ); - - StoreHeader storeHeader(); - } - - abstract static class AbstractRecordKey implements RecordKey - { - @Override - public StoreHeader storeHeader() - { - return NO_STORE_HEADER; - } - } - - protected static Collection randomDynamicNodeLabelRecords() + private double percent( long part, long total ) { - Collection records = new ArrayList<>(); - PropertyStore.allocateArrayRecords( records, randomLabelIds( 20 ), new MyDynamicRecordAllocator() ); - return records; + return 100.0D * part / total; } - private static long[] randomLabelIds( int max ) + private void assertedNext( PageCursor cursor ) throws IOException { - long[] ids = new long[random.nextInt( max )]; - for ( int i = 0; i < ids.length; i++ ) - { - ids[i] = random.nextInt( _16B ); - } - return ids; + boolean couldDoNext = cursor.next(); + assert couldDoNext; } - public static class MyDynamicRecordAllocator implements DynamicRecordAllocator + private long idSureToBeOnTheNextPage( int pageSize, int recordSize ) { - private int next = 1; - - @Override - public int getRecordDataSize() - { - return 60; - } - - @Override - public DynamicRecord nextUsedRecordOrNew( Iterator recordsToUseFirst ) - { - return recordsToUseFirst.hasNext() ? recordsToUseFirst.next() : new DynamicRecord( next++ ); - } + return (pageSize + 100) / recordSize; } } diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordGenerators.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordGenerators.java new file mode 100644 index 0000000000000..74b5872b031f8 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordGenerators.java @@ -0,0 +1,54 @@ +/* + * 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.kernel.impl.store.format; + +import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; +import org.neo4j.kernel.impl.store.record.DynamicRecord; +import org.neo4j.kernel.impl.store.record.LabelTokenRecord; +import org.neo4j.kernel.impl.store.record.NodeRecord; +import org.neo4j.kernel.impl.store.record.PropertyKeyTokenRecord; +import org.neo4j.kernel.impl.store.record.PropertyRecord; +import org.neo4j.kernel.impl.store.record.RelationshipGroupRecord; +import org.neo4j.kernel.impl.store.record.RelationshipRecord; +import org.neo4j.kernel.impl.store.record.RelationshipTypeTokenRecord; + +interface RecordGenerators +{ + interface Generator + { + RECORD get( int recordSize, RecordFormat format ); + } + + Generator node(); + + Generator relationship(); + + Generator property(); + + Generator relationshipGroup(); + + Generator relationshipTypeToken(); + + Generator propertyKeyToken(); + + Generator labelToken(); + + Generator dynamic(); +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKey.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKey.java new file mode 100644 index 0000000000000..9854edf31cf56 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKey.java @@ -0,0 +1,27 @@ +/* + * 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.kernel.impl.store.format; + +import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; + +interface RecordKey +{ + void assertRecordsEquals( RECORD written, RECORD read ); +} \ No newline at end of file diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKeys.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKeys.java new file mode 100644 index 0000000000000..d971cb70565f1 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/RecordKeys.java @@ -0,0 +1,48 @@ +/* + * 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.kernel.impl.store.format; + +import org.neo4j.kernel.impl.store.record.DynamicRecord; +import org.neo4j.kernel.impl.store.record.LabelTokenRecord; +import org.neo4j.kernel.impl.store.record.NodeRecord; +import org.neo4j.kernel.impl.store.record.PropertyKeyTokenRecord; +import org.neo4j.kernel.impl.store.record.PropertyRecord; +import org.neo4j.kernel.impl.store.record.RelationshipGroupRecord; +import org.neo4j.kernel.impl.store.record.RelationshipRecord; +import org.neo4j.kernel.impl.store.record.RelationshipTypeTokenRecord; + +interface RecordKeys +{ + RecordKey node(); + + RecordKey relationship(); + + RecordKey property(); + + RecordKey relationshipGroup(); + + RecordKey relationshipTypeToken(); + + RecordKey propertyKeyToken(); + + RecordKey labelToken(); + + RecordKey dynamic(); +} \ No newline at end of file diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/StandaloneDynamicRecordAllocator.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/StandaloneDynamicRecordAllocator.java new file mode 100644 index 0000000000000..935a809f2eef9 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/store/format/StandaloneDynamicRecordAllocator.java @@ -0,0 +1,42 @@ +/* + * 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.kernel.impl.store.format; + +import java.util.Iterator; + +import org.neo4j.kernel.impl.store.DynamicRecordAllocator; +import org.neo4j.kernel.impl.store.record.DynamicRecord; + +class StandaloneDynamicRecordAllocator implements DynamicRecordAllocator +{ + private int next = 1; + + @Override + public int getRecordDataSize() + { + return 60; + } + + @Override + public DynamicRecord nextUsedRecordOrNew( Iterator recordsToUseFirst ) + { + return recordsToUseFirst.hasNext() ? recordsToUseFirst.next() : new DynamicRecord( next++ ); + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/transaction/state/WriteTransactionCommandOrderingTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/transaction/state/WriteTransactionCommandOrderingTest.java index 919a9bbe1d2e6..efa83290f6758 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/transaction/state/WriteTransactionCommandOrderingTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/transaction/state/WriteTransactionCommandOrderingTest.java @@ -35,7 +35,6 @@ import org.neo4j.kernel.impl.store.NodeStore; import org.neo4j.kernel.impl.store.PropertyStore; import org.neo4j.kernel.impl.store.RelationshipStore; -import org.neo4j.kernel.impl.store.format.lowlimit.LowLimit; import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; import org.neo4j.kernel.impl.store.record.LabelTokenRecord; import org.neo4j.kernel.impl.store.record.NodeRecord; @@ -60,6 +59,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.neo4j.kernel.impl.store.format.InternalRecordFormatSelector.select; + public class WriteTransactionCommandOrderingTest { private final AtomicReference> currentRecording = new AtomicReference<>(); @@ -200,7 +201,7 @@ private static class RecordingPropertyStore extends PropertyStore public RecordingPropertyStore( AtomicReference> currentRecording ) { super( null, new Config(), null, null, NullLogProvider.getInstance(), null, null, null, - LowLimit.RECORD_FORMATS.property(), LowLimit.STORE_VERSION ); + select().property(), select().storeVersion() ); this.currentRecording = currentRecording; } @@ -228,7 +229,7 @@ private static class RecordingNodeStore extends NodeStore public RecordingNodeStore( AtomicReference> currentRecording ) { super( null, new Config(), null, null, NullLogProvider.getInstance(), null, - LowLimit.RECORD_FORMATS.node(), LowLimit.STORE_VERSION ); + select().node(), select().storeVersion() ); this.currentRecording = currentRecording; } @@ -264,7 +265,7 @@ private static class RecordingRelationshipStore extends RelationshipStore public RecordingRelationshipStore( AtomicReference> currentRecording ) { super( null, new Config(), null, null, NullLogProvider.getInstance(), - LowLimit.RECORD_FORMATS.relationship(), LowLimit.STORE_VERSION ); + select().relationship(), select().storeVersion() ); this.currentRecording = currentRecording; } diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/BaseBustedRecordFormat.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/BaseBustedRecordFormat.java new file mode 100644 index 0000000000000..639fd8a69f3d1 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/BaseBustedRecordFormat.java @@ -0,0 +1,244 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; +import java.util.function.Function; + +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; +import org.neo4j.kernel.impl.store.StoreHeader; +import org.neo4j.kernel.impl.store.format.BaseOneByteHeaderRecordFormat; +import org.neo4j.kernel.impl.store.format.busted.Reference.DataAdapter; +import org.neo4j.kernel.impl.store.id.IdSequence; +import org.neo4j.kernel.impl.store.record.AbstractBaseRecord; +import org.neo4j.kernel.impl.store.record.Record; + +import static org.neo4j.kernel.impl.store.RecordPageLocationCalculator.offsetForId; +import static org.neo4j.kernel.impl.store.RecordPageLocationCalculator.pageIdForRecord; +import static org.neo4j.kernel.impl.store.format.busted.Reference.PAGE_CURSOR_ADAPTER; + +/** + * Base class for record format which utilizes dynamically sized references to other record IDs and with ability + * to use record units, meaning that a record may span two physical records in the store. This to keep store size + * low and only have records that have big references occupy double amount of space. This format supports up to + * 58-bit IDs, which is roughly 280 quadrillion. With that size the ID limits can be considered busted, + * hence the format name. The IDs take up between 3-8B depending on the size of the ID where relative ID + * references are used as often as possible. See {@link Reference}. + * + * For consistency, all formats have a one-byte header specifying: + * + *

    + *
  1. 0x1: inUse [0=unused, 1=used]
  2. + *
  3. 0x2: record unit [0=single record, 1=multiple records]
  4. + *
  5. 0x4: record unit type [0=first, 1=consecutive] + *
  6. 0x8 - 0x80 other flags for this record specific to each type
  7. + *
+ * + * NOTE to the rest of the flags is that a good use of them is to denote whether or not an ID reference is + * null (-1) as to save 3B (smallest compressed size) by not writing a reference at all. + * + * For records that are the first out of multiple record units, then immediately following the header byte is + * the reference (3-8B) to the secondary ID. After that the "statically sized" data and in the end the + * dynamically sized data. The general thinking is that the break-off into the secondary record will happen in + * the sequence of dynamically sized references and this will allow for crossing the record boundary + * in between, but even in the middle of, references quite easily since the {@link DataAdapter} + * works on byte-per-byte data. + * + * Assigning secondary record unit IDs is done outside of this format implementation, it is just assumed + * that records that gets {@link #write(AbstractBaseRecord, PageCursor, int, PagedFile) written} have already + * been assigned all required such data. + * + * Usually each records are written and read atomically, so this format requires additional logic to be able to + * write and read multiple records together atomically. For writing then currently this is guarded by + * higher level entity write locks and so the {@link PageCursor} can simply move from the first on to the second + * record and continue writing. For reading, which is optimistic and may require retry, one additional + * {@link PageCursor} needs to be acquired over the second record, checking {@link PageCursor#shouldRetry()} + * on both and potentially re-reading the second or both until a consistent read was had. + * + * @param type of {@link AbstractBaseRecord} + */ +public abstract class BaseBustedRecordFormat + extends BaseOneByteHeaderRecordFormat +{ + static final long NULL = Record.NULL_REFERENCE.intValue(); + static final int HEADER_BIT_RECORD_UNIT = 0b0000_0010; + static final int HEADER_BIT_FIRST_RECORD_UNIT = 0b0000_0100; + + protected BaseBustedRecordFormat( Function recordSize, int recordHeaderSize ) + { + super( recordSize, recordHeaderSize, IN_USE_BIT ); + } + + @Override + protected final void doRead( RECORD record, PageCursor primaryCursor, int recordSize, PagedFile storeFile, + long headerByte, boolean inUse ) throws IOException + { + boolean recordUnit = has( headerByte, HEADER_BIT_RECORD_UNIT ); + if ( recordUnit ) + { + boolean firstRecordUnit = has( headerByte, HEADER_BIT_FIRST_RECORD_UNIT); + if ( !firstRecordUnit ) + { + // This is a record unit and not even the first one, so you cannot go here directly and read it, + // it may only be read as part of reading the primary unit. + record.clear(); + return; + } + } + + long secondaryId = -1; + DataAdapter dataAdapter = PAGE_CURSOR_ADAPTER; + SecondaryPageCursorControl secondaryPageCursorControl = SecondaryPageCursorControl.NULL; + if ( recordUnit ) + { + int primaryEndOffset = primaryCursor.getOffset() + recordSize - 1 /*we've already read the header byte*/; + + // This is a record that is split into multiple record units. We need a bit more clever + // data structures here. For the time being this means instantiating one object, + // but the trade-off is a great reduction in complexity. + secondaryId = Reference.decode( primaryCursor, dataAdapter ); + @SuppressWarnings( "resource" ) + SecondaryPageCursorReadDataAdapter readAdapter = new SecondaryPageCursorReadDataAdapter( + primaryCursor, storeFile, + pageIdForRecord( secondaryId, storeFile.pageSize(), recordSize ), + offsetForId( secondaryId, storeFile.pageSize(), recordSize ), + primaryEndOffset, PagedFile.PF_SHARED_READ_LOCK ); + dataAdapter = readAdapter; + secondaryPageCursorControl = readAdapter; + } + + try + { + do + { + // (re)sets offsets for both cursors + secondaryPageCursorControl.reposition(); + doReadInternal( record, primaryCursor, recordSize, headerByte, inUse, dataAdapter ); + } + while ( secondaryPageCursorControl.shouldRetry() ); + if ( recordUnit ) + { + record.setSecondaryId( secondaryId ); + } + } + finally + { + secondaryPageCursorControl.close(); + } + } + + protected abstract void doReadInternal( RECORD record, PageCursor cursor, int recordSize, + long inUseByte, boolean inUse, DataAdapter adapter ); + + @Override + protected final void doWrite( RECORD record, PageCursor primaryCursor, int recordSize, PagedFile storeFile ) + throws IOException + { + // Let the specific implementation provide the additional header bits and we'll provide the core format bits. + byte headerByte = headerBits( record ); + assert (headerByte & 0x7) == 0 : "Format-specific header bits (" + headerByte + + ") collides with format-generic header bits"; + headerByte = set( headerByte, IN_USE_BIT, record.inUse() ); + headerByte = set( headerByte, HEADER_BIT_RECORD_UNIT, record.requiresTwoUnits() ); + headerByte = set( headerByte, HEADER_BIT_FIRST_RECORD_UNIT, true ); + primaryCursor.putByte( headerByte ); + + DataAdapter dataAdapter = PAGE_CURSOR_ADAPTER; + if ( record.requiresTwoUnits() ) + { + int primaryEndOffset = primaryCursor.getOffset() + recordSize - 1 /*we've already read the header byte*/; + + // Write using the normal adapter since the first reference we write cannot really overflow + // into the secondary record + Reference.encode( record.getSecondaryId(), primaryCursor, PAGE_CURSOR_ADAPTER ); + dataAdapter = new SecondaryPageCursorWriteDataAdapter( + pageIdForRecord( record.getSecondaryId(), storeFile.pageSize(), recordSize ), + offsetForId( record.getSecondaryId(), storeFile.pageSize(), recordSize ), primaryEndOffset ); + } + + doWriteInternal( record, primaryCursor, dataAdapter ); + } + + protected abstract void doWriteInternal( RECORD record, PageCursor cursor, DataAdapter adapter ) + throws IOException; + + protected abstract byte headerBits( RECORD record ); + + @Override + public final void prepare( RECORD record, int recordSize, IdSequence idSequence ) + { + assert record.inUse(); + int length = 1 + requiredDataLength( record ); + if ( length > recordSize ) + { + record.setSecondaryId( idSequence.nextId() ); + } + } + + /** + * Required length of the data in the given record (without the header byte). + * + * @param record data to check how much space it would require. + * @return length required to store the data in the given record. + */ + protected abstract int requiredDataLength( RECORD record ); + + protected static int length( long reference ) + { + return Reference.length( reference ); + } + + protected static int length( long reference, long nullValue ) + { + return reference == nullValue ? 0 : length( reference ); + } + + protected static long decode( PageCursor cursor, DataAdapter adapter ) + { + return Reference.decode( cursor, adapter ); + } + + protected static long decode( PageCursor cursor, + DataAdapter adapter, long headerByte, int headerBitMask, long nullValue ) + { + return has( headerByte, headerBitMask ) ? decode( cursor, adapter ) : nullValue; + } + + protected static void encode( PageCursor cursor, DataAdapter adapter, long reference ) + throws IOException + { + Reference.encode( reference, cursor, adapter ); + } + + protected static void encode( PageCursor cursor, DataAdapter adapter, long reference, + long nullValue ) throws IOException + { + if ( reference != nullValue ) + { + Reference.encode( reference, cursor, adapter ); + } + } + + protected static byte set( byte header, int bitMask, long reference, long nullValue ) + { + return set( header, bitMask, reference != nullValue ); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Busted.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Busted.java new file mode 100644 index 0000000000000..bcd70b8d5e2f5 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Busted.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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import org.neo4j.kernel.impl.store.format.RecordFormat; +import org.neo4j.kernel.impl.store.format.RecordFormats; +import org.neo4j.kernel.impl.store.format.lowlimit.LabelTokenRecordFormat; +import org.neo4j.kernel.impl.store.format.lowlimit.PropertyKeyTokenRecordFormat; +import org.neo4j.kernel.impl.store.format.lowlimit.RelationshipTypeTokenRecordFormat; +import org.neo4j.kernel.impl.store.record.DynamicRecord; +import org.neo4j.kernel.impl.store.record.LabelTokenRecord; +import org.neo4j.kernel.impl.store.record.NodeRecord; +import org.neo4j.kernel.impl.store.record.PropertyKeyTokenRecord; +import org.neo4j.kernel.impl.store.record.PropertyRecord; +import org.neo4j.kernel.impl.store.record.RelationshipGroupRecord; +import org.neo4j.kernel.impl.store.record.RelationshipRecord; +import org.neo4j.kernel.impl.store.record.RelationshipTypeTokenRecord; + +/** + * Record format with very high limits, 58-bit per ID, while at the same time keeping store size small. + * + * @see BaseBustedRecordFormat + */ +public class Busted implements RecordFormats +{ + public static final RecordFormats RECORD_FORMATS = new Busted(); + + @Override + public String storeVersion() + { + return "vE.B.0"; + } + + @Override + public RecordFormat node() + { + return new NodeRecordFormat(); + } + + @Override + public RecordFormat relationship() + { + return new RelationshipRecordFormat(); + } + + @Override + public RecordFormat relationshipGroup() + { + return new RelationshipGroupRecordFormat(); + } + + @Override + public RecordFormat property() + { + return new PropertyRecordFormat(); + } + + @Override + public RecordFormat dynamic() + { + return new DynamicRecordFormat(); + } + + @Override + public RecordFormat labelToken() + { + return new LabelTokenRecordFormat(); + } + + @Override + public RecordFormat propertyKeyToken() + { + return new PropertyKeyTokenRecordFormat(); + } + + @Override + public RecordFormat relationshipTypeToken() + { + return new RelationshipTypeTokenRecordFormat(); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/DynamicRecordFormat.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/DynamicRecordFormat.java new file mode 100644 index 0000000000000..5b5231d9451d9 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/DynamicRecordFormat.java @@ -0,0 +1,77 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; +import org.neo4j.kernel.impl.store.format.BaseOneByteHeaderRecordFormat; +import org.neo4j.kernel.impl.store.record.DynamicRecord; + +import static org.neo4j.kernel.impl.store.format.lowlimit.DynamicRecordFormat.readData; + +public class DynamicRecordFormat extends BaseOneByteHeaderRecordFormat +{ + public static final int RECORD_HEADER_SIZE = 1/*header byte*/ + 3/*# of bytes*/ + 8/*max size of next reference*/; + // = 12 + private static final int START_RECORD_BIT = 0x8; + + protected DynamicRecordFormat() + { + super( INT_STORE_HEADER_READER, RECORD_HEADER_SIZE, IN_USE_BIT ); + } + + @Override + public DynamicRecord newRecord() + { + return new DynamicRecord( -1 ); + } + + @Override + protected void doRead( DynamicRecord record, PageCursor cursor, int recordSize, PagedFile storeFile, + long headerByte, boolean inUse ) throws IOException + { + int length = cursor.getShort() | cursor.getByte() << 16; + long next = cursor.getLong(); + boolean isStartRecord = (headerByte & START_RECORD_BIT) != 0; + record.initialize( inUse, isStartRecord, next, -1, length ); + readData( record, cursor ); + } + + @Override + protected void doWrite( DynamicRecord record, PageCursor cursor, int recordSize, PagedFile storeFile ) + throws IOException + { + assert record.getLength() < (1 << 24) - 1; + byte headerByte = (byte) ((record.inUse() ? IN_USE_BIT : 0) | + (record.isStartRecord() ? START_RECORD_BIT : 0)); + cursor.putByte( headerByte ); + cursor.putShort( (short) record.getLength() ); + cursor.putByte( (byte) (record.getLength() >>> 16 ) ); + cursor.putLong( record.getNextBlock() ); + cursor.putBytes( record.getData() ); + } + + @Override + public long getNextRecordReference( DynamicRecord record ) + { + return record.getNextBlock(); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/NodeRecordFormat.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/NodeRecordFormat.java new file mode 100644 index 0000000000000..a723af3ce0c2b --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/NodeRecordFormat.java @@ -0,0 +1,91 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; + +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.kernel.impl.store.format.busted.Reference.DataAdapter; +import org.neo4j.kernel.impl.store.record.NodeRecord; +import org.neo4j.kernel.impl.store.record.Record; + +public class NodeRecordFormat extends BaseBustedRecordFormat +{ + private static final long NULL_LABELS = Record.NO_LABELS_FIELD.intValue(); + private static final int RECORD_SIZE = 16; + private static final int DENSE_NODE_BIT = 0b0000_1000; + private static final int HAS_RELATIONSHIP_BIT = 0b0001_0000; + private static final int HAS_PROPERTY_BIT = 0b0010_0000; + private static final int HAS_LABELS_BIT = 0b0100_0000; + + public NodeRecordFormat() + { + super( fixedRecordSize( RECORD_SIZE ), 0 ); + } + + @Override + public NodeRecord newRecord() + { + return new NodeRecord( -1 ); + } + + @Override + protected void doReadInternal( NodeRecord record, PageCursor cursor, int recordSize, long headerByte, boolean inUse, + DataAdapter adapter ) + { + // Interpret the header byte + boolean dense = has( headerByte, DENSE_NODE_BIT ); + + // Now read the rest of the data. The adapter will take care of moving the cursor over to the + // other unit when we've exhausted the first one. + long nextRel = decode( cursor, adapter, headerByte, HAS_RELATIONSHIP_BIT, NULL ); + long nextProp = decode( cursor, adapter, headerByte, HAS_PROPERTY_BIT, NULL ); + long labelField = decode( cursor, adapter, headerByte, HAS_LABELS_BIT, NULL_LABELS ); + record.initialize( inUse, nextProp, dense, nextRel, labelField ); + } + + @Override + public int requiredDataLength( NodeRecord record ) + { + return length( record.getNextRel(), NULL ) + + length( record.getNextProp(), NULL ) + + length( record.getLabelField(), NULL_LABELS ); + } + + @Override + protected byte headerBits( NodeRecord record ) + { + byte header = 0; + header = set( header, DENSE_NODE_BIT, record.isDense() ); + header = set( header, HAS_RELATIONSHIP_BIT, record.getNextRel(), NULL ); + header = set( header, HAS_PROPERTY_BIT, record.getNextProp(), NULL ); + header = set( header, HAS_LABELS_BIT, record.getLabelField(), NULL_LABELS ); + return header; + } + + @Override + protected void doWriteInternal( NodeRecord record, PageCursor cursor, DataAdapter adapter ) + throws IOException + { + encode( cursor, adapter, record.getNextRel(), NULL ); + encode( cursor, adapter, record.getNextProp(), NULL ); + encode( cursor, adapter, record.getLabelField(), NULL_LABELS ); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/PropertyRecordFormat.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/PropertyRecordFormat.java new file mode 100644 index 0000000000000..f35f685971e29 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/PropertyRecordFormat.java @@ -0,0 +1,95 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; +import org.neo4j.kernel.impl.store.format.BaseOneByteHeaderRecordFormat; +import org.neo4j.kernel.impl.store.record.PropertyBlock; +import org.neo4j.kernel.impl.store.record.PropertyRecord; + +import static org.neo4j.kernel.impl.store.format.busted.Reference.PAGE_CURSOR_ADAPTER; + +/** + * {@link PropertyRecord} format which currently has some wasted space in the end due to hard coded + * limit of 4 blocks per record, whereas the record size is 64. + */ +public class PropertyRecordFormat extends BaseOneByteHeaderRecordFormat +{ + private static final int RECORD_SIZE = 48; + + protected PropertyRecordFormat() + { + super( fixedRecordSize( RECORD_SIZE ), 0, IN_USE_BIT ); + } + + @Override + public PropertyRecord newRecord() + { + return new PropertyRecord( -1 ); + } + + @Override + protected void doRead( PropertyRecord record, PageCursor cursor, int recordSize, PagedFile storeFile, + long headerByte, boolean inUse ) throws IOException + { + int blockCount = (int) (headerByte >>> 4); + record.initialize( inUse, + Reference.decode( cursor, PAGE_CURSOR_ADAPTER ), + Reference.decode( cursor, PAGE_CURSOR_ADAPTER ) ); + while ( blockCount --> 0 ) + { + record.addLoadedBlock( cursor.getLong() ); + } + } + + @Override + protected void doWrite( PropertyRecord record, PageCursor cursor, int recordSize, PagedFile storeFile ) + throws IOException + { + cursor.putByte( (byte) ((record.inUse() ? IN_USE_BIT : 0) | numberOfBlocks( record ) << 4) ); + Reference.encode( record.getPrevProp(), cursor, PAGE_CURSOR_ADAPTER ); + Reference.encode( record.getNextProp(), cursor, PAGE_CURSOR_ADAPTER ); + for ( PropertyBlock block : record ) + { + for ( long propertyBlock : block.getValueBlocks() ) + { + cursor.putLong( propertyBlock ); + } + } + } + + private int numberOfBlocks( PropertyRecord record ) + { + int count = 0; + for ( PropertyBlock block : record ) + { + count += block.getValueBlocks().length; + } + return count; + } + + @Override + public long getNextRecordReference( PropertyRecord record ) + { + return record.getNextProp(); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Reference.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Reference.java new file mode 100644 index 0000000000000..68f4683e3224a --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/Reference.java @@ -0,0 +1,231 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; + +import org.neo4j.io.pagecache.PageCursor; + +import static java.lang.String.format; + +/** + * {@link #encode(long, Object, DataAdapter) Encoding} and {@link #decode(Object, DataAdapter) decoding} of {@code long} + * references, max 58-bit, into an as compact format as possible. Format is close to how utf-8 does similar encoding. + * + * Basically one or more header bits are used to note the number of bytes required to represent a + * particular {@code long} value followed by the value itself. Number of bytes used for any long ranges from + * 3 up to the full 8 bytes. The header bits sits in the most significant bit(s) of the most significant byte, + * so for that the bytes that make up a value is written (and of course read) in big-endian order. + * + * Negative values are also supported, in order to handle relative references. + * + * @author Mattias Persson + */ +public enum Reference +{ + // bit masks below contain one bit for 's' (sign) so actual address space is one bit less than advertised + + // 3-byte, 23-bit addr space: 0sxx xxxx xxxx xxxx xxxx xxxx + BYTE_3( 3, (byte) 0b0, 1 ), + + // 4-byte, 30-bit addr space: 10sx xxxx xxxx xxxx xxxx xxxx xxxx xxxx + BYTE_4( 4, (byte) 0b10, 2 ), + + // 5-byte, 37-bit addr space: 110s xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx + BYTE_5( 5, (byte) 0b110, 3 ), + + // 6-byte, 44-bit addr space: 1110 sxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx + BYTE_6( 6, (byte) 0b1110, 4 ), + + // 7-byte, 51-bit addr space: 1111 0sxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx + BYTE_7( 7, (byte) 0b1111_0, 5 ), + + // 8-byte, 59-bit addr space: 1111 1sxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx + BYTE_8( 8, (byte) 0b1111_1, 5 ); + + public interface DataAdapter + { + byte get( SOURCE source ); + + void put( byte oneByte, SOURCE source ) throws IOException; + } + + public static final DataAdapter PAGE_CURSOR_ADAPTER = new DataAdapter() + { + @Override + public byte get( PageCursor source ) + { + return source.getByte(); + } + + @Override + public void put( byte oneByte, PageCursor source ) + { + source.putByte( oneByte ); + } + }; + + private final int numberOfBytes; + private final short highHeader; + private final short headerMask; + private final int headerShift; + private short signBitMask; + private final long valueOverflowMask; + + private Reference( int numberOfBytes, byte header, int headerBits ) + { + this.numberOfBytes = numberOfBytes; + this.headerShift = Byte.SIZE - headerBits; + this.highHeader = (short) (((byte) (header << headerShift)) & 0xFF); + this.headerMask = (short) (((byte) (0xFF << headerShift)) & 0xFF); + this.valueOverflowMask = ~valueMask( numberOfBytes, headerShift - 1 /*sign bit uses one bit*/ ); + this.signBitMask = (short) (0x1 << (headerShift - 1)); + } + + private long valueMask( int numberOfBytes, int headerShift ) + { + long mask = (long)Math.pow( 2, headerShift ) - 1; + for ( int i = 0; i < numberOfBytes - 1; i++ ) + { + mask <<= 8; + mask |= 0xFF; + } + return mask; + } + + private boolean canEncode( long absoluteReference ) + { + return (absoluteReference & valueOverflowMask) == 0; + } + + private void encode( long absoluteReference, boolean positive, SOURCE source, + DataAdapter adapter ) throws IOException + { + // use big-endianness, most significant byte written first, since it contains encoding information + int shift = (numberOfBytes-1) << 3; + byte signBit = (byte) ((positive ? 0 : 1) << (headerShift - 1)); + + // first (most significant) byte + adapter.put( (byte) (highHeader | signBit | (byte) ((absoluteReference & (0xFFL << shift)) >>> shift)), + source ); + + do // rest of the bytes + { + shift -= 8; + adapter.put( (byte) ((absoluteReference & (0xFFL << shift)) >>> shift), source ); + } + while ( shift > 0 ); + } + + private boolean canDecode( short firstByte ) + { + return (firstByte & headerMask) == highHeader; + } + + private long decode( short firstByte, SOURCE source, DataAdapter adapter ) + { + int shift = (numberOfBytes-1) << 3; + boolean positive = (firstByte & signBitMask) == 0; + + // first (most significant) byte + long mask = ~(0xFFL << (headerShift - 1)); + long result = (mask & firstByte) << shift; + + do // rest of the bytes + { + shift -= 8; + long currentByte = adapter.get( source ) & 0xFFL; + result |= (currentByte << shift); + } + while ( shift > 0 ); + + return positive ? result : ~result; + } + + private int maxBitsSupported() + { + return Long.SIZE - Long.numberOfLeadingZeros( ~valueOverflowMask ); + } + + // Take one copy here since Enum#values() does an unnecessary defensive copy every time. + private static final Reference[] ENCODINGS = Reference.values(); + + public static void encode( long reference, TARGET target, DataAdapter adapter ) throws IOException + { + // checking with < 0 seems to be the fastest way of telling + boolean positive = reference >= 0; + long absoluteReference = positive ? reference : ~reference; + + for ( Reference encoding : ENCODINGS ) + { + if ( encoding.canEncode( absoluteReference ) ) + { + encoding.encode( absoluteReference, positive, target, adapter ); + return; + } + } + throw unsupportedOperationDueToTooBigReference( reference ); + } + + private static UnsupportedOperationException unsupportedOperationDueToTooBigReference( long reference ) + { + return new UnsupportedOperationException( format( "Reference %d uses too many bits to be encoded by " + + "current compression scheme, max %d bits allowed", reference, maxBits() ) ); + } + + public static int length( long reference ) + { + boolean positive = reference >= 0; + long absoluteReference = positive ? reference : ~reference; + + for ( Reference encoding : ENCODINGS ) + { + if ( encoding.canEncode( absoluteReference ) ) + { + return encoding.numberOfBytes; + } + } + throw unsupportedOperationDueToTooBigReference( reference ); + } + + private static int maxBits() + { + int max = 0; + for ( Reference encoding : ENCODINGS ) + { + max = Math.max( max, encoding.maxBitsSupported() ); + } + return max; + } + + public static long decode( SOURCE source, DataAdapter adapter ) + { + short firstByte = (short) (adapter.get( source ) & 0xFF); + for ( Reference encoding : ENCODINGS ) + { + if ( encoding.canDecode( firstByte ) ) + { + return encoding.decode( firstByte, source, adapter ); + } + } + throw new UnsupportedOperationException( "Reference with first byte " + firstByte + " wasn't recognized" + + " as a reference" ); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipGroupRecordFormat.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipGroupRecordFormat.java new file mode 100644 index 0000000000000..6c816e2e8cf52 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipGroupRecordFormat.java @@ -0,0 +1,92 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.kernel.impl.store.format.busted.Reference.DataAdapter; +import org.neo4j.kernel.impl.store.record.RelationshipGroupRecord; + +public class RelationshipGroupRecordFormat extends BaseBustedRecordFormat +{ + private static final int RECORD_SIZE = 32; + private static final int HAS_OUTGOING_BIT = 0b0000_1000; + private static final int HAS_INCOMING_BIT = 0b0001_0000; + private static final int HAS_LOOP_BIT = 0b0010_0000; + private static final int HAS_NEXT_BIT = 0b0100_0000; + + public RelationshipGroupRecordFormat() + { + super( fixedRecordSize( RECORD_SIZE ), 0 ); + } + + @Override + public RelationshipGroupRecord newRecord() + { + return new RelationshipGroupRecord( -1 ); + } + + @Override + protected void doReadInternal( RelationshipGroupRecord record, PageCursor cursor, int recordSize, long headerByte, + boolean inUse, DataAdapter adapter ) + { + record.initialize( inUse, + cursor.getShort() & 0xFFFF, + decode( cursor, adapter, headerByte, HAS_OUTGOING_BIT, NULL ), + decode( cursor, adapter, headerByte, HAS_INCOMING_BIT, NULL ), + decode( cursor, adapter, headerByte, HAS_LOOP_BIT, NULL ), + decode( cursor, adapter ), + decode( cursor, adapter, headerByte, HAS_NEXT_BIT, NULL ) ); + } + + @Override + protected byte headerBits( RelationshipGroupRecord record ) + { + byte header = 0; + header = set( header, HAS_OUTGOING_BIT, record.getFirstOut(), NULL ); + header = set( header, HAS_INCOMING_BIT, record.getFirstIn(), NULL ); + header = set( header, HAS_LOOP_BIT, record.getFirstLoop(), NULL ); + header = set( header, HAS_NEXT_BIT, record.getNext(), NULL ); + return header; + } + + @Override + protected int requiredDataLength( RelationshipGroupRecord record ) + { + return 2 + // type + length( record.getFirstOut(), NULL ) + + length( record.getFirstIn(), NULL ) + + length( record.getFirstLoop(), NULL ) + + length( record.getOwningNode() ) + + length( record.getNext(), NULL ); + } + + @Override + protected void doWriteInternal( RelationshipGroupRecord record, PageCursor cursor, DataAdapter adapter ) + throws IOException + { + cursor.putShort( (short) record.getType() ); + encode( cursor, adapter, record.getFirstOut(), NULL ); + encode( cursor, adapter, record.getFirstIn(), NULL ); + encode( cursor, adapter, record.getFirstLoop(), NULL ); + encode( cursor, adapter, record.getOwningNode() ); + encode( cursor, adapter, record.getNext(), NULL ); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipRecordFormat.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipRecordFormat.java new file mode 100644 index 0000000000000..5692ea044b7b5 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/RelationshipRecordFormat.java @@ -0,0 +1,103 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.kernel.impl.store.format.busted.Reference.DataAdapter; +import org.neo4j.kernel.impl.store.record.RelationshipRecord; + +public class RelationshipRecordFormat extends BaseBustedRecordFormat +{ + private static final int RECORD_SIZE = 32; + private static final int FIRST_IN_START_BIT = 0b0000_1000; + private static final int FIRST_IN_END_BIT = 0b0001_0000; + private static final int HAS_START_NEXT_BIT = 0b0010_0000; + private static final int HAS_END_NEXT_BIT = 0b0100_0000; + private static final int HAS_PROPERTY_BIT = 0b1000_0000; + + public RelationshipRecordFormat() + { + super( fixedRecordSize( RECORD_SIZE ), 0 ); + } + + @Override + public RelationshipRecord newRecord() + { + return new RelationshipRecord( -1 ); + } + + @Override + protected void doReadInternal( RelationshipRecord record, PageCursor cursor, int recordSize, long headerByte, + boolean inUse, DataAdapter adapter ) + { + int type = cursor.getShort() & 0xFFFF; + record.initialize( inUse, + decode( cursor, adapter, headerByte, HAS_PROPERTY_BIT, NULL ), + decode( cursor, adapter ), + decode( cursor, adapter ), + type, + decode( cursor, adapter ), + decode( cursor, adapter, headerByte, HAS_START_NEXT_BIT, NULL ), + decode( cursor, adapter ), + decode( cursor, adapter, headerByte, HAS_END_NEXT_BIT, NULL ), + has( headerByte, FIRST_IN_START_BIT ), + has( headerByte, FIRST_IN_END_BIT ) ); + } + + @Override + protected byte headerBits( RelationshipRecord record ) + { + byte header = 0; + header = set( header, FIRST_IN_START_BIT, record.isFirstInFirstChain() ); + header = set( header, FIRST_IN_END_BIT, record.isFirstInSecondChain() ); + header = set( header, HAS_PROPERTY_BIT, record.getNextProp(), NULL ); + header = set( header, HAS_START_NEXT_BIT, record.getFirstNextRel(), NULL ); + header = set( header, HAS_END_NEXT_BIT, record.getSecondNextRel(), NULL ); + return header; + } + + @Override + protected int requiredDataLength( RelationshipRecord record ) + { + return 2 + // type + length( record.getNextProp(), NULL ) + + length( record.getFirstNode() ) + + length( record.getSecondNode() ) + + length( record.getFirstPrevRel() ) + + length( record.getFirstNextRel(), NULL ) + + length( record.getSecondPrevRel() ) + + length( record.getSecondNextRel(), NULL ); + } + + @Override + protected void doWriteInternal( RelationshipRecord record, PageCursor cursor, DataAdapter adapter ) + throws IOException + { + cursor.putShort( (short) record.getType() ); + encode( cursor, adapter, record.getNextProp(), NULL ); + encode( cursor, adapter, record.getFirstNode() ); + encode( cursor, adapter, record.getSecondNode() ); + encode( cursor, adapter, record.getFirstPrevRel() ); + encode( cursor, adapter, record.getFirstNextRel(), NULL ); + encode( cursor, adapter, record.getSecondPrevRel() ); + encode( cursor, adapter, record.getSecondNextRel(), NULL ); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorControl.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorControl.java new file mode 100644 index 0000000000000..818d97c4923c4 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorControl.java @@ -0,0 +1,70 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; + +import org.neo4j.io.pagecache.PageCursor; + +/** + * Used in {@link Busted} record format where records may required multiple units, which mean writing and + * reading may require, from one byte to the next, move to another place or cursor to read from or write to. + * Encapsulates logic for checking for consistent reads and repositioning for next retry. + */ +interface SecondaryPageCursorControl extends AutoCloseable +{ + /** + * In the event of a secondary page cursor was used this may return {@code true}, in which case + * (at least) the second record unit needs to be re-read. The check whether or not the primary unit + * needs to be retried happens as part of the outer "normal" read/write, not here. + * + * @return whether or not a potential second record unit needs to be retried. + * @throws IOException on error reading/writing or switching {@link PageCursor}. + * @see PageCursor#shouldRetry() + */ + boolean shouldRetry() throws IOException; + + /** + * Repositions cursor(s) before retrying operation after seeing that {@link #shouldRetry()} returned {@code true}. + */ + void reposition(); + + @Override + void close(); + + public static final SecondaryPageCursorControl NULL = new SecondaryPageCursorControl() + { + @Override + public boolean shouldRetry() throws IOException + { + return false; + } + + @Override + public void reposition() + { // No need + } + + @Override + public void close() + { // Nothing to close + } + }; +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorReadDataAdapter.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorReadDataAdapter.java new file mode 100644 index 0000000000000..a1ba5547e8b7f --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorReadDataAdapter.java @@ -0,0 +1,101 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; + +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; +import org.neo4j.kernel.impl.store.format.busted.Reference.DataAdapter; + +/** + * {@link DataAdapter} able to acquire a secondary {@link PageCursor} on potentially a different page + * for continuing reading contents belonging to the primary record. + */ +class SecondaryPageCursorReadDataAdapter implements DataAdapter, SecondaryPageCursorControl +{ + private final PageCursor primaryCursor; + private final int primaryInitialOffset; + private final int primaryEndOffset; + private final PageCursor secondaryCursor; + private final int secondaryOffset; + private boolean switched; + + SecondaryPageCursorReadDataAdapter( PageCursor cursor, PagedFile storeFile, + long secondaryPageId, int secondaryOffset, int primaryEndOffset, int pfFlags ) throws IOException + { + this.primaryCursor = cursor; + this.primaryEndOffset = primaryEndOffset; + this.primaryInitialOffset = cursor.getOffset(); + this.secondaryCursor = storeFile.io( secondaryPageId, pfFlags ); + this.secondaryCursor.next(); + this.secondaryOffset = secondaryOffset; + } + + @Override + public byte get( PageCursor primaryCursor /*same as the one we have*/ ) + { + if ( primaryCursor.getOffset() == primaryEndOffset ) + { + // We've come to the end of the primary cursor, use the secondary cursor instead + if ( !switched ) + { + // Just read out the header, get it out of the way and verify that this secondary record + // is in fact a secondary record. + // TODO can we do this in BaseBustedRecordFormat (the place where this adapter is created) instead? + byte secondaryHeaderByte = secondaryCursor.getByte(); + assert (secondaryHeaderByte & BaseBustedRecordFormat.HEADER_BIT_RECORD_UNIT) != 0; + assert (secondaryHeaderByte & BaseBustedRecordFormat.HEADER_BIT_FIRST_RECORD_UNIT) == 0; + switched = true; + } + return secondaryCursor.getByte(); + } + + // There's still data to be read from the primary cursor + return primaryCursor.getByte(); + } + + @Override + public void put( byte oneByte, PageCursor primaryCursor ) + { + throw new UnsupportedOperationException(); + } + + @Override + public void reposition() + { + primaryCursor.setOffset( primaryInitialOffset ); + secondaryCursor.setOffset( secondaryOffset ); + } + + @Override + public boolean shouldRetry() throws IOException + { + // Don't check shouldRetry on primary here since that will happen in the outer loop + // and will guard both units. + return secondaryCursor.shouldRetry(); + } + + @Override + public void close() + { + secondaryCursor.close(); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorWriteDataAdapter.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorWriteDataAdapter.java new file mode 100644 index 0000000000000..8e613d84ab815 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/store/format/busted/SecondaryPageCursorWriteDataAdapter.java @@ -0,0 +1,72 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import java.io.IOException; + +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.kernel.impl.store.UnderlyingStorageException; +import org.neo4j.kernel.impl.store.format.busted.Reference.DataAdapter; + +import static org.neo4j.kernel.impl.store.format.BaseRecordFormat.IN_USE_BIT; +import static org.neo4j.kernel.impl.store.format.busted.BaseBustedRecordFormat.HEADER_BIT_RECORD_UNIT; + +/** + * {@link DataAdapter} able to move the {@link PageCursor} to another record, potentially on another page, + * for continued writing of contents to a secondary record unit. + */ +class SecondaryPageCursorWriteDataAdapter implements DataAdapter +{ + private boolean switched; + private final long pageIdForSecondaryRecord; + private final int offsetForSecondaryId; + private final int primaryEndOffset; + + SecondaryPageCursorWriteDataAdapter( long pageIdForSecondaryRecord, + int offsetForSecondaryId, int primaryEndOffset ) + { + this.pageIdForSecondaryRecord = pageIdForSecondaryRecord; + this.offsetForSecondaryId = offsetForSecondaryId; + this.primaryEndOffset = primaryEndOffset; + } + + @Override + public byte get( PageCursor source ) + { + throw new UnsupportedOperationException(); + } + + @Override + public void put( byte oneByte, PageCursor cursor ) throws IOException + { + if ( !switched && cursor.getOffset() == primaryEndOffset ) + { + if ( !cursor.next( pageIdForSecondaryRecord ) ) + { + throw new UnderlyingStorageException( "Couldn't move to secondary page " + pageIdForSecondaryRecord ); + } + cursor.setOffset( offsetForSecondaryId ); + cursor.putByte( (byte) (IN_USE_BIT | HEADER_BIT_RECORD_UNIT) ); + switched = true; + } + + cursor.putByte( oneByte ); + } +} diff --git a/enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/BustedRecordFormatTest.java b/enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/BustedRecordFormatTest.java new file mode 100644 index 0000000000000..5b44a24887a14 --- /dev/null +++ b/enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/BustedRecordFormatTest.java @@ -0,0 +1,33 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format; + +import org.neo4j.kernel.impl.store.format.busted.Busted; + +public class BustedRecordFormatTest extends RecordFormatTest +{ + protected static final RecordGenerators _58_BIT_LIMITS = new LimitedRecordGenerators( random, 58, 58, 58, 16, NULL ); + protected static final RecordGenerators _50_BIT_LIMITS = new LimitedRecordGenerators( random, 50, 50, 50, 16, NULL ); + + public BustedRecordFormatTest() + { + super( Busted.RECORD_FORMATS, _50_BIT_LIMITS ); + } +} diff --git a/enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/busted/ReferenceTest.java b/enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/busted/ReferenceTest.java new file mode 100644 index 0000000000000..73971820009f4 --- /dev/null +++ b/enterprise/kernel/src/test/java/org/neo4j/kernel/impl/store/format/busted/ReferenceTest.java @@ -0,0 +1,94 @@ +/* + * 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 Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.store.format.busted; + +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.neo4j.io.pagecache.StubPageCursor; +import org.neo4j.kernel.impl.store.format.busted.Reference; +import org.neo4j.test.RandomRule; + +import static org.junit.Assert.assertEquals; + +import static org.neo4j.kernel.impl.store.format.busted.Reference.PAGE_CURSOR_ADAPTER; + +public class ReferenceTest +{ + public final @Rule RandomRule random = new RandomRule(); + private final ByteBuffer buffer = ByteBuffer.allocateDirect( 100 ); + private final StubPageCursor cursor = new StubPageCursor( 0, buffer ); + + @Test + public void shouldEncodeRandomLongs() throws Exception + { + // WHEN/THEN + long mask = numberOfBits( 58 ); + for ( int i = 0; i < 100_000_000; i++ ) + { + long reference = limit( random.nextLong(), mask ); + assertDecodedMatchesEncoded( reference ); + } + } + + private long numberOfBits( int count ) + { + long result = 0; + for ( int i = 0; i < count; i++ ) + { + result = (result << 1) | 1; + } + return result; + } + + /** + * The current scheme only allows us to use 58 bits for a reference. Adhere to that limit here. + */ + private long limit( long reference, long mask ) + { + boolean positive = true; + if ( reference < 0 ) + { + positive = false; + reference = ~reference; + } + + reference &= mask; + + if ( !positive ) + { + reference = ~reference; + } + return reference; + } + + private void assertDecodedMatchesEncoded( long reference ) throws IOException + { + cursor.setOffset( 0 ); + Reference.encode( reference, cursor, PAGE_CURSOR_ADAPTER ); + + cursor.setOffset( 0 ); + long read = Reference.decode( cursor, PAGE_CURSOR_ADAPTER ); + assertEquals( reference, read ); + } +}