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 ); + } +}