Repository: hiero-ledger/hiero-block-node
Pull request #1064: test: Files Historic Plugin Unit Tests Pt. 5
Author: login: ata-nas
Labels: name: pull request
description: label to get past the "label required" check when no label is needed or appropriate.
Created: 2025-04-24T11:24:56Z | Merged: 2025-04-29T11:08:14Z

## Reviewer Notes

- Part 5 of tests for the `Files Historic` Plugin.
- We test the actual plugin here.
- Introduced a new blocking serial executor service for testing purposes that will allow us to serially execute tasks in the order they have been submitted, all on the caller thread, this ensure that tests will always be in assertable state and debugging is a breeze.
- Found a bug when merging a range in a ranged set, we allow the `LongRange` record to have -1 for start and end, but the merging logic does not allow that, which produces a runtime exception when we initialize a ranged set with `-1 -> -1` and then we try to add anything to it, for example if we add `0 - 9` to it, the ranged set will attempt to produce the following values `-1 -> 9` which the constructor of the `LongRange` record does not allow. For now, I have handled the ranged set in the plugin, the available ranges to only insert anything if we have found some values initially when starting up the server. Otherwise the first insert will be after the first successful zip.
- Another bug was in the `start()` where the range was not correctly established
  - I have extracted and reused the zipping logic from the notification handling method
  - A concern here is again "where are the blocks coming from?"
  - I think we might need to extend the historical block facility method to provide us with accessors from a provider with some priority filter, but I am not sure as of yet if we really need that + if that will over complicate things, potentially hanging some places to wait for accessors that may never come but are available in other places. What are the tradeoffs?
- Am I missing some good cases that I can add as tests?

## Related Issue(s)

Closes #1039 


Linked issue #1039: Files Historic Plugin Unit Tests Pt. 5
### Story Form

As a Block Node Developer
I want to have unit tests for the `Files Historic` Plugin
So that I can have confidence in existing logic and catch regression

### Technical Notes

- part 5 of unit tests for the `Files Historic` Plugin

Diff:
diff --git a/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java b/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
new file mode 100644
index 0000000000..d6c59689ce
--- /dev/null
+++ b/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.async;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A very simple executor to be used only for testing! This executor will
+ * collect all submitted tasks to it and will hold them in order. After that
+ * when the {@link #executeSerially()} method is called, all tasks will be
+ * executed in order serially on the same thread that called the method. This
+ * will ensure that all tasks will complete before doing any asserts
+ */
+public class BlockingSerialExecutor extends ThreadPoolExecutor {
+    /** The work queue that will be used to hold the tasks. */
+    private final BlockingQueue<Runnable> workQueue;
+    /** Counter to indicate total submitted tasks. */
+    private int tasksSubmitted;
+
+    /**
+     * Constructor.
+     */
+    public BlockingSerialExecutor(@NonNull final BlockingQueue<Runnable> workQueue) {
+        // supply super with arbitrary values, they will not be used
+        super(
+                1,
+                1,
+                0L,
+                TimeUnit.MILLISECONDS,
+                Objects.requireNonNull(workQueue), // super will not use this queue
+                Executors.defaultThreadFactory(),
+                new AbortPolicy());
+        this.workQueue = workQueue; // actual work queue
+    }
+
+    /**
+     * Overriding this method in order to make sure this logic will be called
+     * every time we will submit a task to the pool.
+     */
+    @Override
+    @SuppressWarnings("all")
+    public void execute(@NonNull final Runnable command) {
+        workQueue.offer(command);
+        tasksSubmitted++;
+    }
+
+    /**
+     * Invoke this method once you have submitted all the tasks that need to be
+     * run by this executor. This method will then proceed to poll each task
+     * (in the same order that they have been submitted) and will execute the
+     * tasks serially on the same thread that called this method. Once this
+     * method returns (or throws), we are certain that all tasks have run and
+     * can safely assert based on that. This method will throw an
+     * {@link IllegalStateException} to indicate a broken state, when the queue
+     * is empty.
+     */
+    public void executeSerially() {
+        if (workQueue.isEmpty()) {
+            throw new IllegalStateException("Queue is empty");
+        } else {
+            while (!workQueue.isEmpty()) {
+                workQueue.poll().run();
+            }
+        }
+    }
+
+    /**
+     * This method indicates if any task was ever submitted to this executor.
+     * This is useful during tests in order to assert that the pool was
+     * essentially not interacted with. An example would be if we have a test
+     * where we want to assert that some production logic will never submit a
+     * task to the executor given some condition, then we can use this method
+     * to assert that. This method does not reflect the current state of the
+     * queue, meaning the queue might be empty due to a call to the
+     * {@link #executeSerially()} method, but this method will still return
+     * true if any task was submitted before that.
+     *
+     * @return boolean value, true if any task was ever submitted, false
+     * otherwise
+     */
+    public boolean wasAnyTaskSubmitted() {
+        return tasksSubmitted > 0;
+    }
+
+    @Override
+    public void close() {
+        shutdownNow();
+    }
+
+    @Override
+    public void shutdown() {
+        shutdownNow();
+    }
+
+    @Override
+    public boolean awaitTermination(final long timeout, final TimeUnit unit) {
+        shutdownNow();
+        return true;
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+}
diff --git a/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/NoOpServiceBuilder.java b/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/NoOpServiceBuilder.java
new file mode 100644
index 0000000000..9cb68ce916
--- /dev/null
+++ b/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/NoOpServiceBuilder.java
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.plugintest;
+
+import com.hedera.pbj.runtime.grpc.ServiceInterface;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.helidon.webserver.http.HttpService;
+import org.hiero.block.node.spi.ServiceBuilder;
+
+/**
+ * A simple no-op implementation of {@link ServiceBuilder} that does nothing.
+ * To be used for testing purposes only where we need a non-null implementation
+ * that we do not want to act upon.
+ */
+public final class NoOpServiceBuilder implements ServiceBuilder {
+    /**
+     * No-op implementation, does nothing.
+     */
+    @Override
+    public void registerHttpService(final String path, final HttpService... service) {
+        // No-op implementation, does nothing.
+    }
+
+    /**
+     * No-op implementation, does nothing.
+     */
+    @Override
+    public void registerGrpcService(@NonNull final ServiceInterface service) {
+        // No-op implementation, does nothing.
+    }
+}
diff --git a/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java b/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
index 6cecae4843..1ac808b402 100644
--- a/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
+++ b/block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
@@ -43,11 +43,16 @@ public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
      */
     @Override
     public void handleBlockItemsReceived(BlockItems blockItems) {
+        handleBlockItemsReceived(blockItems, true);
+    }
+
+    public void handleBlockItemsReceived(BlockItems blockItems, final boolean sendNotification) {
         if (!disablePlugin.get()) {
             if (blockItems.isStartOfNewBlock()) {
                 if (!partialBlock.isEmpty()) {
                     throw new RuntimeException(
-                            "Something went wrong, partitionedBlock is not empty. So we never got a end block for current block");
+                            "Something went wrong, partitionedBlock is not empty. So we never got a end "
+                                    + "block for current block");
                 }
                 currentBlockNumber.set(blockItems.newBlockNumber());
             }
@@ -63,9 +68,11 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
                 availableBlocks.add(blockNumber);
                 partialBlock.clear();
                 // send block persisted message
-                blockNodeContext
-                        .blockMessaging()
-                        .sendBlockPersisted(new PersistedNotification(blockNumber, blockNumber, 2000));
+                if (sendNotification) {
+                    blockNodeContext
+                            .blockMessaging()
+                            .sendBlockPersisted(new PersistedNotification(blockNumber, blockNumber, 2000));
+                }
             }
         }
     }
diff --git a/block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java b/block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
index 77a5b1a57e..6c74ed4e7d 100644
--- a/block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
+++ b/block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
@@ -6,6 +6,7 @@
 import java.lang.System.Logger.Level;
 import java.nio.file.Files;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -23,11 +24,11 @@
  * This plugin provides a block provider that stores historical blocks in file. It is designed to store them in the
  * most compressed optimal way possible. It is designed to be used with the
  */
