Skip to content

feat(client): add BatchExecutor and batch.threads option - #460

Open
dheerajsngh wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
dheerajsngh:feat/md-batch-executor-slim
Open

dheerajsngh wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
dheerajsngh:feat/md-batch-executor-slim

Conversation

@dheerajsngh

Copy link
Copy Markdown
Contributor

Type of Change

  • feat: A new feature
  • fix: A bug fix
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
  • refactor: A code change that neither fixes a bug nor adds a feature
  • perf: A code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • chore: Changes to the build process or auxiliary tools and libraries such as documentation generation

Description

What?

  • Adds package-private BatchExecutor in com.google.cloud.gcs.analyticscore.client to run independent GCS requests concurrently on a bounded daemon ThreadPoolExecutor (numThreads core/max threads, 10s keep-alive, numThreads * 20 bounded queue with caller-runs backpressure while active, or MoreExecutors.newDirectExecutorService() when numThreads == 0):
    • queue(Callable<T>, FutureCallback<? super T>): dispatches per-request results/errors to an optional callback with onSuccess isolated outside the task.call() try/catch block, rethrows Errors, and periodically prunes completed futures so retained memory stays bounded across large streaming listings.
    • awaitRequestsCompletion(): reusable phase barrier under a documented single-owner threading contract (with callback retry re-queuing supported) that awaits all queued requests before throwing the first IOException (attaching up to 100 suppressed failures), or cancels remaining requests and restores interrupt status when the waiting thread is interrupted.
    • shutdown() and isIdle(): drains queued work and always terminates the pool in finally (1s grace period before shutdownNow(), cancelling any never-started queued tasks so futures always complete).
  • Adds batch.threads (getBatchThreads() / Builder.setBatchThreads(int)) to GcsClientOptions with default 15 and validation bounds 0..1024.
  • Adds unit tests in BatchExecutorTest and GcsClientOptionsTest covering pool and direct execution, backpressure, callback isolation, phase barriers, callback retries, bounded future retention, interruption, forced shutdown, and option parsing.

Why?

  • Upcoming directory metadata operations (such as streaming recursive delete and flat-namespace directory rename) must issue per-object GCS RPCs concurrently across paginated listings with explicit barriers between phases (for example, completing data-object moves before directory placeholders and commit markers) without retaining unbounded Future instances in memory.
  • google-cloud-storage supports single-object delete and moveBlob uniformly across both HTTP and gRPC transports, whereas JSON multipart batching (Storage.batch()) is unsupported on gRPC and executes sub-request chunks sequentially; a bounded client-side executor provides transport-agnostic concurrency with caller-runs backpressure.

Checklist

  • PR title follows Conventional Commits (e.g., feat(core): ...)
  • All files include the Apache License 2.0 header
  • Documentation has been updated to reflect changes

Generated/Assisted by Agent? Yes

Adds a package-private BatchExecutor modeled on the hadoop-connectors
implementation, plus only the changes analytics-core needs: a reusable
awaitRequestsCompletion() phase barrier that drains every request before
throwing, bounded memory by dropping succeeded futures, onSuccess isolated
from onFailure, InterruptedIOException on owner interruption, and a
shutdown() that always stops the pool. Documents a single-owner threading
contract.

Adds GcsClientOptions batch.threads (default 15, range 0..1024; defaults
mirror hadoop-connectors pending benchmarking).
Cancels the never-started requests that shutdownNow() returns so their
futures always complete and a later awaitRequestsCompletion()/shutdown()
cannot block. Documents that a phase must be fully queued before
awaitRequestsCompletion() is called, and adds a test for reporting the
same exception instance from several requests without self-suppression.
@dheerajsngh
dheerajsngh requested a review from a team as a code owner November 3, 2026 05:10
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new BatchExecutor component designed to handle concurrent GCS RPC operations efficiently. By providing a bounded, transport-agnostic executor, it enables future directory metadata operations to perform concurrent requests without risking memory exhaustion or unbounded future retention. The change also includes necessary configuration options to tune this concurrency, ensuring robust performance and resource management.

Highlights

  • BatchExecutor Implementation: Introduced a package-private BatchExecutor to manage concurrent GCS requests using a bounded daemon thread pool, providing transport-agnostic concurrency and caller-runs backpressure.
  • Configuration Updates: Added 'batch.threads' to GcsClientOptions, allowing users to configure the thread pool size with a default of 15 and validation bounds of 0 to 1024.
  • Testing: Added comprehensive unit tests in BatchExecutorTest and GcsClientOptionsTest to verify pool behavior, backpressure, callback isolation, and option parsing.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.25%. Comparing base (f425f88) to head (9f6dfe0).

