Skip to content

Make runWithRetries non-blocking so ingest concurrency isn't capped by the executor thread count #292

Description

@matthewmcneely

What

CompletableFutures.runWithRetries wraps every query/mutation/commit as CompletableFuture.supplyAsync(() -> ctxCallable.call().get(), executor). The .get() blocks the executor thread for the entire gRPC round-trip, even though the underlying stub call is already async — the response arrives via StreamObserverBridge on a gRPC event-loop thread. So each in-flight request holds one executor thread until it completes.

The default executor is ForkJoinPool.commonPool() (parallelism ≈ availableProcessors() - 1). Net effect: concurrent in-flight transactions are hard-capped at roughly the client's core count, regardless of how many transactions the caller launches — extra work just queues in the pool.

Why it matters

For high-throughput ingest against a multi-core alpha this starves the server: a client on an N-core box keeps at most ~N requests in flight, so a large alpha sees only ~N concurrent mutations and its apply cores sit idle. Turning up ingest parallelism on the client has no effect past the pool size. Today's workarounds (pass a large custom Executor, use multiple channels) mitigate it but burn a blocked thread per in-flight request and require every caller to know the trap — and the ergonomic DgraphClient.ClientOptions.build() path (single channel + default commonPool) is the least concurrent of all.

Proposed fix

Rewrite runWithRetries to compose on the future instead of blocking on it: return the CompletableFuture from ctxCallable.call() directly and attach the JWT-expiry retry via .handle/.thenCompose rather than supplyAsync(() -> ...get()). No thread is held for the round-trip; in-flight concurrency is then bounded only by HTTP/2 streams (and whatever the caller chooses), not by the executor's size.

Preserve: the single JWT-refresh-then-retry-once semantics, gRPC Context propagation (Context.current().wrap(...)), and exception translation.

Acceptance

  • No executor thread is blocked for the duration of a request's round-trip.
  • Concurrent in-flight request count is not bounded by the executor's parallelism (verify: launch M ≫ pool-size async mutations, observe ~M in flight).
  • Existing retry / JWT-refresh and exception-translation behavior unchanged; tests green.

Refs

  • src/main/java/io/dgraph/CompletableFutures.javarunWithRetries
  • src/main/java/io/dgraph/DgraphAsyncClient.java — executor defaults to ForkJoinPool.commonPool()
  • src/main/java/io/dgraph/AsyncTransaction.javadoRequest/commit route through runWithRetries

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions