private void read(GetOptions getOptions, ObjectReader reader, DataBlock dataBlock, EventLoop eventLoop) {
            ThrottleStrategy throttleStrategy = getOptions.readahead ? ThrottleStrategy.CATCH_UP : ThrottleStrategy.BYPASS;
            reader.retain();
            boolean acquired = sizeLimiter.acquire(dataBlock.dataBlockIndex().size(), () -> {
                reader.read(new ObjectReader.ReadOptions().throttleStrategy(throttleStrategy), dataBlock.dataBlockIndex()).whenCompleteAsync((rst, ex) -> {
                    StorageOperationStats.getInstance().blockCacheReadS3Throughput.add(MetricsLevel.INFO, dataBlock.dataBlockIndex().size());
                    reader.release();
                    DataBlockGroupKey key = new DataBlockGroupKey(dataBlock.objectId(), dataBlock.dataBlockIndex());
                    if (ex != null) {
                        dataBlock.completeExceptionally(ex);
                        blocks.remove(key, dataBlock);
                    } else {
                        lru.put(key, dataBlock);
                        dataBlock.complete(rst);
                    }
                    if (sizeLimiter.requiredRelease()) {
                        // In the described scenario, with maxSize set to 1, upon the sequential arrival of requests #getBlock(size=2) and #getBlock(3),
                        // #getBlock(3) will wait in the queue until permits are available.
                        // If, after #getBlock(size=2) completes, permits are still lacking in the sizeLimiter, implying queued tasks,
                        // invoke #evict is necessary to release permits for scheduling #getBlock(3).
                        DataBlockCache.this.evict();
                    }
                }, eventLoop);
                // after the data block is freed, the sizeLimiter will be released and try re-schedule the waiting tasks
                return dataBlock.freeFuture();
            }, eventLoop);
            if (!acquired) {
                DataBlockCache.this.evict();
            }
        }
Get the data block from cache.
<p>
Note: the data block should invoke the {@link DataBlock#release()} after using.

@param options        the get options
@param objectReader   the object reader could be used read the data block when cache misses
@param dataBlockIndex the required data block's index
@return the future of {@link DataBlock}, the future will be completed in the same stream's eventLoop.
s3stream/src/main/java/com/automq/stream/s3/cache/blockcache/DataBlockCache.java