-public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNotificationHandler {
+public final class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNotificationHandler {
     /** The logger for this class. */
     private final System.Logger LOGGER = System.getLogger(getClass().getName());
     /** The executor service for moving blocks to zip files in a background thread. */
-    private final ExecutorService zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    private final ExecutorService zipMoveExecutorService;
     /** The block node context. */
     private BlockNodeContext context;
     /** The zip block archive. */
@@ -39,6 +40,19 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
 
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    /**
+     * Constructor, used only for testing temporarily, need to introduce the
+     * executor factory method in the test context to remove this.
+     */
+    BlocksFilesHistoricPlugin(final ExecutorService zipMoveExecutorService) {
+        this.zipMoveExecutorService = Objects.requireNonNull(zipMoveExecutorService);
+    }
+
     // ==== BlockProviderPlugin Methods ================================================================================
 
     /**
@@ -54,8 +68,8 @@ public List<Class<? extends Record>> configDataTypes() {
      * {@inheritDoc}
      */
     @Override
-    public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
-        this.context = context;
+    public void init(final BlockNodeContext context, final ServiceBuilder serviceBuilder) {
+        this.context = Objects.requireNonNull(context);
         final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
         // create plugin data root directory if it does not exist
         try {
@@ -69,7 +83,19 @@ public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
         zipBlockArchive = new ZipBlockArchive(context, config);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
     }
 
     /**
@@ -77,21 +103,7 @@ public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
      */
     @Override
     public void start() {
-        // determine if there are any batches of blocks that need to be moved to zip files
-        // get the largest stored block number
-        long largestStoredBlockNumber =
-                context.historicalBlockProvider().availableBlocks().max();
-        if (largestStoredBlockNumber > availableBlocks.max()) {
-            // complete if there are any blocks that need to be moved to zip files
-            long startBlockNumber = availableBlocks.max() + 1;
-            while ((startBlockNumber + numberOfBlocksPerZipFile) < largestStoredBlockNumber) {
-                // move the batch of blocks to a zip file
-                startMovingBatchOfBlocksToZipFile(
-                        new LongRange(startBlockNumber, startBlockNumber + numberOfBlocksPerZipFile));
-                // move to next batch
-                startBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+        attemptZipping();
     }
 
     /**
@@ -133,24 +145,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
     }
 
     // ==== Private Methods ============================================================================================
+    private void attemptZipping() {
+        // compute the min and max block in next batch to zip
+        long minBlockNumber = availableBlocks().max() + 1;
+        long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
+        // check all those blocks are available
+        final BlockRangeSet historicalAvailable =
+                context.historicalBlockProvider().availableBlocks();
+        // while we can zip blocks, we must keep zipping
+        while (historicalAvailable.max() >= maxBlockNumber) {
+            if (historicalAvailable.contains(minBlockNumber, maxBlockNumber)) {
+                final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
+                // move the batch of blocks to a zip file
+                startMovingBatchOfBlocksToZipFile(batchRange);
+            }
+            // try the next batch just in case there is more than one that became available
+            minBlockNumber += numberOfBlocksPerZipFile;
+            maxBlockNumber += numberOfBlocksPerZipFile;
+        }
+    }
 
     /**
      * Start moving a batch of blocks to a zip file in background as long as batch is not already in progress or queued
@@ -190,26 +212,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
                     "Failed to move batch of blocks[" + batchFirstBlockNumber + " -> " + batchLastBlockNumber
                             + "] to zip file",
                     e);
+            return;
         }
+        // if we have reached here, then the batch of blocks has been zipped,
+        // now we need to make some updates
+        // update the first and last block numbers
+        availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
+        // log done
+        LOGGER.log(
+                System.Logger.Level.INFO,
+                "Moved batch of blocks[%d -> %d] to zip file",
+                batchFirstBlockNumber,
+                batchLastBlockNumber);
+        // now all the blocks are in the zip file and accessible, send notification
+        context.blockMessaging()
+                .sendBlockPersisted(
+                        new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
+        // remove the batch of blocks from in progress ranges
+        inProgressZipRanges.remove(batchRange);
     }
 }
diff --git a/block-node/block-providers/files.historic/src/test/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPluginTest.java b/block-node/block-providers/files.historic/src/test/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPluginTest.java
new file mode 100644
index 0000000000..29ddb52373
--- /dev/null
+++ b/block-node/block-providers/files.historic/src/test/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPluginTest.java
@@ -0,0 +1,728 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.blocks.files.historic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatNullPointerException;
+
+import com.hedera.pbj.runtime.ParseException;
+import com.hedera.pbj.runtime.io.buffer.Bytes;
+import com.swirlds.config.api.Configuration;
+import com.swirlds.config.api.ConfigurationBuilder;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.hiero.block.internal.BlockItemUnparsed;
+import org.hiero.block.internal.BlockUnparsed;
+import org.hiero.block.node.app.fixtures.async.BlockingSerialExecutor;
+import org.hiero.block.node.app.fixtures.blocks.SimpleTestBlockItemBuilder;
+import org.hiero.block.node.app.fixtures.plugintest.NoOpServiceBuilder;
+import org.hiero.block.node.app.fixtures.plugintest.PluginTestBase;
+import org.hiero.block.node.app.fixtures.plugintest.SimpleInMemoryHistoricalBlockFacility;
+import org.hiero.block.node.app.fixtures.plugintest.TestBlockMessagingFacility;
+import org.hiero.block.node.app.fixtures.plugintest.TestHealthFacility;
+import org.hiero.block.node.base.CompressionType;
+import org.hiero.block.node.spi.BlockNodeContext;
+import org.hiero.block.node.spi.ServiceBuilder;
+import org.hiero.block.node.spi.blockmessaging.BlockItems;
+import org.hiero.block.node.spi.blockmessaging.PersistedNotification;
+import org.hiero.block.node.spi.historicalblocks.BlockAccessor;
+import org.hiero.block.node.spi.historicalblocks.BlockRangeSet;
+import org.hiero.block.node.spi.historicalblocks.HistoricalBlockFacility;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Tests for {@link BlocksFilesHistoricPlugin}.
+ */
+@DisplayName("BlocksFilesHistoricPlugin Tests")
+class BlocksFilesHistoricPluginTest {
+    /** The test block messaging facility to use for testing. */
+    private final SimpleInMemoryHistoricalBlockFacility testHistoricalBlockFacility;
+    /** The test block serial executor service to use for the plugin. */
+    private final BlockingSerialExecutor pluginExecutor;
+    /** The test config to use for the plugin. */
+    private final FilesHistoricConfig testConfig;
+    /** The instance under test. */
+    private final BlocksFilesHistoricPlugin toTest;
+
+    /**
+     * Construct test environment.
+     */
+    BlocksFilesHistoricPluginTest(@TempDir final Path tempDir) {
+        Objects.requireNonNull(tempDir);
+        // create a blocking serial executor to run the plugin tasks that use
+        // an executor service internally
+        pluginExecutor = new BlockingSerialExecutor(new LinkedBlockingQueue<>());
+        // generate test config, for the purposes of this test, we will always
+        // use 10 blocks per zip, assuming that the first zip file will contain
+        // for example blocks 0-9, the second zip file will contain blocks 10-19
+        // also we will not use compression, and we will use the jUnit temp dir
+        testConfig = new FilesHistoricConfig(tempDir, CompressionType.NONE, 1);
+        // build the plugin using the test environment
+        toTest = new BlocksFilesHistoricPlugin(pluginExecutor);
+        // initialize an in memory historical block facility to use for testing
+        testHistoricalBlockFacility = new SimpleInMemoryHistoricalBlockFacility();
+    }
+
+    /**
+     * Teardown logic to run after each test.
+     */
+    @AfterEach
+    void tearDown() {
+        pluginExecutor.shutdownNow();
+    }
+
+    /**
+     * Constructor and Init tests.
+     */
+    @Nested
+    @DisplayName("Constructor & Init Tests")
+    final class ConstructorAndInitTests {
+        /**
+         * This test aims to verify that the no args constructor of
+         * {@link BlocksFilesHistoricPlugin} does not throw any exceptions.
+         */
+        @Test
+        @DisplayName("Test no args constructor does not throw any exceptions")
+        void testNoArgsConstructor() {
+            assertThatNoException().isThrownBy(BlocksFilesHistoricPlugin::new);
+        }
+
+        /**
+         * This test aims to verify that the
+         * {@link BlocksFilesHistoricPlugin#init(BlockNodeContext, ServiceBuilder)}
+         * method throws a {@link NullPointerException} if the context is null.
+         */
+        @Test
+        @DisplayName("Test init throws null pointer when supplied with null context")
+        void testInitNullContext() {
+            final BlocksFilesHistoricPlugin toTest = new BlocksFilesHistoricPlugin();
+            assertThatNullPointerException().isThrownBy(() -> toTest.init(null, new NoOpServiceBuilder()));
+        }
+
+        /**
+         * This test aims to verify that the
+         * {@link BlocksFilesHistoricPlugin#init(BlockNodeContext, ServiceBuilder)}
+         * method throws a {@link NullPointerException} if the context is null.
+         */
+        @Test
+        @DisplayName("Test init does not throw when ServiceBuilder is null (currently unused)")
+        void testInitNullServiceBuilder(@TempDir final Path tempDir) {
+            // setup a local valid context
+            final Configuration configuration = ConfigurationBuilder.create()
+                    .withConfigDataType(FilesHistoricConfig.class)
+                    .withValue("files.historic.rootPath", tempDir.toString())
+                    .build();
+            final HistoricalBlockFacility historicalBlockProvider = new SimpleInMemoryHistoricalBlockFacility();
+            final BlockNodeContext testContext = new BlockNodeContext(
+                    configuration,
+                    null,
+                    new TestHealthFacility(),
+                    new TestBlockMessagingFacility(),
+                    historicalBlockProvider,
+                    null);
+            // call
+            final BlocksFilesHistoricPlugin toTest = new BlocksFilesHistoricPlugin();
+            assertThatNoException().isThrownBy(() -> toTest.init(testContext, null));
+        }
+    }
+
+    /**
+     * Plugin tests.
+     */
+    @Nested
+    @DisplayName("Plugin Tests")
+    final class PluginTests extends PluginTestBase<BlocksFilesHistoricPlugin> {
+        /**
+         * Construct plugin base.
+         */
+        PluginTests() {
+            // match overrides to the test config
+            final Map<String, String> configOverrides = getConfigOverrides();
+            // initialize and start the test plugin using the config overrides
+            start(toTest, testHistoricalBlockFacility, configOverrides);
+        }
+
+        private Map<String, String> getConfigOverrides() {
+            final Entry<String, String> rootPath =
+                    Map.entry("files.historic.rootPath", testConfig.rootPath().toString());
+            final Entry<String, String> compression = Map.entry(
+                    "files.historic.compression", testConfig.compression().name());
+            final Entry<String, String> powersOfTenPerZipFileContents = Map.entry(
+                    "files.historic.powersOfTenPerZipFileContents",
+                    String.valueOf(testConfig.powersOfTenPerZipFileContents()));
+            return Map.ofEntries(rootPath, compression, powersOfTenPerZipFileContents);
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-9 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival")
+        void testZipRangeHappyPathArchival() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-19 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test for two full
+         * consecutive batches to be archived in a single notification. We expect
+         * all blocks to be archived.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival two full consecutive batches")
+        void testZipRangeHappyPathArchivalTwoFullBatches() throws IOException {
+            // generate first 20 blocks from numbers 0-19 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 20; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 20 blocks are zipped yet
+            for (int i = 0; i < 20; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 19, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 20 blocks are zipped now
+            for (int i = 0; i < 20; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-14 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test for two full
+         * consecutive batches to be archived in a single notification. We expect
+         * all blocks to be archived.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival batch and a half")
+        void testZipRangeHappyPathArchivalBatchAndAHalf() throws IOException {
+            // generate first 15 blocks from numbers 0-14 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 14; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 20 blocks are zipped yet
+            for (int i = 0; i < 14; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 14, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+            // assert that the next 5 blocks are not zipped however
+            for (int i = 10; i < 15; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-9 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test. We assert
+         * here the contents of each entry produce the same blocks as before
+         * archival.
+         */
+        @Test
+        @DisplayName("Test happy path zip archive contents")
+        void testZipRangeHappyPathArchiveContents() throws IOException, ParseException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            final List<BlockUnparsed> expectedBlocks = new ArrayList<>();
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+                expectedBlocks.add(new BlockUnparsed(List.of(block)));
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert the contents of the zip file
+            for (int i = 0; i < 10; i++) {
+                final BlockPath blockPath = BlockPath.computeExistingBlockPath(testConfig, i);
+                try (final ZipFile zipFile = new ZipFile(blockPath.zipFilePath().toFile())) {
+                    // assert that the zip file contains the block file
+                    final ZipEntry zipEntry = zipFile.getEntry(blockPath.blockFileName());
+                    assertThat(zipEntry).isNotNull();
+                    final byte[] zipEntryBytes =
+                            zipFile.getInputStream(zipEntry).readAllBytes();
+                    final BlockUnparsed actual = BlockUnparsed.PROTOBUF.parse(Bytes.wrap(zipEntryBytes));
+                    assertThat(actual).isEqualTo(expectedBlocks.get(i));
+                }
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-4 which does not fit the
+         * config of 10 blocks per zip), i.e. this is the happy path test.
+         * This test will assert the behavior of the plugin when we send
+         * multiple notifications, mimicking the scenario where the provider
+         * will receive and make available some of the blocks in the range
+         * we expect and at a later time it will make available more blocks in
+         * the range we expect. The plugin under test should be sure that the
+         * range it is following is covered before it will zip the blocks.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival on multiple notifications")
+        void testZipRangeWaitForEnoughAvailable() throws IOException {
+            // generate first 5 blocks from numbers 0-4 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 5; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 5 blocks are zipped yet
+            for (int i = 0; i < 5; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 4, toTest.defaultPriority() + 1));
+            // assert that no task has been submitted to the pool because we have
+            // not yet reached the desired amount of blocks we want to archive
+            final boolean anyTaskSubmitted = pluginExecutor.wasAnyTaskSubmitted();
+            assertThat(anyTaskSubmitted).isFalse();
+            // assert that the first 5 blocks do not exist
+            for (int i = 0; i < 5; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // generate the next 5 blocks from numbers 5-9 and add them to the
+            // test historical block facility
+            for (int i = 5; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(5, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not zip anything when
+         * a notification that covers the range of persisted blocks is sent
+         * from a provider that has a lower priority than the plugin we are
+         * testing. We expect that the plugin we test will not create a zip in
+         * those cases.
+         */
+        @Test
+        @DisplayName("Test no zip will be created when notification is from lower priority provider")
+        void testNoZipForLowerPriorityNotifications() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() - 1));
+            // assert that no zipping task was submitted
+            final boolean anyTaskSubmitted = pluginExecutor.wasAnyTaskSubmitted();
+            assertThat(anyTaskSubmitted).isFalse();
+            // assert that the first 10 blocks are not zipped
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not zip anything when
+         * a notification that covers the range of persisted blocks is sent
+         * from a provider that has the same priority as the plugin we are
+         * testing. We expect that the plugin we test will not create a zip in
+         * those cases.
+         */
+        @Test
+        @DisplayName("Test no zip will be created when notification is from same priority provider")
+        void testNoZipForSamePriorityNotifications() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority()));
+            // assert that no zipping task was submitted
+            final boolean anyTaskSubmitted = pluginExecutor.wasAnyTaskSubmitted();
+            assertThat(anyTaskSubmitted).isFalse();
+            // assert that the first 10 blocks are not zipped
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that a block accessor will be available for
+         * the blocks that have been zipped after they have been zipped.
+         */
+        @Test
+        @DisplayName("Test happy path zip block accessor")
+        void testZipRangeBlockAccessor() {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks have accessors yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks will have an accessor
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that available block accessors will contain
+         * the correct contents (or will rather be able to supply the correct
+         * contents) after the blocks have been zipped.
+         */
+        @Test
+        @DisplayName("Test happy path zip block accessor contents")
+        void testZipRangeBlockAccessorContents() {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            final List<BlockUnparsed> expectedBlocks = new ArrayList<>();
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+                expectedBlocks.add(new BlockUnparsed(List.of(block)));
+            }
+            // assert that none of the first 10 blocks have accessors yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert the contents of the now available block accessors
+            for (int i = 0; i < 10; i++) {
+                final BlockAccessor blockAccessor = toTest.block(i);
+                final BlockUnparsed actual = blockAccessor.blockUnparsed();
+                assertThat(actual).isEqualTo(expectedBlocks.get(i));
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will proceed to send a
+         * {@link PersistedNotification} with the correct range of blocks
+         * after a successful archival.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful notification sent")
+        void testZipRangeHappyPathNotificationSent() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that a persistence notification was sent, we expect 2
+            // notifications total, one in the beginning of this test and one
+            // sent by the plugin itself
+            final List<PersistedNotification> sentPersistedNotifications =
+                    blockMessaging.getSentPersistedNotifications();
+            assertThat(sentPersistedNotifications)
+                    .isNotEmpty()
+                    .hasSize(2)
+                    .element(1)
+                    .returns(0L, PersistedNotification::startBlockNumber)
+                    .returns(9L, PersistedNotification::endBlockNumber)
+                    .returns(toTest.defaultPriority(), PersistedNotification::blockProviderPriority);
+        }
+
+        /**
+         * This test aims to verify that the plugin will properly update it's
+         * available blocks list after a successful archival.
+         */
+        @Test
+        @DisplayName("Test happy path zip blocks in range")
+        void testZipRangeHappyPathBlocksInRange() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks appear in the available range
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.availableBlocks().contains(i)).isFalse();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks now appear in the available range
+            final BlockRangeSet availableBlocks = toTest.availableBlocks();
+            for (int i = 0; i < 10; i++) {
+                final int blockNumber = i;
+                assertThat(availableBlocks).returns(true, set -> set.contains(blockNumber));
+            }
+            assertThat(availableBlocks).returns(0L, BlockRangeSet::min).returns(9L, BlockRangeSet::max);
+        }
+
+        /**
+         * This test aims to verify that the plugin will not write data if
+         * an exception occurs just before actually writing anything.
+         */
+        @Test
+        @DisplayName("Test exception during move no data written")
+        void testExceptionDuringMoveNoDataWritten() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are not zipped, but revert proper
+            // perms so we can assert the file is deleted
+            assertThat(targetZipPath).isEmptyFile();
+        }
+
+        /**
+         * This test aims to verify that the plugin will not return any block
+         * accessors for any blocks in the batch that failed exceptionally.
+         */
+        @Test
+        @DisplayName("Test exception during move no accessors available")
+        void testExceptionDuringMoveNoAccessorsAvailable() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that no accessor will be returned for the blocks
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not update the range of
+         * available blocks with any block numbers of the blocks in the batch
+         * that failed exceptionally.
+         */
+        @Test
+        @DisplayName("Test exception during move no available blocks in range")
+        void testExceptionDuringMoveNoAvailableBlocksInRange() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // asser that available blocks do not contain any of the blocks
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.availableBlocks().contains(i)).isFalse();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not send any
+         * {@link PersistedNotification} for a zip that failed exceptionally.
+         */
+        @Test
+        @DisplayName("Test exception during move no persistence notification sent")
+        void testExceptionDuringMoveNoPersistenceNotificationSent() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that no notification was sent to the block messaging,
+            // we expect only one notification to have been sent, the one we
+            // initially sent in the beginning of the test
+            final int totalSentNotifications =
+                    blockMessaging.getSentPersistedNotifications().size();
+            assertThat(totalSentNotifications).isEqualTo(1);
+        }
+
+        /**
+         * This test aims to verify that the plugin will correctly zip the blocks
+         * that are available at the time of startup.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival from start()")
+        void testZipRangeHappyPathArchivalDuringStartup() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // assert that no task was ever submitted to the pool until now
+            assertThat(pluginExecutor.wasAnyTaskSubmitted()).isFalse();
+            // call the start method, we expect that it will queue a new task
+            // that we can execute
+            toTest.start();
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
+    }
+}


Reviews:
user: login: ata-nas
body: Self review to help other reviewers with the PR.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:00:49Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:15:47Z
user: login: jasperpotts
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:08:14Z
user: login: jasperpotts
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:37:13Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:42:34Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:50:20Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:54:19Z
user: login: Nana-EC
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T04:05:34Z
user: login: Nana-EC
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:47:55Z
user: login: Nana-EC
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:58:10Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:19:46Z
user: login: jsync-swirlds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T16:15:24Z
user: login: jsync-swirlds
body: Some thoughts and questions.  Nothing here is blocking.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T17:34:11Z
user: login: jasperpotts
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T00:48:49Z
user: login: jasperpotts
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T00:52:23Z
user: login: jasperpotts
body: Happy for merge as well but would be good to think about some of the issues brought up in future improvements .
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T00:54:38Z
user: login: jsync-swirlds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T00:59:12Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T07:41:21Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T07:45:16Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T07:50:03Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T07:54:39Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T08:00:09Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T08:01:08Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T08:05:11Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:12:21Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:16:56Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:43:20Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:44:45Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:54:38Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:59:49Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T11:59:57Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T12:00:34Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T12:01:14Z
user: login: Nana-EC
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T14:17:47Z
user: login: Nana-EC
body: Unblocking.
Please open a ticket around Historical Plugin that captures the comments or open requests in this PR so we can follow up.
If they are straightforward go ahead and tackle if not let's discuss as a team
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T14:20:34Z
user: login: Nana-EC
body: Left additional considerations.
Comments can be resolved once captured in a follow up ticket if applicable
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T15:13:16Z
user: login: georgi-l95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T15:24:36Z
user: login: georgi-l95
body: LG
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-28T15:24:46Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T17:25:26Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T17:25:32Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:31:55Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:45:10Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:53:54Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:54:36Z
user: login: ata-nas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T11:07:00Z

Review comments:
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: This is done because we are still waiting to be able to override the converters of the configs.
created_at: 2025-04-24T14:48:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: I have added  this because initially the min and max stored return a `-1`. We then create a `LongRange` of `-1 -> -1` which is accepted, but whenever we try to update this, the range should become `-1 -> n` which is not acceptible by the preconditions in the `LongRange` constructor. We should IMO remove the `-1 -> -1` possibility and only create ranges when we have an actual value to create. In this case this produces an exception when we try to update the set the second time (after the first run of an archive task is complete). Essentially, the merge method in the `ConcurrentLongRangeSet` at line 381 where it creates a new `LongRange` fails because the `mergedStart` remains -1 (which is expected and logical due to the fact that we try to merge `-1 -> -1` with some other values that are higher).

Opinions?
created_at: 2025-04-24T14:52:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
     }
 
     /**
      * On plugin start, check if there are any batches of blocks that need to be moved to zip files.
      */
     @Override
     public void start() {
-        // determine if there are any batches of blocks that need to be moved to zip files
-        // get the largest stored block number
-        long largestStoredBlockNumber =
-                context.historicalBlockProvider().availableBlocks().max();
-        if (largestStoredBlockNumber > availableBlocks.max()) {
-            // complete if there are any blocks that need to be moved to zip files
-            long startBlockNumber = availableBlocks.max() + 1;
-            while ((startBlockNumber + numberOfBlocksPerZipFile) < largestStoredBlockNumber) {
-                // move the batch of blocks to a zip file
-                startMovingBatchOfBlocksToZipFile(
-                        new LongRange(startBlockNumber, startBlockNumber + numberOfBlocksPerZipFile));
-                // move to next batch
-                startBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+        attemptZipping();
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: I have extracted this because we can reuse the same logic twice, once here and once in the start method. The start method's previous implementation had a few bugs and was not correctly calculating the ranges.
created_at: 2025-04-24T14:53:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +149,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: Is this a concern? The problem here might be that even if the notification is ok for the check to pass, later when we actually querry the accessors from the facility, we do not know from where each accessor is from (priority wise). So essentially lower down the drain we can still be taking accessors from place we might not want them from?
created_at: 2025-04-24T14:55:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
                     "Failed to move batch of blocks[" + batchFirstBlockNumber + " -> " + batchLastBlockNumber
                             + "] to zip file",
                     e);
+            return;
         }
+        // if we have reached here, then the batch of blocks has been zipped,
+        // now we need to make some updates
+        // update the first and last block numbers
+        availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
+        // log done
+        LOGGER.log(
+                System.Logger.Level.INFO,
+                "Moved batch of blocks[%d -> %d] to zip file",
+                batchFirstBlockNumber,
+                batchLastBlockNumber);
+        // now all the blocks are in the zip file and accessible, send notification
+        context.blockMessaging()
+                .sendBlockPersisted(
+                        new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
+        // remove the batch of blocks from in progress ranges
+        inProgressZipRanges.remove(batchRange);
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: I have extracted this so that the try catch will be smaller, but also in order to not do any roll back logic (like reverting available blocks changes if exception happens after that etc etc)

created_at: 2025-04-24T14:56:33Z
author_association: CONTRIBUTOR
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: ata-nas
body: I have re-introduced this. We had a blocking executor in the past that worked similarly to this one, but this one is actually serial and is called on the same thread. This means that this is way easier to debug and also allows us to do some neat asserts. Calling the `executeSerially()` ensures that after we pop from it, all tasks have run which means that asserts in the tests can be safely done every time (removing any potential flakiness), but the trade off is that running tasks serially is not the same as running them async and waiting (blocking) until all complete (which was a past implementation of this one). This **should be the better choice** here and also for other places, but in places where we have high concurrency and things are very sensitive, we may want to introduce the async runner blocking pool again.
created_at: 2025-04-24T15:00:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,647 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.blocks.files.historic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.hedera.pbj.runtime.ParseException;
+import com.hedera.pbj.runtime.io.buffer.Bytes;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.hiero.block.internal.BlockItemUnparsed;
+import org.hiero.block.internal.BlockUnparsed;
+import org.hiero.block.node.app.fixtures.async.BlockingSerialExecutor;
+import org.hiero.block.node.app.fixtures.blocks.SimpleTestBlockItemBuilder;
+import org.hiero.block.node.app.fixtures.plugintest.PluginTestBase;
+import org.hiero.block.node.app.fixtures.plugintest.SimpleInMemoryHistoricalBlockFacility;
+import org.hiero.block.node.base.CompressionType;
+import org.hiero.block.node.spi.blockmessaging.BlockItems;
+import org.hiero.block.node.spi.blockmessaging.PersistedNotification;
+import org.hiero.block.node.spi.historicalblocks.BlockAccessor;
+import org.hiero.block.node.spi.historicalblocks.BlockRangeSet;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Tests for {@link BlocksFilesHistoricPlugin}.
+ */
+@DisplayName("BlocksFilesHistoricPlugin Tests")
+class BlocksFilesHistoricPluginTest {
+    /** The test block messaging facility to use for testing. */
+    private final SimpleInMemoryHistoricalBlockFacility testHistoricalBlockFacility;
+    /** The test block serial executor service to use for the plugin. */
+    private final BlockingSerialExecutor pluginExecutor;
+    /** The test config to use for the plugin. */
+    private final FilesHistoricConfig testConfig;
+    /** The instance under test. */
+    private final BlocksFilesHistoricPlugin toTest;
+
+    /**
+     * Construct test environment.
+     */
+    BlocksFilesHistoricPluginTest(@TempDir final Path tempDir) {
+        Objects.requireNonNull(tempDir);
+        // create a blocking serial executor to run the plugin tasks that use
+        // an executor service internally
+        pluginExecutor = new BlockingSerialExecutor(new LinkedBlockingQueue<>());
+        // generate test config, for the purposes of this test, we will always
+        // use 10 blocks per zip, assuming that the first zip file will contain
+        // for example blocks 0-9, the second zip file will contain blocks 10-19
+        // also we will not use compression, and we will use the jUnit temp dir
+        testConfig = new FilesHistoricConfig(tempDir, CompressionType.NONE, 1);
+        // build the plugin using the test environment
+        toTest = new BlocksFilesHistoricPlugin(testConfig, pluginExecutor);
+        // initialize an in memory historical block facility to use for testing
+        testHistoricalBlockFacility = new SimpleInMemoryHistoricalBlockFacility();
+    }
+
+    /**
+     * Teardown logic to run after each test.
+     */
+    @AfterEach
+    void tearDown() {
+        pluginExecutor.shutdownNow();
+    }
+
+    /**
+     * Plugin tests.
+     */
+    @Nested
+    @DisplayName("Plugin Tests")
+    final class PluginTests extends PluginTestBase<BlocksFilesHistoricPlugin> {
+        /**
+         * Construct plugin base.
+         */
+        PluginTests() {
+            start(toTest, testHistoricalBlockFacility);
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-9 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival")
+        void testZipRangeHappyPathArchival() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-19 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test for two full
+         * consecutive batches to be archived in a single notification. We expect
+         * all blocks to be archived.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival two full consecutive batches")
+        void testZipRangeHappyPathArchivalTwoFullBatches() throws IOException {
+            // generate first 20 blocks from numbers 0-19 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 20; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 20 blocks are zipped yet
+            for (int i = 0; i < 20; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 19, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 20 blocks are zipped now
+            for (int i = 0; i < 20; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-14 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test for two full
+         * consecutive batches to be archived in a single notification. We expect
+         * all blocks to be archived.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival batch and a half")
+        void testZipRangeHappyPathArchivalBatchAndAHalf() throws IOException {
+            // generate first 15 blocks from numbers 0-14 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 14; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 20 blocks are zipped yet
+            for (int i = 0; i < 14; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 14, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+            // assert that the next 5 blocks are not zipped however
+            for (int i = 10; i < 15; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-9 which fits the config
+         * of 10 blocks per zip), i.e. this is the happy path test. We assert
+         * here the contents of each entry produce the same blocks as before
+         * archival.
+         */
+        @Test
+        @DisplayName("Test happy path zip archive contents")
+        void testZipRangeHappyPathArchiveContents() throws IOException, ParseException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            final List<BlockUnparsed> expectedBlocks = new ArrayList<>();
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+                expectedBlocks.add(new BlockUnparsed(List.of(block)));
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert the contents of the zip file
+            for (int i = 0; i < 10; i++) {
+                final BlockPath blockPath = BlockPath.computeExistingBlockPath(testConfig, i);
+                try (final ZipFile zipFile = new ZipFile(blockPath.zipFilePath().toFile())) {
+                    // assert that the zip file contains the block file
+                    final ZipEntry zipEntry = zipFile.getEntry(blockPath.blockFileName());
+                    assertThat(zipEntry).isNotNull();
+                    final byte[] zipEntryBytes =
+                            zipFile.getInputStream(zipEntry).readAllBytes();
+                    final BlockUnparsed actual = BlockUnparsed.PROTOBUF.parse(Bytes.wrap(zipEntryBytes));
+                    assertThat(actual).isEqualTo(expectedBlocks.get(i));
+                }
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin can handle a simple range of
+         * blocks that have been persisted and a notification is sent to the
+         * messaging facility. The block provider that has persisted the blocks
+         * must have a higher priority than the plugin we are testing. We expect
+         * that the plugin we test will create a zip file with all the blocks in
+         * the notification range (we set the range to 0-4 which does not fit the
+         * config of 10 blocks per zip), i.e. this is the happy path test.
+         * This test will assert the behavior of the plugin when we send
+         * multiple notifications, mimicking the scenario where the provider
+         * will receive and make available some of the blocks in the range
+         * we expect and at a later time it will make available more blocks in
+         * the range we expect. The plugin under test should be sure that the
+         * range it is following is covered before it will zip the blocks.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival on multiple notifications")
+        void testZipRangeWaitForEnoughAvailable() throws IOException {
+            // generate first 5 blocks from numbers 0-4 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 5; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 5 blocks are zipped yet
+            for (int i = 0; i < 5; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 4, toTest.defaultPriority() + 1));
+            // assert that no task has been submitted to the pool because we have
+            // not yet reached the desired amount of blocks we want to archive
+            final boolean anyTaskSubmitted = pluginExecutor.wasAnyTaskSubmitted();
+            assertThat(anyTaskSubmitted).isFalse();
+            // assert that the first 5 blocks do not exist
+            for (int i = 0; i < 5; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // generate the next 5 blocks from numbers 5-9 and add them to the
+            // test historical block facility
+            for (int i = 5; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(5, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not zip anything when
+         * a notification that covers the range of persisted blocks is sent
+         * from a provider that has a lower priority than the plugin we are
+         * testing. We expect that the plugin we test will not create a zip in
+         * those cases.
+         */
+        @Test
+        @DisplayName("Test no zip will be created when notification is from lower priority provider")
+        void testNoZipForLowerPriorityNotifications() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() - 1));
+            // assert that no zipping task was submitted
+            final boolean anyTaskSubmitted = pluginExecutor.wasAnyTaskSubmitted();
+            assertThat(anyTaskSubmitted).isFalse();
+            // assert that the first 10 blocks are not zipped
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not zip anything when
+         * a notification that covers the range of persisted blocks is sent
+         * from a provider that has the same priority as the plugin we are
+         * testing. We expect that the plugin we test will not create a zip in
+         * those cases.
+         */
+        @Test
+        @DisplayName("Test no zip will be created when notification is from same priority provider")
+        void testNoZipForSamePriorityNotifications() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority()));
+            // assert that no zipping task was submitted
+            final boolean anyTaskSubmitted = pluginExecutor.wasAnyTaskSubmitted();
+            assertThat(anyTaskSubmitted).isFalse();
+            // assert that the first 10 blocks are not zipped
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that a block accessor will be available for
+         * the blocks that have been zipped after they have been zipped.
+         */
+        @Test
+        @DisplayName("Test happy path zip block accessor")
+        void testZipRangeBlockAccessor() {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks have accessors yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks will have an accessor
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNotNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that available block accessors will contain
+         * the correct contents (or will rather be able to supply the correct
+         * contents) after the blocks have been zipped.
+         */
+        @Test
+        @DisplayName("Test happy path zip block accessor contents")
+        void testZipRangeBlockAccessorContents() {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            final List<BlockUnparsed> expectedBlocks = new ArrayList<>();
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+                expectedBlocks.add(new BlockUnparsed(List.of(block)));
+            }
+            // assert that none of the first 10 blocks have accessors yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert the contents of the now available block accessors
+            for (int i = 0; i < 10; i++) {
+                final BlockAccessor blockAccessor = toTest.block(i);
+                final BlockUnparsed actual = blockAccessor.blockUnparsed();
+                assertThat(actual).isEqualTo(expectedBlocks.get(i));
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will proceed to send a
+         * {@link PersistedNotification} with the correct range of blocks
+         * after a successful archival.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful notification sent")
+        void testZipRangeHappyPathNotificationSent() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that a persistence notification was sent, we expect 2
+            // notifications total, one in the beginning of this test and one
+            // sent by the plugin itself
+            final List<PersistedNotification> sentPersistedNotifications =
+                    blockMessaging.getSentPersistedNotifications();
+            assertThat(sentPersistedNotifications)
+                    .isNotEmpty()
+                    .hasSize(2)
+                    .element(1)
+                    .returns(0L, PersistedNotification::startBlockNumber)
+                    .returns(9L, PersistedNotification::endBlockNumber)
+                    .returns(toTest.defaultPriority(), PersistedNotification::blockProviderPriority);
+        }
+
+        /**
+         * This test aims to verify that the plugin will properly update it's
+         * available blocks list after a successful archival.
+         */
+        @Test
+        @DisplayName("Test happy path zip blocks in range")
+        void testZipRangeHappyPathBlocksInRange() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks appear in the available range
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.availableBlocks().contains(i)).isFalse();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks now appear in the available range
+            final BlockRangeSet availableBlocks = toTest.availableBlocks();
+            for (int i = 0; i < 10; i++) {
+                final int blockNumber = i;
+                assertThat(availableBlocks).returns(true, set -> set.contains(blockNumber));
+            }
+            assertThat(availableBlocks).returns(0L, BlockRangeSet::min).returns(9L, BlockRangeSet::max);
+        }
+
+        /**
+         * This test aims to verify that the plugin will not write data if
+         * an exception occurs just before actually writing anything.
+         */
+        @Test
+        @DisplayName("Test exception during move no data written")
+        void testExceptionDuringMoveNoDataWritten() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are not zipped, but revert proper
+            // perms so we can assert the file is deleted
+            assertThat(targetZipPath).isEmptyFile();
+        }
+
+        /**
+         * This test aims to verify that the plugin will not return any block
+         * accessors for any blocks in the batch that failed exceptionally.
+         */
+        @Test
+        @DisplayName("Test exception during move no accessors available")
+        void testExceptionDuringMoveNoAccessorsAvailable() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that no accessor will be returned for the blocks
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.block(i)).isNull();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not update the range of
+         * available blocks with any block numbers of the blocks in the batch
+         * that failed exceptionally.
+         */
+        @Test
+        @DisplayName("Test exception during move no available blocks in range")
+        void testExceptionDuringMoveNoAvailableBlocksInRange() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // asser that available blocks do not contain any of the blocks
+            for (int i = 0; i < 10; i++) {
+                assertThat(toTest.availableBlocks().contains(i)).isFalse();
+            }
+        }
+
+        /**
+         * This test aims to verify that the plugin will not send any
+         * {@link PersistedNotification} for a zip that failed exceptionally.
+         */
+        @Test
+        @DisplayName("Test exception during move no persistence notification sent")
+        void testExceptionDuringMoveNoPersistenceNotificationSent() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // send a block persisted notification for the range we just created
+            blockMessaging.sendBlockPersisted(new PersistedNotification(0, 9, toTest.defaultPriority() + 1));
+            // calculate the target zip path that we expect the plugin to create
+            final Path targetZipPath = BlockPath.computeBlockPath(testConfig, 0).zipFilePath();
+            Files.createDirectories(targetZipPath.getParent());
+            // create the file with no permissions to simulate a failure later on
+            Files.createFile(targetZipPath);
+            Files.setPosixFilePermissions(targetZipPath, Collections.emptySet());
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that no notification was sent to the block messaging,
+            // we expect only one notification to have been sent, the one we
+            // initially sent in the beginning of the test
+            final int totalSentNotifications =
+                    blockMessaging.getSentPersistedNotifications().size();
+            assertThat(totalSentNotifications).isEqualTo(1);
+        }
+
+        /**
+         * This test aims to verify that the plugin will correctly zip the blocks
+         * that are available at the time of startup.
+         */
+        @Test
+        @DisplayName("Test happy path zip successful archival from start()")
+        void testZipRangeHappyPathArchivalDuringStartup() throws IOException {
+            // generate first 10 blocks from numbers 0-9 and add them to the
+            // test historical block facility
+            for (int i = 0; i < 10; i++) {
+                final BlockItemUnparsed[] block = SimpleTestBlockItemBuilder.createSimpleBlockUnparsedWithNumber(i);
+                testHistoricalBlockFacility.handleBlockItemsReceived(new BlockItems(List.of(block), i), false);
+            }
+            // assert that none of the first 10 blocks are zipped yet
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNull();
+            }
+            // assert that no task was ever submitted to the pool until now
+            assertThat(pluginExecutor.wasAnyTaskSubmitted()).isFalse();
+            // call the start method, we expect that it will queue a new task
+            // that we can execute
+            toTest.start();
+            // execute serially to ensure all tasks are completed
+            pluginExecutor.executeSerially();
+            // assert that the first 10 blocks are zipped now
+            for (int i = 0; i < 10; i++) {
+                assertThat(BlockPath.computeExistingBlockPath(testConfig, i)).isNotNull();
+            }
+        }
path: block-node/block-providers/files.historic/src/test/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPluginTest.java
user: login: ata-nas
body: The `start()` reuses the same logic as the notification handler method. I have added only the happy path here, I see no value in having all the tests done again, but called from the start method. This should be sufficient.
created_at: 2025-04-24T15:15:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    /** Constructor, used only for testing temporarily while we wait for
+     * extended functionality to support overriding config converters
+     * PR #18617 in hiero-consensus-node.
+     */
+    BlocksFilesHistoricPlugin(final FilesHistoricConfig config, final ExecutorService zipMoveExecutorService) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jasperpotts
body: I don't love adding API/Code to production for testing. This burnt us in the last week on consensus node as we could not do something because of the way a class was using the code. This was a huge problem and after many hours investigation we found the only reason the class existed still that was using that code, was because it was used by a for test only method. So we almost said we could not deliver a feature because it was impossible, just because of an old for test method that should have gone away.

I like your executor service swap. For config I added ability to override config for plugin test classes. I wonder if we should have a createExecutorService(int numThreads, String namePrefix) to BlockNodeContext. That way we can add central tracking, startup logging of number of threads created and by who. We have issue in consensus node that we create executors all over the place and do not track how many threads are created or by who. We have a plan to add ThreadManager one day there but not got to it today. If we added that to context then in plugin test it can swap out the executor in test fixture and require no special code in production plugin. Thoughts? 
created_at: 2025-04-24T16:08:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jasperpotts
body: This brings up a big design question. That would be huge to change but worth asking the question. That is:

inclusive vs exclusive for all block number ranges in block node?

So far it is "inclusive" everywhere which is fine other than it doesn't allow for an empty range as 0->0 includes 0 rather than being empty. If we move to exclusive then we could avoid `-1`s to have empty. This would be a horrible change to code base but easier now than later. As code base will only get bigger 😃
created_at: 2025-04-24T16:37:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: I think maybe this is simpler. The way I see it that currently it is fine and behaves as expected (problem is only when we have `-1 -> -1` initially and then we want to add any value). Inclusive is I think the right way to go (as it is now). So for instance as you mention `0 -> 0` will return true only if we query for a 0. To me the range set class is empty when it has no `LongRange` instance internally in it. In fact this is why the code change is working. When the ranged set is instantiated initially, it is empty and retruns false on everything for the sole reason that there are no `LongRange` instances inside it. So, for an empty ranged set, can we simply have a set that has no `LongRange` instances inside of it (this is an implementation detail for the `ConcurrentLongRangeSet` we currently use). Am I missing something?
created_at: 2025-04-24T16:42:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    /** Constructor, used only for testing temporarily while we wait for
+     * extended functionality to support overriding config converters
+     * PR #18617 in hiero-consensus-node.
+     */
+    BlocksFilesHistoricPlugin(final FilesHistoricConfig config, final ExecutorService zipMoveExecutorService) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: If by adding code you meant the constructor, I completely agree. We were waiting on the changes (still waiting I think) for the custom converters. But in this case I am using the jUnit temp dir because jimfs does not support the `File` abstraction unfortunately and it is used for the zips :/ But yeah, looks like you did introduce some new logic with latest merge to main, I will check it out and attempt to pass the path through there.

As far as the executor service factory method, I really like that as well. It will neatly fit into the testing scenarios as well, since we will not have to pass test instance of executors through constructors and introduce "test only" logic in production like it happened now. So if we make the change you suggested, we can rework this as well. Shall I attempt that tomorrow or would you like to take a look into that factory method?
created_at: 2025-04-24T16:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
                     "Failed to move batch of blocks[" + batchFirstBlockNumber + " -> " + batchLastBlockNumber
                             + "] to zip file",
                     e);
+            return;
         }
+        // if we have reached here, then the batch of blocks has been zipped,
+        // now we need to make some updates
+        // update the first and last block numbers
+        availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
+        // log done
+        LOGGER.log(
+                System.Logger.Level.INFO,
+                "Moved batch of blocks[%d -> %d] to zip file",
+                batchFirstBlockNumber,
+                batchLastBlockNumber);
+        // now all the blocks are in the zip file and accessible, send notification
+        context.blockMessaging()
+                .sendBlockPersisted(
+                        new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
+        // remove the batch of blocks from in progress ranges
+        inProgressZipRanges.remove(batchRange);
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: P.S. do we want to catch any exceptions produced by the calls here? If so, we can revert, but we need to make sure that in the catch, we also leave the plugin in a proper state, reverting available blocks changes, not removing in progress ranges etc etc
created_at: 2025-04-24T16:54:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    /** Constructor, used only for testing temporarily while we wait for
+     * extended functionality to support overriding config converters
+     * PR #18617 in hiero-consensus-node.
+     */
+    BlocksFilesHistoricPlugin(final FilesHistoricConfig config, final ExecutorService zipMoveExecutorService) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: Nana-EC
body: I also like the factory within the context idea to centralize thread management.
@ata-nas let's open a ticket for that and add it to the sprint backlog and verification of refactor code EPIC.
If it becomes required you can pick it up next week otherwise we can get to it soon.

created_at: 2025-04-25T04:05:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: Nana-EC
body: I'd also prefer inclusive. 
Although I'm not a fan of the support for start = end in the range class without practical usage.
What's the value of an empty range?
created_at: 2025-04-25T11:47:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +149,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: Nana-EC
body: Definitely something to watch.
I believe right now priority may not be accurately set on new notifications. 
I see defaultPriority set but not much beyond that. Need to look more
created_at: 2025-04-25T11:58:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    /** Constructor, used only for testing temporarily while we wait for
+     * extended functionality to support overriding config converters
+     * PR #18617 in hiero-consensus-node.
+     */
+    BlocksFilesHistoricPlugin(final FilesHistoricConfig config, final ExecutorService zipMoveExecutorService) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: @Nana-EC as requested, I have created the issue: #1067
created_at: 2025-04-25T12:19:46Z
author_association: CONTRIBUTOR
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: jsync-swirlds
body: One small (possible) improvement would be to have the serial executor use its own thread to execute all the tasks, and when you call `executeSerially` you `join` that thread instead of executing everything on the original thread.  That helps flesh out more possible asynchronous bugs and also avoids some very-hard-to-diagnose deadlock opportunities.
created_at: 2025-04-25T16:15:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.async;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A very simple executor to be used only for testing! This executor will
+ * collect all submitted tasks to it and will hold them in order. After that
+ * when the {@link #executeSerially()} method is called, all tasks will be
+ * executed in order serially on the same thread that called the method. This
+ * will ensure that all tasks will complete before doing any asserts
+ */
+public class BlockingSerialExecutor extends ThreadPoolExecutor {
+    /** The work queue that will be used to hold the tasks. */
+    private final BlockingQueue<Runnable> workQueue;
+    /** Counter to indicate total submitted tasks. */
+    private int tasksSubmitted;
+
+    /**
+     * Constructor.
+     */
+    public BlockingSerialExecutor(@NonNull final BlockingQueue<Runnable> workQueue) {
+        // supply super with arbitrary values, they will not be used
+        super(
+                1,
+                1,
+                0L,
+                TimeUnit.MILLISECONDS,
+                Objects.requireNonNull(workQueue), // super will not use this queue
+                Executors.defaultThreadFactory(),
+                new AbortPolicy());
+        this.workQueue = workQueue; // actual work queue
+    }
+
+    /**
+     * Overriding this method in order to make sure this logic will be called
+     * every time we will submit a task to the pool.
+     */
+    @Override
+    @SuppressWarnings("all")
+    public void execute(@NonNull final Runnable command) {
+        workQueue.offer(command);
+        tasksSubmitted++;
+    }
+
+    /**
+     * Invoke this method once you have submitted all the tasks that need to be
+     * run by this executor. This method will then proceed to poll each task
+     * (in the same order that they have been submitted) and will execute the
+     * tasks serially on the same thread that called this method. Once this
+     * method returns (or throws), we are certain that all tasks have run and
+     * can safely assert based on that. This method will throw an
+     * {@link IllegalStateException} to indicate a broken state, when the queue
+     * is empty.
+     */
+    public void executeSerially() {
+        if (workQueue.isEmpty()) {
+            throw new IllegalStateException("Queue is empty");
+        } else {
+            while (!workQueue.isEmpty()) {
+                workQueue.poll().run();
+            }
+        }
+    }
+
+    /**
+     * This method indicates if any task was ever submitted to this executor.
+     * This is useful during tests in order to assert that the pool was
+     * essentially not interacted with. An example would be if we have a test
+     * where we want to assert that some production logic will never submit a
+     * task to the executor given some condition, then we can use this method
+     * to assert that. This method does not reflect the current state of the
+     * queue, meaning the queue might be empty due to a call to the
+     * {@link #executeSerially()} method, but this method will still return
+     * true if any task was submitted before that.
+     *
+     * @return boolean value, true if any task was ever submitted, false
+     * otherwise
+     */
+    public boolean wasAnyTaskSubmitted() {
+        return tasksSubmitted > 0;
+    }
+
+    @Override
+    public void close() {
+        shutdownNow();
+    }
+
+    @Override
+    public void shutdown() {
+        shutdownNow();
+    }
+
+    @Override
+    public boolean awaitTermination(final long timeout, final TimeUnit unit) {
+        shutdownNow();
+        return true;
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: jsync-swirlds
body: Just a thought.
Instead of the custom `executeSerially` method, we could use these two as follows:
1. build a list from the work queue
    * Also create an empty "results" list.
2. for each item in the list, execute that item and put it's (completed) Future (e.g. `list.add(new CompletedFuture(task.call())`) in the results list.
3. Return the results list.  As every Future is already completed in that list, the results are immediately available.

This is not necessarily appropriate for this PR, but perhaps worth documenting for follow-up.
created_at: 2025-04-25T17:15:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,9 +65,11 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
             availableBlocks.add(blockNumber);
             partialBlock.clear();
             // send block persisted message
-            blockNodeContext
-                    .blockMessaging()
-                    .sendBlockPersisted(new PersistedNotification(blockNumber, blockNumber, 2000));
+            if (sendNotification) {
+                blockNodeContext
+                        .blockMessaging()
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
user: login: jsync-swirlds
body: What is the magic number `2000` for?  Should that be a constant or configuration item?
created_at: 2025-04-25T17:16:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jsync-swirlds
body: This definitely should be created via Context or some other provided factory...
Ideally we wouldn't be creating single thread executors in general, and would use task pools, optionally with virtual threads.
created_at: 2025-04-25T17:19:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jsync-swirlds
body: This raises a question that the team should probably discuss.
the `ERROR` level is usually a "wake the team" log level, and unless the block node is in a failure state, we probably shouldn't use it.
I would recommend `WARNING` here (as this is serious, but not fatal); but I think the team should discuss (and then document) what the 7 log levels mean for this project and when to use them.
created_at: 2025-04-25T17:30:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
                     "Failed to move batch of blocks[" + batchFirstBlockNumber + " -> " + batchLastBlockNumber
                             + "] to zip file",
                     e);
+            return;
         }
+        // if we have reached here, then the batch of blocks has been zipped,
+        // now we need to make some updates
+        // update the first and last block numbers
+        availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
+        // log done
+        LOGGER.log(
+                System.Logger.Level.INFO,
+                "Moved batch of blocks[%d -> %d] to zip file",
+                batchFirstBlockNumber,
+                batchLastBlockNumber);
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jsync-swirlds
body: This is another log level question (there are others, but I'll stop with this one).
INFO is a significant level, and (contrary to the name) should only contain _significant_ events (e.g. things that require operations attention).
This log (which is "we successfully did the thing we are supposed to do") is more often a `TRACE` level, or `DEBUG` at most.
created_at: 2025-04-25T17:32:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jasperpotts
body: I just saw your comment and made me question the inclusive decision. I have been fixing anywhere I had seen ranges of block numbers even in test fixtures to always be inclusive so we do not have to think on case by case basis and know if working with block numbers and in block node code base it is always inclusive. So strongly feel we should have one kind everywhere. Just wanted to make sure I had not missed something and chosen the wrong one. 
created_at: 2025-04-26T00:48:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    /** Constructor, used only for testing temporarily while we wait for
+     * extended functionality to support overriding config converters
+     * PR #18617 in hiero-consensus-node.
+     */
+    BlocksFilesHistoricPlugin(final FilesHistoricConfig config, final ExecutorService zipMoveExecutorService) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jasperpotts
body: I think you need both executor factory method in context and the new method for config override that I added. Just need to make sure it works with non-string values. It may not. In which case we need the PR from Hendrik and should push to get it reviewed and merged.
created_at: 2025-04-26T00:52:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: jsync-swirlds
body: 1. I agree we should have one decision everywhere if possible, and avoid the confusion possible with multiple decisions.
1. inclusive of both start and end seems the cleanest approach to me as well, it's closer to basic intuition and less likely to be misunderstood.
created_at: 2025-04-26T00:59:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: Good discussion.

Here are a couple of thoughts summarized:
1. I think ranges should be positive or zero
1. I think ranges should be inclusive always
1. I think we should not have the notion of an `empty` range, but simply one that not exist, this will drastically reduce complexity and fix current issues with exceptions thrown when merging a range that starts with `-1` with another range
1. I also agree that we should agree upon a single approach everywhere

As an action item in a follow up:
1. I think we should remove the possibility to have `-1 -> -1` range (`LongRange`) and treat it as an empty range
1. Update logic to support the removal of the `-1 -> -1` "empty" range
1. The ranged sets will be empty (behave like we would expect them to behave empty) when they simply have no ranges (`LongRange`) currently inside them.

I think that these changes will drastically reduce complexity (already it is working because of the change I made, but we need to update the logic to ensure no negative ranges and agree to switch to the new approach) and do cover our needs.

@jasperpotts @Nana-EC @jsync-swirlds opinions?

P.S. I guess the question really is do we really need an "empty" range like we have one now or can we say that if there is no range (`null`) we treat that as a set that is empty. I kind of think of the `LongRange` class as something that is a property of the sets rather than a standalone thing (although technically it could and does exist without being a part of the set). 
created_at: 2025-04-28T07:41:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +149,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: @Nana-EC I think of making an issue for this so we can not lose this. We can chose to do something about this in the future as needed.
created_at: 2025-04-28T07:45:16Z
author_association: CONTRIBUTOR
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: ata-nas
body: Neat.
I think that we can extend the executor to support both as well. Let's think about this in the future, there are a couple of ways to do this and depending on what we need, we can either support multiple simultaneous calls of the `executeSerially` method or one only. It is hard for me to think about that without having the actual need to do so. In the test environment I do see the need to have both options as some asserts (and/or behavior) can be done via one option or the other.
created_at: 2025-04-28T07:50:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.async;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A very simple executor to be used only for testing! This executor will
+ * collect all submitted tasks to it and will hold them in order. After that
+ * when the {@link #executeSerially()} method is called, all tasks will be
+ * executed in order serially on the same thread that called the method. This
+ * will ensure that all tasks will complete before doing any asserts
+ */
+public class BlockingSerialExecutor extends ThreadPoolExecutor {
+    /** The work queue that will be used to hold the tasks. */
+    private final BlockingQueue<Runnable> workQueue;
+    /** Counter to indicate total submitted tasks. */
+    private int tasksSubmitted;
+
+    /**
+     * Constructor.
+     */
+    public BlockingSerialExecutor(@NonNull final BlockingQueue<Runnable> workQueue) {
+        // supply super with arbitrary values, they will not be used
+        super(
+                1,
+                1,
+                0L,
+                TimeUnit.MILLISECONDS,
+                Objects.requireNonNull(workQueue), // super will not use this queue
+                Executors.defaultThreadFactory(),
+                new AbortPolicy());
+        this.workQueue = workQueue; // actual work queue
+    }
+
+    /**
+     * Overriding this method in order to make sure this logic will be called
+     * every time we will submit a task to the pool.
+     */
+    @Override
+    @SuppressWarnings("all")
+    public void execute(@NonNull final Runnable command) {
+        workQueue.offer(command);
+        tasksSubmitted++;
+    }
+
+    /**
+     * Invoke this method once you have submitted all the tasks that need to be
+     * run by this executor. This method will then proceed to poll each task
+     * (in the same order that they have been submitted) and will execute the
+     * tasks serially on the same thread that called this method. Once this
+     * method returns (or throws), we are certain that all tasks have run and
+     * can safely assert based on that. This method will throw an
+     * {@link IllegalStateException} to indicate a broken state, when the queue
+     * is empty.
+     */
+    public void executeSerially() {
+        if (workQueue.isEmpty()) {
+            throw new IllegalStateException("Queue is empty");
+        } else {
+            while (!workQueue.isEmpty()) {
+                workQueue.poll().run();
+            }
+        }
+    }
+
+    /**
+     * This method indicates if any task was ever submitted to this executor.
+     * This is useful during tests in order to assert that the pool was
+     * essentially not interacted with. An example would be if we have a test
+     * where we want to assert that some production logic will never submit a
+     * task to the executor given some condition, then we can use this method
+     * to assert that. This method does not reflect the current state of the
+     * queue, meaning the queue might be empty due to a call to the
+     * {@link #executeSerially()} method, but this method will still return
+     * true if any task was submitted before that.
+     *
+     * @return boolean value, true if any task was ever submitted, false
+     * otherwise
+     */
+    public boolean wasAnyTaskSubmitted() {
+        return tasksSubmitted > 0;
+    }
+
+    @Override
+    public void close() {
+        shutdownNow();
+    }
+
+    @Override
+    public void shutdown() {
+        shutdownNow();
+    }
+
+    @Override
+    public boolean awaitTermination(final long timeout, final TimeUnit unit) {
+        shutdownNow();
+        return true;
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: ata-nas
body: Cool idea. I am thinking of adding an issue to extend/improve the executor. We will need the extensions for sure in the future and I want to capture your ideas here. I hope that this class will be our go-to executor when testing and as @jasperpotts suggested, it will be injectable via the context once we add that support as well. This means that we will no longer need to have non-production logic just to facilitate the possibility to test. 🔥 
created_at: 2025-04-28T07:54:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,9 +65,11 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
             availableBlocks.add(blockNumber);
             partialBlock.clear();
             // send block persisted message
-            blockNodeContext
-                    .blockMessaging()
-                    .sendBlockPersisted(new PersistedNotification(blockNumber, blockNumber, 2000));
+            if (sendNotification) {
+                blockNodeContext
+                        .blockMessaging()
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
user: login: ata-nas
body: Interesting, well this is from the past looks like. Since this class is not a `BlockProviderPlugin`, it does not implement the `BlockProviderPlugin#defaultPriority`. Technically the historical block facility consists of multiple registered plugins which have the default priority and execute their logic respectively. Here this number is I believe one that fits the test cases we have up until now. I believe we should be setting this via the constructor so we will have control over this and we are mindful (my fear is that not having the correct number might produce false positives in our tests). Good catch @jsync-swirlds 🚀 
created_at: 2025-04-28T08:00:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: We have a ticket for that #1067
created_at: 2025-04-28T08:01:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: I suspect that we can facilitate one of the future internal design discussions to include the point about logging, the process we should agree to use to determine levels etc., but also it would be good to discuss when we should consider to shutdown the server (call the shutdown on the health facility). Sound like a good pair of (somewhat) connected items to discuss in a sitting.
created_at: 2025-04-28T08:05:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +39,22 @@ public class BlocksFilesHistoricPlugin implements BlockProviderPlugin, BlockNoti
     private final ConcurrentLongRangeSet availableBlocks = new ConcurrentLongRangeSet();
     /** List of all zip ranges that are in progress, so we do not start a duplicate job. */
     private final CopyOnWriteArrayList<LongRange> inProgressZipRanges = new CopyOnWriteArrayList<>();
+    /** Config used internally. Should come from the context, this is temporary until we have some needed logic. */
+    private FilesHistoricConfig config;
+
+    /** Constructor, used for normal plugin loading */
+    public BlocksFilesHistoricPlugin() {
+        this.zipMoveExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    /** Constructor, used only for testing temporarily while we wait for
+     * extended functionality to support overriding config converters
+     * PR #18617 in hiero-consensus-node.
+     */
+    BlocksFilesHistoricPlugin(final FilesHistoricConfig config, final ExecutorService zipMoveExecutorService) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: @jasperpotts in our case, supplying a string value of the path (a jUnit `@TempDir` for these tests) works just fine, because the temp dir is resolved directly to the os (somewhere under `/var` randomly and gets cleaned up after tests). The config dependency will indeed create a new instance of `Path` (`UnixPath` specifically in our case) during initialization, but that is fine, because the string value of the path will be the same and we know that it will point physically to the same place, so cleanup will not be an issue.

The overrides you introduced recently, the ones I started using with latest commit here, only work with string values: api from consensus lib for configs, and we will go through their resolution logic once we build the configs. This is problematic when we want to use `jimfs` because there we need an instance of their implementation of the `Path` interface, which the config dependency will never build (hence why we are waiting for the change you have requested for the ability to override converters for classes supported by default, so we can supply the `jimfs` specific `Path`, to be used in other places)

Since @jsync-swirlds's latest changes conflict with mine, I resolved that and took the opportunity to do a slight improvement and use your overrides. For the thread pool, I will revisit soon, when we have implemented #1067. Let's proceed with what we have for now.
created_at: 2025-04-28T11:12:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: No longer relevant, some overriding changes were introduced and we no longer have this concern for this test. This has been reverted with latest commits.
created_at: 2025-04-28T11:16:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +149,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: @Nana-EC I have created #1069 to capture this and not lose it. Let's explore this concern and act upon it if we deem necessary. Otherwise we can close and keep history.
created_at: 2025-04-28T11:43:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
                     "Failed to move batch of blocks[" + batchFirstBlockNumber + " -> " + batchLastBlockNumber
                             + "] to zip file",
                     e);
+            return;
         }
+        // if we have reached here, then the batch of blocks has been zipped,
+        // now we need to make some updates
+        // update the first and last block numbers
+        availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
+        // log done
+        LOGGER.log(
+                System.Logger.Level.INFO,
+                "Moved batch of blocks[%d -> %d] to zip file",
+                batchFirstBlockNumber,
+                batchLastBlockNumber);
+        // now all the blocks are in the zip file and accessible, send notification
+        context.blockMessaging()
+                .sendBlockPersisted(
+                        new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
+        // remove the batch of blocks from in progress ranges
+        inProgressZipRanges.remove(batchRange);
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: Let's proceed like this for now, as mentioned elsewhere, let's discuss logging + when to shutdown server and strategies to use for throwing/catching etc. in an upcoming internal design meeting.
created_at: 2025-04-28T11:44:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.async;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A very simple executor to be used only for testing! This executor will
+ * collect all submitted tasks to it and will hold them in order. After that
+ * when the {@link #executeSerially()} method is called, all tasks will be
+ * executed in order serially on the same thread that called the method. This
+ * will ensure that all tasks will complete before doing any asserts
+ */
+public class BlockingSerialExecutor extends ThreadPoolExecutor {
+    /** The work queue that will be used to hold the tasks. */
+    private final BlockingQueue<Runnable> workQueue;
+    /** Counter to indicate total submitted tasks. */
+    private int tasksSubmitted;
+
+    /**
+     * Constructor.
+     */
+    public BlockingSerialExecutor(@NonNull final BlockingQueue<Runnable> workQueue) {
+        // supply super with arbitrary values, they will not be used
+        super(
+                1,
+                1,
+                0L,
+                TimeUnit.MILLISECONDS,
+                Objects.requireNonNull(workQueue), // super will not use this queue
+                Executors.defaultThreadFactory(),
+                new AbortPolicy());
+        this.workQueue = workQueue; // actual work queue
+    }
+
+    /**
+     * Overriding this method in order to make sure this logic will be called
+     * every time we will submit a task to the pool.
+     */
+    @Override
+    @SuppressWarnings("all")
+    public void execute(@NonNull final Runnable command) {
+        workQueue.offer(command);
+        tasksSubmitted++;
+    }
+
+    /**
+     * Invoke this method once you have submitted all the tasks that need to be
+     * run by this executor. This method will then proceed to poll each task
+     * (in the same order that they have been submitted) and will execute the
+     * tasks serially on the same thread that called this method. Once this
+     * method returns (or throws), we are certain that all tasks have run and
+     * can safely assert based on that. This method will throw an
+     * {@link IllegalStateException} to indicate a broken state, when the queue
+     * is empty.
+     */
+    public void executeSerially() {
+        if (workQueue.isEmpty()) {
+            throw new IllegalStateException("Queue is empty");
+        } else {
+            while (!workQueue.isEmpty()) {
+                workQueue.poll().run();
+            }
+        }
+    }
+
+    /**
+     * This method indicates if any task was ever submitted to this executor.
+     * This is useful during tests in order to assert that the pool was
+     * essentially not interacted with. An example would be if we have a test
+     * where we want to assert that some production logic will never submit a
+     * task to the executor given some condition, then we can use this method
+     * to assert that. This method does not reflect the current state of the
+     * queue, meaning the queue might be empty due to a call to the
+     * {@link #executeSerially()} method, but this method will still return
+     * true if any task was submitted before that.
+     *
+     * @return boolean value, true if any task was ever submitted, false
+     * otherwise
+     */
+    public boolean wasAnyTaskSubmitted() {
+        return tasksSubmitted > 0;
+    }
+
+    @Override
+    public void close() {
+        shutdownNow();
+    }
+
+    @Override
+    public void shutdown() {
+        shutdownNow();
+    }
+
+    @Override
+    public boolean awaitTermination(final long timeout, final TimeUnit unit) {
+        shutdownNow();
+        return true;
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: ata-nas
body: P.S. I disabled these for now because they hang and the process cannot exit, but your proposal will put these into proper use.
created_at: 2025-04-28T11:54:38Z
author_association: CONTRIBUTOR
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: ata-nas
body: @jsync-swirlds @Nana-EC I have made #1070 to capture @jsync-swirlds improvement proposals.
created_at: 2025-04-28T11:59:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.async;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A very simple executor to be used only for testing! This executor will
+ * collect all submitted tasks to it and will hold them in order. After that
+ * when the {@link #executeSerially()} method is called, all tasks will be
+ * executed in order serially on the same thread that called the method. This
+ * will ensure that all tasks will complete before doing any asserts
+ */
+public class BlockingSerialExecutor extends ThreadPoolExecutor {
+    /** The work queue that will be used to hold the tasks. */
+    private final BlockingQueue<Runnable> workQueue;
+    /** Counter to indicate total submitted tasks. */
+    private int tasksSubmitted;
+
+    /**
+     * Constructor.
+     */
+    public BlockingSerialExecutor(@NonNull final BlockingQueue<Runnable> workQueue) {
+        // supply super with arbitrary values, they will not be used
+        super(
+                1,
+                1,
+                0L,
+                TimeUnit.MILLISECONDS,
+                Objects.requireNonNull(workQueue), // super will not use this queue
+                Executors.defaultThreadFactory(),
+                new AbortPolicy());
+        this.workQueue = workQueue; // actual work queue
+    }
+
+    /**
+     * Overriding this method in order to make sure this logic will be called
+     * every time we will submit a task to the pool.
+     */
+    @Override
+    @SuppressWarnings("all")
+    public void execute(@NonNull final Runnable command) {
+        workQueue.offer(command);
+        tasksSubmitted++;
+    }
+
+    /**
+     * Invoke this method once you have submitted all the tasks that need to be
+     * run by this executor. This method will then proceed to poll each task
+     * (in the same order that they have been submitted) and will execute the
+     * tasks serially on the same thread that called this method. Once this
+     * method returns (or throws), we are certain that all tasks have run and
+     * can safely assert based on that. This method will throw an
+     * {@link IllegalStateException} to indicate a broken state, when the queue
+     * is empty.
+     */
+    public void executeSerially() {
+        if (workQueue.isEmpty()) {
+            throw new IllegalStateException("Queue is empty");
+        } else {
+            while (!workQueue.isEmpty()) {
+                workQueue.poll().run();
+            }
+        }
+    }
+
+    /**
+     * This method indicates if any task was ever submitted to this executor.
+     * This is useful during tests in order to assert that the pool was
+     * essentially not interacted with. An example would be if we have a test
+     * where we want to assert that some production logic will never submit a
+     * task to the executor given some condition, then we can use this method
+     * to assert that. This method does not reflect the current state of the
+     * queue, meaning the queue might be empty due to a call to the
+     * {@link #executeSerially()} method, but this method will still return
+     * true if any task was submitted before that.
+     *
+     * @return boolean value, true if any task was ever submitted, false
+     * otherwise
+     */
+    public boolean wasAnyTaskSubmitted() {
+        return tasksSubmitted > 0;
+    }
+
+    @Override
+    public void close() {
+        shutdownNow();
+    }
+
+    @Override
+    public void shutdown() {
+        shutdownNow();
+    }
+
+    @Override
+    public boolean awaitTermination(final long timeout, final TimeUnit unit) {
+        shutdownNow();
+        return true;
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> T invokeAny(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(@NonNull final Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
+
+    /**
+     * Operation currently not supported!
+     */
+    @Override
+    @NonNull
+    public <T> List<Future<T>> invokeAll(
+            @NonNull final Collection<? extends Callable<T>> tasks, final long timeout, @NonNull final TimeUnit unit) {
+        throw new UnsupportedOperationException("Operation not supported, to be extended as needed");
+    }
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: ata-nas
body: @jsync-swirlds @Nana-EC I have made #1070 to capture @jsync-swirlds improvement proposals.
created_at: 2025-04-28T11:59:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,9 +65,11 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
             availableBlocks.add(blockNumber);
             partialBlock.clear();
             // send block persisted message
-            blockNodeContext
-                    .blockMessaging()
-                    .sendBlockPersisted(new PersistedNotification(blockNumber, blockNumber, 2000));
+            if (sendNotification) {
+                blockNodeContext
+                        .blockMessaging()
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
user: login: ata-nas
body: P.S. is there any action item here? Creating an issue or something else? @jasperpotts @jsync-swirlds @Nana-EC ?
created_at: 2025-04-28T12:00:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
                     "Failed to move batch of blocks[" + batchFirstBlockNumber + " -> " + batchLastBlockNumber
                             + "] to zip file",
                     e);
+            return;
         }
+        // if we have reached here, then the batch of blocks has been zipped,
+        // now we need to make some updates
+        // update the first and last block numbers
+        availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
+        // log done
+        LOGGER.log(
+                System.Logger.Level.INFO,
+                "Moved batch of blocks[%d -> %d] to zip file",
+                batchFirstBlockNumber,
+                batchLastBlockNumber);
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: Same as [here](https://github.com/hiero-ledger/hiero-block-node/pull/1064#discussion_r2063120950)
created_at: 2025-04-28T12:01:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,9 +65,11 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
             availableBlocks.add(blockNumber);
             partialBlock.clear();
             // send block persisted message
-            blockNodeContext
-                    .blockMessaging()
-                    .sendBlockPersisted(new PersistedNotification(blockNumber, blockNumber, 2000));
+            if (sendNotification) {
+                blockNodeContext
+                        .blockMessaging()
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
user: login: Nana-EC
body: Same question.
I think @jasperpotts may have an idea here.
I see `BlocksFilesHistoricPlugin` has a defaultPriority of 100, `BlocksFilesRecentPlugin` and `BlocksCloudHistoricPlugin` have a default of 2000.
I'd say it's prioritizing recent and cloud over historic but we don't seem to have the priority logic documented so i'm not sure of the other options.

I agree not being clear here will have tests with low confidence.
@ata-nas you can open a ticket to clarify priority values of the Plugins so it's clear as right now it seems a little arbitrary.
created_at: 2025-04-28T14:17:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.async;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A very simple executor to be used only for testing! This executor will
+ * collect all submitted tasks to it and will hold them in order. After that
+ * when the {@link #executeSerially()} method is called, all tasks will be
+ * executed in order serially on the same thread that called the method. This
+ * will ensure that all tasks will complete before doing any asserts
+ */
+public class BlockingSerialExecutor extends ThreadPoolExecutor {
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: Nana-EC
body: If only for test can this be moved under a test dir and not included in product code?
created_at: 2025-04-28T14:35:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.plugintest;
+
+import com.hedera.pbj.runtime.grpc.ServiceInterface;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.helidon.webserver.http.HttpService;
+import org.hiero.block.node.spi.ServiceBuilder;
+
+/**
+ * A simple no-op implementation of {@link ServiceBuilder} that does nothing.
+ * To be used for testing purposes only where we need a non-null implementation
+ * that we do not want to act upon.
+ */
+public final class NoOpServiceBuilder implements ServiceBuilder {
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/NoOpServiceBuilder.java
user: login: Nana-EC
body: Similar comment, could this be moved under test dir outside of product code?
created_at: 2025-04-28T14:40:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: Nana-EC
body: I agree with and prefer the summary and actions above. Scrap the empty range unless we can clearly say why it's need.
Range should be valid and inclusive.
created_at: 2025-04-28T14:50:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +145,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
     }
 
     // ==== Private Methods ============================================================================================
+    private void attemptZipping() {
+        // compute the min and max block in next batch to zip
+        long minBlockNumber = availableBlocks().max() + 1;
+        long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
+        // check all those blocks are available
+        final BlockRangeSet historicalAvailable =
+                context.historicalBlockProvider().availableBlocks();
+        // while we can zip blocks, we must keep zipping
+        while (historicalAvailable.max() >= maxBlockNumber) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: Nana-EC
body: Q: could the logic to decide whether to zip be simplified.
I see a few calls and calculations to manage ranges, would these be simplified if `ZipBlockArchive` tracked on its own the max block number it's had archived?
The number would be calculated once on `BlockFilesHistoricPlugin` init and then updated at the end of the zip and move.
Still thinking through the implications but I'm curious
created_at: 2025-04-28T15:10:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,26 +216,29 @@ private void moveBatchOfBlocksToZipFile(final LongRange batchRange) {
                     batchFirstBlockNumber,
                     batchLastBlockNumber);
             zipBlockArchive.writeNewZipFile(batchFirstBlockNumber);
-            // update the first and last block numbers
-            availableBlocks.add(batchFirstBlockNumber, batchLastBlockNumber);
-            // log done
-            LOGGER.log(
-                    System.Logger.Level.INFO,
-                    "Moved batch of blocks[%d -> %d] to zip file",
-                    batchFirstBlockNumber,
-                    batchLastBlockNumber);
-            // now all the blocks are in the zip file and accessible, send notification
-            context.blockMessaging()
-                    .sendBlockPersisted(
-                            new PersistedNotification(batchFirstBlockNumber, batchLastBlockNumber, defaultPriority()));
-            // remove the batch of blocks from in progress ranges
-            inProgressZipRanges.remove(batchRange);
-        } catch (Exception e) {
+        } catch (final IOException e) {
             LOGGER.log(
                     System.Logger.Level.ERROR,
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: georgi-l95
body: Good remark from Joseph. I agree that we should discuss and document 👍
created_at: 2025-04-28T15:24:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.async;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A very simple executor to be used only for testing! This executor will
+ * collect all submitted tasks to it and will hold them in order. After that
+ * when the {@link #executeSerially()} method is called, all tasks will be
+ * executed in order serially on the same thread that called the method. This
+ * will ensure that all tasks will complete before doing any asserts
+ */
+public class BlockingSerialExecutor extends ThreadPoolExecutor {
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/async/BlockingSerialExecutor.java
user: login: ata-nas
body: @Nana-EC this is test fixtures. Available for test only.
created_at: 2025-04-28T17:25:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0
+package org.hiero.block.node.app.fixtures.plugintest;
+
+import com.hedera.pbj.runtime.grpc.ServiceInterface;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.helidon.webserver.http.HttpService;
+import org.hiero.block.node.spi.ServiceBuilder;
+
+/**
+ * A simple no-op implementation of {@link ServiceBuilder} that does nothing.
+ * To be used for testing purposes only where we need a non-null implementation
+ * that we do not want to act upon.
+ */
+public final class NoOpServiceBuilder implements ServiceBuilder {
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/NoOpServiceBuilder.java
user: login: ata-nas
body: @Nana-EC this is test fixtures. Available for test only.
created_at: 2025-04-28T17:25:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,42 +73,41 @@ public List<Class<? extends Record>> configDataTypes() {
     @Override
     public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
         this.context = context;
-        final FilesHistoricConfig config = context.configuration().getConfigData(FilesHistoricConfig.class);
+        final FilesHistoricConfig localConfig =
+                this.config == null ? context.configuration().getConfigData(FilesHistoricConfig.class) : this.config;
         // create plugin data root directory if it does not exist
         try {
-            Files.createDirectories(config.rootPath());
+            Files.createDirectories(localConfig.rootPath());
         } catch (IOException e) {
             LOGGER.log(Level.ERROR, "Could not create root directory", e);
             context.serverHealth().shutdown(name(), "Could not create root directory");
         }
         // register to listen to block notifications
         context.blockMessaging().registerBlockNotificationHandler(this, false, "Blocks Files Historic");
-        numberOfBlocksPerZipFile = (int) Math.pow(10, config.powersOfTenPerZipFileContents());
-        zipBlockArchive = new ZipBlockArchive(context, config);
+        numberOfBlocksPerZipFile = (int) Math.pow(10, localConfig.powersOfTenPerZipFileContents());
+        zipBlockArchive = new ZipBlockArchive(context, localConfig);
         // get the first and last block numbers from the zipBlockArchive
-        availableBlocks.add(zipBlockArchive.minStoredBlockNumber(), zipBlockArchive.maxStoredBlockNumber());
+        final long firstZippedBlock = zipBlockArchive.minStoredBlockNumber();
+        final long latestZippedBlock = zipBlockArchive.maxStoredBlockNumber();
+        if (latestZippedBlock > firstZippedBlock) {
+            // we never expect to enter here, if we do, we have an issue that
+            // needs to be investigated
+            throw new IllegalStateException(
+                    "Latest zipped block number [%d] cannot be greater than the first zipped block number [%d]"
+                            .formatted(latestZippedBlock, firstZippedBlock));
+        }
+        if (firstZippedBlock >= 0) {
+            // add the blocks to the available blocks only if the range is a valid one (positive)
+            availableBlocks.add(firstZippedBlock, latestZippedBlock);
+        }
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: @Nana-EC @jasperpotts @jsync-swirlds #1072 is made to address (finish) this discussion and so we do not miss this.
created_at: 2025-04-29T13:31:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,9 +65,11 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
             availableBlocks.add(blockNumber);
             partialBlock.clear();
             // send block persisted message
-            blockNodeContext
-                    .blockMessaging()
-                    .sendBlockPersisted(new PersistedNotification(blockNumber, blockNumber, 2000));
+            if (sendNotification) {
+                blockNodeContext
+                        .blockMessaging()
path: block-node/app/src/testFixtures/java/org/hiero/block/node/app/fixtures/plugintest/SimpleInMemoryHistoricalBlockFacility.java
user: login: ata-nas
body: @Nana-EC #1073 is created to follow up with this.
created_at: 2025-04-29T13:45:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +145,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
     }
 
     // ==== Private Methods ============================================================================================
+    private void attemptZipping() {
+        // compute the min and max block in next batch to zip
+        long minBlockNumber = availableBlocks().max() + 1;
+        long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
+        // check all those blocks are available
+        final BlockRangeSet historicalAvailable =
+                context.historicalBlockProvider().availableBlocks();
+        // while we can zip blocks, we must keep zipping
+        while (historicalAvailable.max() >= maxBlockNumber) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: I would counter here. Deciding when and what to zip IMO should not be a concern of the zip archiver. The archiver should only do it's job and should not be a decision maker. As long as it is called and given parameters to work with, it will do it's job. IMO if it has some internal state that will follow next in line will in fact complicate the whole thing. As the source of truth regarding what blocks are available remains the plugin itself, let's manage starting zips and updating availables via it.
created_at: 2025-04-29T13:53:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +145,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
     }
 
     // ==== Private Methods ============================================================================================
+    private void attemptZipping() {
+        // compute the min and max block in next batch to zip
+        long minBlockNumber = availableBlocks().max() + 1;
+        long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
+        // check all those blocks are available
+        final BlockRangeSet historicalAvailable =
+                context.historicalBlockProvider().availableBlocks();
+        // while we can zip blocks, we must keep zipping
+        while (historicalAvailable.max() >= maxBlockNumber) {
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: P.S. the update of the available is necessary because the ranged set is the way we can determine if a plugin can provide a queried block.
created_at: 2025-04-29T13:54:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,24 +149,34 @@ public BlockRangeSet availableBlocks() {
     @Override
     public void handlePersisted(PersistedNotification notification) {
         if (notification.blockProviderPriority() > defaultPriority()) {
-            // compute the min and max block in next batch to zip
-            long minBlockNumber = availableBlocks().max() + 1;
-            long maxBlockNumber = minBlockNumber + numberOfBlocksPerZipFile - 1;
-            // check all those blocks are available
-            while (context.historicalBlockProvider().availableBlocks().max() >= maxBlockNumber) {
-                if (context.historicalBlockProvider().availableBlocks().contains(minBlockNumber, maxBlockNumber)) {
-                    final LongRange batchRange = new LongRange(minBlockNumber, maxBlockNumber);
-                    // move the batch of blocks to a zip file
-                    startMovingBatchOfBlocksToZipFile(batchRange);
-                }
-                // try the next batch just in case there is more than one that became available
-                minBlockNumber += numberOfBlocksPerZipFile;
-                maxBlockNumber += numberOfBlocksPerZipFile;
-            }
-        }
+            attemptZipping();
+        } // todo this is not enough of an assertion that the blocks will be coming from the right place
+        //     as notifications are async and things can happen, when we get the accessors later, we should
+        //     be able to get accessors only from places that have higher priority than us. We should probably
+        //     have that as a feature in the block accessor api. (meaning we should be able to query the
+        //     historical block facility for blocks that are coming from higher priority plugins)
path: block-node/block-providers/files.historic/src/main/java/org/hiero/block/node/blocks/files/historic/BlocksFilesHistoricPlugin.java
user: login: ata-nas
body: @Nana-EC @jsync-swirlds I have made a slight extension to the description and title of #1069. I have added the following:
- should we also assert that all blocks in the given range are present (we have the correct accessors for)?
  - if so, then what do we do if there are gaps, do we fail the zip or are gaps in the zip OK?
  - currently nothing is asserted
created_at: 2025-05-27T11:07:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: ata-nas
created_at: 2025-04-28T11:23:46Z
body: @jasperpotts @jsync-swirlds thanks for your approvals, had to rebase because of @jsync-swirlds's latest merge to main. But also took the opportunity to do some additional cleanup.

I kindly ask for your re-approvals. I have also captured all the feedback and created some issues so that we do not lose it. Will iterate with some improvements.
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-29T11:08:20Z
body: ## [Codecov](https://app.codecov.io/gh/hiero-ledger/hiero-block-node/pull/1064?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hiero-ledger) Report
Attention: Patch coverage is `83.78378%` with `6 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/hiero-ledger/hiero-block-node/pull/1064?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hiero-ledger) | Patch % | Lines |
|---|---|---|
| [...ocks/files/historic/BlocksFilesHistoricPlugin.java](https://app.codecov.io/gh/hiero-ledger/hiero-block-node/pull/1064?src=pr&el=tree&filepath=block-node%2Fblock-providers%2Ffiles.historic%2Fsrc%2Fmain%2Fjava%2Forg%2Fhiero%2Fblock%2Fnode%2Fblocks%2Ffiles%2Fhistoric%2FBlocksFilesHistoricPlugin.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hiero-ledger#diff-YmxvY2stbm9kZS9ibG9jay1wcm92aWRlcnMvZmlsZXMuaGlzdG9yaWMvc3JjL21haW4vamF2YS9vcmcvaGllcm8vYmxvY2svbm9kZS9ibG9ja3MvZmlsZXMvaGlzdG9yaWMvQmxvY2tzRmlsZXNIaXN0b3JpY1BsdWdpbi5qYXZh) | 83.78% | [3 Missing and 3 partials :warning: ](https://app.codecov.io/gh/hiero-ledger/hiero-block-node/pull/1064?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hiero-ledger) |

```diff
@@             Coverage Diff              @@
##               main    #1064      +/-   ##
============================================
+ Coverage     82.07%   83.58%   +1.50%     
- Complexity      858      874      +16     
============================================
  Files           102      102              
  Lines          3694     3703       +9     
  Branches        380      380              
============================================
+ Hits           3032     3095      +63     
+ Misses          525      467      -58     
- Partials        137      141       +4     
```

| [Files with missing lines](https://app.codecov.io/gh/hiero-ledger/hiero-block-node/pull/1064?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hiero-ledger) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...ocks/files/historic/BlocksFilesHistoricPlugin.java](https://app.codecov.io/gh/hiero-ledger/hiero-block-node/pull/1064?src=pr&el=tree&filepath=block-node%2Fblock-providers%2Ffiles.historic%2Fsrc%2Fmain%2Fjava%2Forg%2Fhiero%2Fblock%2Fnode%2Fblocks%2Ffiles%2Fhistoric%2FBlocksFilesHistoricPlugin.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hiero-ledger#diff-YmxvY2stbm9kZS9ibG9jay1wcm92aWRlcnMvZmlsZXMuaGlzdG9yaWMvc3JjL21haW4vamF2YS9vcmcvaGllcm8vYmxvY2svbm9kZS9ibG9ja3MvZmlsZXMvaGlzdG9yaWMvQmxvY2tzRmlsZXNIaXN0b3JpY1BsdWdpbi5qYXZh) | `80.26% <83.78%> (+80.26%)` | `15.00 <4.00> (+15.00)` | |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/hiero-ledger/hiero-block-node/pull/1064/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hiero-ledger)

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE