Skip to content

Add optional executor to GrpcPlatformServerDefinition - #152

Open
RishabhB99 wants to merge 1 commit into
hypertrace:mainfrom
RishabhB99:add-grpc-server-executor-hook
Open

Add optional executor to GrpcPlatformServerDefinition#152
RishabhB99 wants to merge 1 commit into
hypertrace:mainfrom
RishabhB99:add-grpc-server-executor-hook

Conversation

@RishabhB99

Copy link
Copy Markdown

Problem

GrpcPlatformServiceContainer.initializeBuilder(...) builds the network gRPC server but never calls ServerBuilder.executor(...). As a result gRPC falls back to its default unbounded cached thread pool to run RPC handlers.

For services whose handlers block (e.g. making downstream I/O calls to other services), this means one platform thread is pinned per concurrent in-flight RPC, with no ceiling. Under load this leads to unbounded thread growth and eventually OutOfMemoryError: unable to create native thread. There is currently no config key, builder field, or overridable hook for a subclass to supply its own server executor.

Change

Expose an optional Executor on GrpcPlatformServerDefinition and wire it into the NettyServerBuilder when present:

if (serverDefinition.getExecutor() != null) {
  builder.executor(serverDefinition.getExecutor());
}
  • Fully backward compatible: when the field is null (the default), no .executor(...) call is made and behavior is identical to today (gRPC's default executor).
  • The framework itself takes no new dependency and stays JDK-agnostic — it merely accepts an Executor. The caller chooses the implementation.

Motivation / usage

Services on JDK 21 can now supply Executors.newVirtualThreadPerTaskExecutor() so that blocking handler work no longer pins platform threads (virtual threads unmount during I/O), decoupling live thread count from request concurrency. Services that prefer explicit back-pressure can instead supply a bounded ThreadPoolExecutor.

Example (subclass overriding getServerDefinitions()):

GrpcPlatformServerDefinition.builder()
    .name(getServiceName())
    .port(getServicePort())
    .serviceFactory(...)
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .build();

Testing

./gradlew :platform-grpc-service-framework:compileJava :platform-grpc-service-framework:spotlessJavaCheck passes. This module has no existing test source set, and initializeBuilder is private; given the change is a backward-compatible optional pass-through to NettyServerBuilder.executor(...), no test harness was scaffolded. Happy to add one if maintainers prefer.

🤖 Generated with Claude Code

The gRPC network server was always built without calling
ServerBuilder.executor(...), so gRPC falls back to its default unbounded
cached thread pool to run RPC handlers. Services with blocking handlers
(e.g. those making downstream I/O calls) therefore pin one platform
thread per concurrent in-flight RPC, with no ceiling, which can exhaust
native threads under load.

Expose an optional Executor on GrpcPlatformServerDefinition and wire it
into the NettyServerBuilder when present. When null (the default),
behavior is unchanged, so this is fully backward compatible. Services on
JDK 21 can now supply Executors.newVirtualThreadPerTaskExecutor() to
decouple handler dispatch from platform-thread count; others can supply a
bounded pool for back-pressure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@RishabhB99
RishabhB99 requested a review from a team as a code owner August 10, 2026 11:22
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.

1 participant