diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/IndexKeyStorage.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/IndexKeyStorage.java new file mode 100644 index 0000000000000..36760e5fbfdaa --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/IndexKeyStorage.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.index.schema; + +import java.io.File; +import java.io.IOException; + +import org.neo4j.index.internal.gbptree.Layout; +import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.io.pagecache.PageCursor; + +import static java.lang.String.format; + +class IndexKeyStorage> extends SimpleEntryStorage> +{ + private static final byte KEY_TYPE = 1; + private final Layout layout; + + IndexKeyStorage( FileSystemAbstraction fs, File file, ByteBufferFactory byteBufferFactory, int blockSize, Layout layout ) throws IOException + { + super( fs, file, byteBufferFactory, blockSize ); + this.layout = layout; + } + + @Override + void add( KEY key, PageCursor pageCursor ) throws IOException + { + int entrySize = TYPE_SIZE + BlockEntry.keySize( layout, key ); + prepareWrite( entrySize ); + pageCursor.putByte( KEY_TYPE ); + BlockEntry.write( pageCursor, layout, key ); + } + + @Override + KeyEntryCursor reader( PageCursor pageCursor ) + { + return new KeyEntryCursor<>( pageCursor, layout ); + } + + static class KeyEntryCursor implements BlockEntryCursor + { + private final PageCursor pageCursor; + private final Layout layout; + private final KEY key; + + KeyEntryCursor( PageCursor pageCursor, Layout layout ) + { + this.pageCursor = pageCursor; + this.layout = layout; + this.key = layout.newKey(); + } + + @Override + public boolean next() throws IOException + { + byte type = pageCursor.getByte(); + if ( type == STOP_TYPE ) + { + return false; + } + if ( type != KEY_TYPE ) + { + throw new RuntimeException( format( "Unexpected entry type. Expected %d or %d, but was %d.", STOP_TYPE, KEY_TYPE, type ) ); + } + BlockEntry.read( pageCursor, layout, key ); + return true; + } + + @Override + public KEY key() + { + return key; + } + + @Override + public Void value() + { + return null; + } + + @Override + public void close() throws IOException + { + pageCursor.close(); + } + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorage.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorage.java index 4d7dc560c7a50..968b45002f1b4 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorage.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorage.java @@ -19,20 +19,14 @@ */ package org.neo4j.kernel.impl.index.schema; -import java.io.Closeable; import java.io.File; import java.io.IOException; -import java.nio.ByteBuffer; import org.neo4j.index.internal.gbptree.Layout; import org.neo4j.io.fs.FileSystemAbstraction; -import org.neo4j.io.fs.OpenMode; -import org.neo4j.io.fs.StoreChannel; -import org.neo4j.io.pagecache.ByteArrayPageCursor; import org.neo4j.io.pagecache.PageCursor; import org.neo4j.kernel.api.index.IndexEntryUpdate; import org.neo4j.kernel.impl.api.index.UpdateMode; -import org.neo4j.kernel.impl.transaction.log.ReadAheadChannel; import static org.neo4j.kernel.impl.index.schema.NativeIndexUpdater.initializeKeyAndValueFromUpdate; import static org.neo4j.kernel.impl.index.schema.NativeIndexUpdater.initializeKeyFromUpdate; @@ -40,40 +34,25 @@ /** * Buffer {@link IndexEntryUpdate} by writing them out to a file. Can be read back in insert order through {@link #reader()}. */ -public class IndexUpdateStorage,VALUE extends NativeIndexValue> implements Closeable +public class IndexUpdateStorage, VALUE extends NativeIndexValue> + extends SimpleEntryStorage,IndexUpdateCursor> { - private static final int TYPE_SIZE = Byte.BYTES; - static final byte STOP_TYPE = -1; - private final Layout layout; - private final FileSystemAbstraction fs; - private final File file; - private final ByteBufferFactory byteBufferFactory; - private final int blockSize; - private final ByteBuffer buffer; - private final ByteArrayPageCursor pageCursor; - private final StoreChannel storeChannel; private final KEY key1; private final KEY key2; private final VALUE value; - private volatile long count; - IndexUpdateStorage( Layout layout, FileSystemAbstraction fs, File file, ByteBufferFactory byteBufferFactory, int blockSize ) throws IOException + IndexUpdateStorage( FileSystemAbstraction fs, File file, ByteBufferFactory byteBufferFactory, int blockSize, Layout layout ) throws IOException { + super( fs, file, byteBufferFactory, blockSize ); this.layout = layout; - this.fs = fs; - this.file = file; - this.byteBufferFactory = byteBufferFactory; - this.blockSize = blockSize; - this.buffer = byteBufferFactory.newBuffer( blockSize ); - this.pageCursor = new ByteArrayPageCursor( buffer ); - this.storeChannel = fs.create( file ); this.key1 = layout.newKey(); this.key2 = layout.newKey(); this.value = layout.newValue(); } - public void add( IndexEntryUpdate update ) throws IOException + @Override + public void add( IndexEntryUpdate update, PageCursor pageCursor ) throws IOException { int entrySize = TYPE_SIZE; UpdateMode updateMode = update.updateMode(); @@ -96,50 +75,15 @@ public void add( IndexEntryUpdate update ) throws IOException throw new IllegalArgumentException( "Unknown update mode " + updateMode ); } - if ( entrySize > buffer.remaining() ) - { - flush(); - } + prepareWrite( entrySize ); pageCursor.putByte( (byte) updateMode.ordinal() ); IndexUpdateEntry.write( pageCursor, layout, updateMode, key1, key2, value ); - // a single thread, and the same thread every time, increments this count - count++; - } - - void doneAdding() throws IOException - { - if ( buffer.remaining() < TYPE_SIZE ) - { - flush(); - } - pageCursor.putByte( STOP_TYPE ); - flush(); - } - - public IndexUpdateCursor reader() throws IOException - { - ReadAheadChannel channel = new ReadAheadChannel<>( fs.open( file, OpenMode.READ ), byteBufferFactory.newBuffer( blockSize ) ); - PageCursor pageCursor = new ReadableChannelPageCursor( channel ); - return new IndexUpdateCursor<>( pageCursor, layout ); - } - - private void flush() throws IOException - { - buffer.flip(); - storeChannel.write( buffer ); - buffer.clear(); - } - - long count() - { - return count; } @Override - public void close() throws IOException + public IndexUpdateCursor reader( PageCursor pageCursor ) { - storeChannel.close(); - fs.deleteFile( file ); + return new IndexUpdateCursor<>( pageCursor, layout ); } } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/SimpleEntryStorage.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/SimpleEntryStorage.java new file mode 100644 index 0000000000000..701de84983a26 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/SimpleEntryStorage.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.index.schema; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.io.fs.OpenMode; +import org.neo4j.io.fs.StoreChannel; +import org.neo4j.io.pagecache.ByteArrayPageCursor; +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.kernel.impl.transaction.log.ReadAheadChannel; + +import static org.neo4j.io.IOUtils.closeAllUnchecked; +import static org.neo4j.util.concurrent.Runnables.runAll; + +/** + * Not thread safe, except for {@link #count()} which does not support calls concurrent with {@link #add(Object)}. + * + * Storage that store {@link ENTRY entries} in a file by simply appending them. + * Entries can be read back, in the order they where added, through a {@link CURSOR}. + * This storage is useful when we don't want to hold all entries in memory. + * + * Extending classes are responsible for serializing and deserializing entries. + * + * On close, file will be deleted but provided {@link ByteBufferFactory} will not be closed. + * + * @param Type of entry we are storing. + * @param Cursor type responsible for deserializing what we have stored. + */ +public abstract class SimpleEntryStorage implements Closeable +{ + static final int TYPE_SIZE = Byte.BYTES; + static final byte STOP_TYPE = -1; + private static final byte[] NO_ENTRIES = {STOP_TYPE}; + private final File file; + private final FileSystemAbstraction fs; + private final int blockSize; + private final ByteBufferFactory byteBufferFactory; + + // Resources allocated lazily upon add + private boolean allocated; + private ByteBuffer buffer; + private ByteArrayPageCursor pageCursor; + private StoreChannel storeChannel; + + private volatile long count; + + SimpleEntryStorage( FileSystemAbstraction fs, File file, ByteBufferFactory byteBufferFactory, int blockSize ) throws IOException + { + this.fs = fs; + this.file = file; + this.byteBufferFactory = byteBufferFactory; + this.blockSize = blockSize; + } + + void add( ENTRY entry ) throws IOException + { + allocateResources(); + add( entry, pageCursor ); + // a single thread, and the same thread every time, increments this count + count++; + } + + CURSOR reader() throws IOException + { + if ( !allocated ) + { + return reader( new ByteArrayPageCursor( NO_ENTRIES ) ); + } + ReadAheadChannel channel = new ReadAheadChannel<>( fs.open( file, OpenMode.READ ), byteBufferFactory.newBuffer( blockSize ) ); + PageCursor pageCursor = new ReadableChannelPageCursor( channel ); + return reader( pageCursor ); + } + + long count() + { + return count; + } + + void doneAdding() throws IOException + { + if ( !allocated ) + { + return; + } + if ( buffer.remaining() < TYPE_SIZE ) + { + flush(); + } + pageCursor.putByte( STOP_TYPE ); + flush(); + } + + @Override + public void close() throws IOException + { + if ( allocated ) + { + runAll( "Failed while trying to close " + getClass().getSimpleName(), + () -> closeAllUnchecked( pageCursor, storeChannel ), + () -> fs.deleteFile( file ) + ); + } + else + { + fs.deleteFile( file ); + } + } + + /** + * DON'T CALL THIS METHOD DIRECTLY. Instead, use {@link #add(Object)}. + * Write entry to pageCursor. Implementor of this method is responsible for calling {@link #prepareWrite(int)} before actually start writing. + */ + abstract void add( ENTRY entry, PageCursor pageCursor ) throws IOException; + + /** + * DON'T CALL THIS METHOD DIRECTLY. Instead use {@link #reader()}. + * Return {@link CURSOR} responsible for deserializing wrapping provided {@link PageCursor}, pointing to head of file. + */ + abstract CURSOR reader( PageCursor pageCursor ) throws IOException; + + /** + * DON'T CALL THIS METHOD DIRECTLY. Only used by subclasses. + */ + void prepareWrite( int entrySize ) throws IOException + { + if ( entrySize > buffer.remaining() ) + { + flush(); + } + } + + private void flush() throws IOException + { + buffer.flip(); + storeChannel.write( buffer ); + buffer.clear(); + } + + private void allocateResources() throws IOException + { + if ( !allocated ) + { + this.buffer = byteBufferFactory.newBuffer( blockSize ); + this.pageCursor = new ByteArrayPageCursor( buffer ); + this.storeChannel = fs.create( file ); + this.allocated = true; + } + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/IndexKeyStorageTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/IndexKeyStorageTest.java new file mode 100644 index 0000000000000..6ec1e18e2c967 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/IndexKeyStorageTest.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.index.schema; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.impl.index.schema.config.ConfiguredSpaceFillingCurveSettingsCache; +import org.neo4j.kernel.impl.index.schema.config.IndexSpecificSpaceFillingCurveSettingsCache; +import org.neo4j.memory.LocalMemoryTracker; +import org.neo4j.test.extension.Inject; +import org.neo4j.test.extension.RandomExtension; +import org.neo4j.test.extension.TestDirectoryExtension; +import org.neo4j.test.rule.RandomRule; +import org.neo4j.test.rule.TestDirectory; +import org.neo4j.values.storable.Value; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.neo4j.kernel.impl.index.schema.ByteBufferFactory.HEAP_BUFFER_FACTORY; +import static org.neo4j.kernel.impl.index.schema.NativeIndexKey.Inclusion.NEUTRAL; + +@ExtendWith( {TestDirectoryExtension.class, RandomExtension.class} ) +class IndexKeyStorageTest +{ + private static final int BLOCK_SIZE = 2000; + private static final IndexSpecificSpaceFillingCurveSettingsCache spatialSettings = + new IndexSpecificSpaceFillingCurveSettingsCache( new ConfiguredSpaceFillingCurveSettingsCache( Config.defaults() ), new HashMap<>() ); + + @Inject + protected TestDirectory directory; + + @Inject + protected RandomRule random; + + private GenericLayout layout; + private int numberOfSlots; + + @BeforeEach + void createLayout() + { + this.numberOfSlots = random.nextInt( 1, 3 ); + this.layout = new GenericLayout( numberOfSlots, spatialSettings ); + } + + @Test + void shouldAddAndReadZeroKey() throws IOException + { + try ( IndexKeyStorage keyStorage = keyStorage() ) + { + keyStorage.doneAdding(); + try ( IndexKeyStorage.KeyEntryCursor reader = keyStorage.reader() ) + { + assertFalse( reader.next(), "Didn't expect reader to have any entries." ); + } + } + } + + @Test + void shouldAddAndReadOneKey() throws IOException + { + try ( IndexKeyStorage keyStorage = keyStorage() ) + { + GenericKey expected = randomKey( 1 ); + keyStorage.add( expected ); + keyStorage.doneAdding(); + try ( IndexKeyStorage.KeyEntryCursor reader = keyStorage.reader() ) + { + assertTrue( reader.next(), "Expected reader to have one entry" ); + GenericKey actual = reader.key(); + assertEquals( 0, layout.compare( expected, actual ), "Expected stored key to be equal to original." ); + assertFalse( reader.next(), "Expected reader to have only one entry, second entry was " + reader.key() ); + } + } + } + + @Test + void shouldAddAndReadMultipleKeys() throws IOException + { + List keys = new ArrayList<>(); + int numberOfKeys = 1000; + for ( int i = 0; i < numberOfKeys; i++ ) + { + keys.add( randomKey( i ) ); + } + try ( IndexKeyStorage keyStorage = keyStorage() ) + { + for ( GenericKey key : keys ) + { + keyStorage.add( key ); + } + keyStorage.doneAdding(); + try ( IndexKeyStorage.KeyEntryCursor reader = keyStorage.reader() ) + { + for ( GenericKey expected : keys ) + { + assertTrue( reader.next() ); + GenericKey actual = reader.key(); + assertEquals( 0, layout.compare( expected, actual ), "Expected stored key to be equal to original." ); + } + assertFalse( reader.next(), "Expected reader to have no more entries, but had at least one additional " + reader.key() ); + } + } + } + + @Test + void shouldNotCreateFileIfNoData() throws IOException + { + FileSystemAbstraction fs = directory.getFileSystem(); + File makeSureImDeleted = directory.file( "makeSureImDeleted" ); + try ( IndexKeyStorage keyStorage = keyStorage( makeSureImDeleted ) ) + { + assertFalse( fs.fileExists( makeSureImDeleted ), "Expected this file to exist now so that we can assert deletion later." ); + keyStorage.doneAdding(); + assertFalse( fs.fileExists( makeSureImDeleted ), "Expected this file to exist now so that we can assert deletion later." ); + } + assertFalse( fs.fileExists( makeSureImDeleted ), "Expected this file to be deleted on close." ); + } + + @Test + void shouldDeleteFileOnCloseWithData() throws IOException + { + FileSystemAbstraction fs = directory.getFileSystem(); + File makeSureImDeleted = directory.file( "makeSureImDeleted" ); + try ( IndexKeyStorage keyStorage = keyStorage( makeSureImDeleted ) ) + { + keyStorage.add( randomKey( 1 ) ); + keyStorage.doneAdding(); + assertTrue( fs.fileExists( makeSureImDeleted ), "Expected this file to exist now so that we can assert deletion later." ); + } + assertFalse( fs.fileExists( makeSureImDeleted ), "Expected this file to be deleted on close." ); + } + + @Test + void shouldDeleteFileOnCloseWithDataBeforeDoneAdding() throws IOException + { + FileSystemAbstraction fs = directory.getFileSystem(); + File makeSureImDeleted = directory.file( "makeSureImDeleted" ); + try ( IndexKeyStorage keyStorage = keyStorage( makeSureImDeleted ) ) + { + keyStorage.add( randomKey( 1 ) ); + assertTrue( fs.fileExists( makeSureImDeleted ), "Expected this file to exist now so that we can assert deletion later." ); + } + assertFalse( fs.fileExists( makeSureImDeleted ), "Expected this file to be deleted on close." ); + } + + @Test + void mustAllocateResourcesLazilyAndCleanUpOnClose() throws IOException + { + FileSystemAbstraction fs = directory.getFileSystem(); + LocalMemoryTracker allocationTracker = new LocalMemoryTracker(); + File file = directory.file( "file" ); + try ( UnsafeDirectByteBufferFactory bufferFactory = new UnsafeDirectByteBufferFactory( allocationTracker ); + IndexKeyStorage keyStorage = keyStorage( file, bufferFactory ) ) + { + assertEquals( 0, allocationTracker.usedDirectMemory(), "Expected to not have any buffers allocated yet" ); + assertFalse( fs.fileExists( file ), "Expected file to be created lazily" ); + keyStorage.add( randomKey( 1 ) ); + assertEquals( BLOCK_SIZE, allocationTracker.usedDirectMemory(), "Expected to have exactly one buffer allocated by now" ); + assertTrue( fs.fileExists( file ), "Expected file to be created by now" ); + } + assertFalse( fs.fileExists( file ), "Expected file to be deleted on close" ); + } + + @Test + void shouldReportCorrectCount() throws IOException + { + try ( IndexKeyStorage keyStorage = keyStorage() ) + { + assertEquals( 0, keyStorage.count() ); + keyStorage.add( randomKey( 1 ) ); + assertEquals( 1, keyStorage.count() ); + keyStorage.add( randomKey( 2 ) ); + assertEquals( 2, keyStorage.count() ); + keyStorage.doneAdding(); + assertEquals( 2, keyStorage.count() ); + } + } + + private GenericKey randomKey( int entityId ) + { + GenericKey key = layout.newKey(); + key.initialize( entityId ); + for ( int i = 0; i < numberOfSlots; i++ ) + { + Value value = random.randomValues().nextValue(); + key.initFromValue( i, value, NEUTRAL ); + } + return key; + } + + private IndexKeyStorage keyStorage() throws IOException + { + return keyStorage( directory.file( "file" ) ); + } + + private IndexKeyStorage keyStorage( File file ) throws IOException + { + return keyStorage( file, HEAP_BUFFER_FACTORY ); + } + + private IndexKeyStorage keyStorage( File file, ByteBufferFactory bufferFactory ) throws IOException + { + return new IndexKeyStorage<>( directory.getFileSystem(), file, bufferFactory, BLOCK_SIZE, layout ); + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorageTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorageTest.java index 5d5cfc992d993..57fc52be75037 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorageTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/IndexUpdateStorageTest.java @@ -64,8 +64,9 @@ class IndexUpdateStorageTest void shouldAddZeroEntries() throws IOException { // given - try ( IndexUpdateStorage storage = new IndexUpdateStorage<>( layout, directory.getFileSystem(), directory.file( "file" ), - HEAP_BUFFER_FACTORY, 1000 ) ) + try ( IndexUpdateStorage storage = new IndexUpdateStorage<>( directory.getFileSystem(), directory.file( "file" ), + HEAP_BUFFER_FACTORY, 1000, layout + ) ) { // when List> expected = generateSomeUpdates( 0 ); @@ -80,8 +81,9 @@ void shouldAddZeroEntries() throws IOException void shouldAddFewEntries() throws IOException { // given - try ( IndexUpdateStorage storage = new IndexUpdateStorage<>( layout, directory.getFileSystem(), directory.file( "file" ), - HEAP_BUFFER_FACTORY, 1000 ) ) + try ( IndexUpdateStorage storage = new IndexUpdateStorage<>( directory.getFileSystem(), directory.file( "file" ), + HEAP_BUFFER_FACTORY, 1000, layout + ) ) { // when List> expected = generateSomeUpdates( 5 ); @@ -96,8 +98,9 @@ void shouldAddFewEntries() throws IOException void shouldAddManyEntries() throws IOException { // given - try ( IndexUpdateStorage storage = new IndexUpdateStorage<>( layout, directory.getFileSystem(), directory.file( "file" ), - HEAP_BUFFER_FACTORY, 1000 ) ) + try ( IndexUpdateStorage storage = new IndexUpdateStorage<>( directory.getFileSystem(), directory.file( "file" ), + HEAP_BUFFER_FACTORY, 1000, layout + ) ) { // when List> expected = generateSomeUpdates( 1_000 );