Files with missing lines Patch % Lines
.../cloud/gcs/analyticscore/client/BatchExecutor.java 91.60% 8 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #460      +/-   ##
==========================================
- Coverage   98.47%   98.25%   -0.22%     
==========================================
  Files          79       80       +1     
  Lines        3922     4064     +142     
  Branches      457      483      +26     
==========================================
+ Hits         3862     3993     +131     
- Misses         32       40       +8     
- Partials       28       31       +3     
Files with missing lines Coverage Δ
...oud/gcs/analyticscore/client/GcsClientOptions.java 97.50% <100.00%> (+0.25%) ⬆️
.../cloud/gcs/analyticscore/client/BatchExecutor.java 91.60% <91.60%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces BatchExecutor to run independent GCS requests on a bounded thread pool, along with corresponding unit tests and configuration options in GcsClientOptions to control the thread count. The review feedback suggests adhering to the repository style guide by avoiding the static import of Guava's newDirectExecutorService. Additionally, it recommends improving error handling in BatchExecutor.execute to ensure fatal JVM Errors are prioritized and not suppressed if a callback's onFailure method throws an exception.

@gcs-analytics-core-robot

gcs-analytics-core-robot commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

📊 Hermetic JMH Benchmark Summary (PR vs Main)

PR: #460 · Commit: 9f6dfe08951a · Kokoro Build: 711fed16-1966-4385-809f-6957299e76a8

Published Assets: 🌐 Interactive HTML Report | 📄 Markdown Report | 📦 Raw JMH JSON | 🔬 Kokoro / Sponge Artifacts

Suite-Level Executive Summary

