private CompletableFuture<Void> loadMoreBlocksWithoutData0(long endOffset) {
        if (inflightLoadIndexCf != null) {
            return inflightLoadIndexCf.thenCompose(rst -> loadMoreBlocksWithoutData0(endOffset));
        }
        if (endOffset != -1L && endOffset <= loadedBlockIndexEndOffset) {
            return CompletableFuture.completedFuture(null);
        }
        long currentBlocksEpoch = blocksEpoch;
        inflightLoadIndexCf = new CompletableFuture<>();
        long nextLoadingOffset = calWindowBlocksEndOffset();
        AtomicLong nextFindStartOffset = new AtomicLong(nextLoadingOffset);
        TimerUtil time = new TimerUtil();
        // 1. get objects
        CompletableFuture<List<S3ObjectMetadata>> getObjectsCf = objectManager.getObjects(streamId, nextLoadingOffset, endOffset, GET_OBJECT_STEP);
        // 2. get block indexes from objects
        CompletableFuture<Void> findBlockIndexesCf = getObjectsCf.whenComplete((rst, ex) -> {
            StorageOperationStats.getInstance().getIndicesTimeGetObjectStats.record(time.elapsedAndResetAs(TimeUnit.NANOSECONDS));
        }).thenComposeAsync(objects -> {
            CompletableFuture<Void> prevCf = CompletableFuture.completedFuture(null);
            for (S3ObjectMetadata objectMetadata : objects) {
                // the object reader will be release in the whenComplete
                @SuppressWarnings("resource") ObjectReader objectReader = objectReaderFactory.get(objectMetadata);
                // invoke basicObjectInfo to warm up the objectReader
                objectReader.basicObjectInfo();
                prevCf = prevCf.thenCompose(nil -> {
                    if (currentBlocksEpoch != blocksEpoch) {
                        // The blocks are reset, we need to stop the load
                        return CompletableFuture.completedFuture(null);
                    }
                    return objectReader.find(streamId, nextFindStartOffset.get(), -1L, Integer.MAX_VALUE).thenAcceptAsync(findRst -> {
                        if (currentBlocksEpoch != blocksEpoch) {
                            // The blocks are reset, we need to stop the load
                            return;
                        }
                        findRst.streamDataBlocks().forEach(streamDataBlock -> {
                            DataBlockIndex index = streamDataBlock.dataBlockIndex();
                            Block block = new Block(objectMetadata, index);
                            if (!putBlock(block)) {
                                // After object compaction, the blocks get from different objectManager#getObjects maybe not continuous.
                                throw new BlockNotContinuousException();
                            }
                            nextFindStartOffset.set(streamDataBlock.getEndOffset());
                        });
                    }, eventLoop);
                }).whenComplete((nil, ex) -> objectReader.release());
            }
            return prevCf;
        }, eventLoop);
        findBlockIndexesCf.whenCompleteAsync((nil, ex) -> {
            if (ex != null) {
                inflightLoadIndexCf.completeExceptionally(ex);
                return;
            }
            StorageOperationStats.getInstance().getIndicesTimeFindIndexStats.record(time.elapsedAs(TimeUnit.NANOSECONDS));
            CompletableFuture<Void> cf = inflightLoadIndexCf;
            inflightLoadIndexCf = null;
            cf.complete(null);
        }, eventLoop);
        return inflightLoadIndexCf;
    }
Load more block indexes

@return whether load more blocks
s3stream/src/main/java/com/automq/stream/s3/cache/blockcache/StreamReader.java