Suite Cases GeoMean Latency Δ Latency Δ Range GCS I/O Counters (opens/op, bytes/op)
FooterPrefetchBenchmark 4 +0.49% -4.64% to +6.00% 0 Δ (invariants held)
GcsReadChannelOpenBenchmark 2 +8.17% +6.13% to +10.26% 0 Δ (invariants held)
HermeticInputStreamBenchmark 8 -2.50% -12.07% to +4.48% 0 Δ (invariants held)
ReadStrategyBenchmark 12 +1.66% -2.69% to +9.04% 0 Δ (invariants held)
SmallObjectCacheBenchmark 2 +1.05% -3.35% to +5.64% 0 Δ (invariants held)
TelemetryOverheadBenchmark 8 +1.17% -0.78% to +6.47% CPU-only
VectoredRangeCoalescingBenchmark 9 -0.79% -3.19% to +2.54% CPU-only
📋 Full Hermetic Benchmark Breakdown (45 cases)
Benchmark Params Unit Main PR Latency Δ Opens/op Bytes/op Alloc (B/op)
FooterPrefetchBenchmark.readFooterUnknownSize prefetchEnabled=true us/op 40.832 43.283 +6.00% 1 128.0 KiB 395128
FooterPrefetchBenchmark.readFooterUnknownSize prefetchEnabled=false us/op 3.171 3.024 -4.64% 2 64.0 KiB 1872
FooterPrefetchBenchmark.readTail prefetchEnabled=true us/op 16.715 17.008 +1.75% 1 128 KiB 133576
FooterPrefetchBenchmark.readTail prefetchEnabled=false us/op 3.198 3.170 -0.88% 1 64 KiB 2128
GcsReadChannelOpenBenchmark.openAndReadOneByte_withFileInfo - us/op 0.230 0.244 +6.13% 1 1 B 744
GcsReadChannelOpenBenchmark.openAndReadOneByte_withItemId - us/op 0.629 0.694 +10.26% 1 1 B 1536
HermeticInputStreamBenchmark.positionedReads objectSize=1048576 us/op 123.073 120.315 -2.24% 64 1.47 MiB 718657
HermeticInputStreamBenchmark.positionedReads objectSize=16777216 us/op 61.362 59.673 -2.75% 64 508 KiB 326648
HermeticInputStreamBenchmark.readTailWithFooterPrefetch objectSize=1048576 us/op 16.812 16.660 -0.90% 1 128 KiB 133640
HermeticInputStreamBenchmark.readTailWithFooterPrefetch objectSize=16777216 us/op 18.119 18.931 +4.48% 1 128 KiB 133408
HermeticInputStreamBenchmark.readVectored objectSize=1048576 us/op 166.116 153.935 -7.33% 1 1 MiB 1053386
HermeticInputStreamBenchmark.readVectored objectSize=16777216 us/op 119.042 104.678 -12.07% 16 1 MiB 1060208
HermeticInputStreamBenchmark.sequentialRead objectSize=1048576 us/op 51.580 52.569 +1.92% 1 1 MiB 133930
HermeticInputStreamBenchmark.sequentialRead objectSize=16777216 us/op 701.920 701.283 -0.09% 1 16 MiB 149366
ReadStrategyBenchmark.seekAndRead forwardGapBytes=0, strategy=SEQUENTIAL us/op 0.929 0.968 +4.29% 1 8 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=0, strategy=RANDOM us/op 1.001 0.974 -2.69% 1 8 KiB 1472
ReadStrategyBenchmark.seekAndRead forwardGapBytes=0, strategy=ADAPTIVE us/op 0.984 0.958 -2.65% 1 8 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=4096, strategy=SEQUENTIAL us/op 1.144 1.114 -2.61% 1 12 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=4096, strategy=RANDOM us/op 1.124 1.155 +2.77% 1 12 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=4096, strategy=ADAPTIVE us/op 1.161 1.153 -0.66% 1 12 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=65536, strategy=SEQUENTIAL us/op 3.412 3.504 +2.68% 1 72 KiB 1508
ReadStrategyBenchmark.seekAndRead forwardGapBytes=65536, strategy=RANDOM us/op 3.433 3.743 +9.04% 1 72 KiB 1472
ReadStrategyBenchmark.seekAndRead forwardGapBytes=65536, strategy=ADAPTIVE us/op 3.386 3.553 +4.94% 1 72 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=1048576, strategy=SEQUENTIAL us/op 0.962 0.988 +2.76% 2 8 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=1048576, strategy=RANDOM us/op 1.121 1.115 -0.46% 2 8 KiB 1736
ReadStrategyBenchmark.seekAndRead forwardGapBytes=1048576, strategy=ADAPTIVE us/op 1.104 1.140 +3.27% 2 8 KiB 1896
SmallObjectCacheBenchmark.coldRead - us/op 48.335 46.716 -3.35% 1 256 KiB 263680
SmallObjectCacheBenchmark.warmRead - us/op 9.631 10.175 +5.64% 0 0 B 1208
TelemetryOverheadBenchmark.caffeineCacheHit - ns/op 16.509 17.576 +6.47% - - 0
TelemetryOverheadBenchmark.measureOperation listenerCount=0 ns/op 2.986 2.963 -0.78% - - 0
TelemetryOverheadBenchmark.measureOperation listenerCount=1 ns/op 156.565 156.273 -0.19% - - 392
TelemetryOverheadBenchmark.measureOperation listenerCount=8 ns/op 200.003 199.024 -0.49% - - 392
TelemetryOverheadBenchmark.noOpCacheLookup - ns/op 3.241 3.228 -0.41% - - 0
TelemetryOverheadBenchmark.recordMetric listenerCount=0 ns/op 3.201 3.203 +0.05% - - 0
TelemetryOverheadBenchmark.recordMetric listenerCount=1 ns/op 25.448 25.478 +0.12% - - 24
TelemetryOverheadBenchmark.recordMetric listenerCount=8 ns/op 39.490 41.406 +4.85% - - 24
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=0, ranges=16 ns/op 213.592 213.530 -0.03% - - 528
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=0, ranges=256 ns/op 3123.558 3134.566 +0.35% - - 6768
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=0, ranges=4096 ns/op 31159.605 30250.488 -2.92% - - 92176
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1024, ranges=16 ns/op 213.588 214.670 +0.51% - - 528
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1024, ranges=256 ns/op 3047.826 3125.155 +2.54% - - 6768
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1024, ranges=4096 ns/op 32050.158 31029.280 -3.19% - - 105184
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1048576, ranges=16 ns/op 228.480 227.675 -0.35% - - 1176
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1048576, ranges=256 ns/op 3769.243 3728.588 -1.08% - - 18920
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1048576, ranges=4096 ns/op 48657.952 47296.653 -2.80% - - 295640

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants