Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Threading & Executors

The Right Executor for the Job

Java 21's virtual threads are excellent for I/O-bound work but can hurt performance for CPU-intensive tasks. Brane's BraneExecutors provides properly configured executors for each use case.

import sh.brane.rpc.BraneExecutors;

I/O-Bound Work

Use virtual threads for operations that spend most of their time waiting:

  • RPC calls
  • File I/O
  • Database queries
  • Network requests

Example: Parallel RPC Calls

import sh.brane.rpc.BraneExecutors;
import sh.brane.rpc.BraneProvider;
import java.util.List;
import java.util.concurrent.Future;
import java.util.ArrayList;
 
try (var exec = BraneExecutors.newIoBoundExecutor()) {
    List<Future<?>> futures = new ArrayList<>();
 
    // Fire 10,000 requests in parallel
    for (int i = 0; i < 10_000; i++) {
        futures.add(exec.submit(() ->
            provider.send("eth_blockNumber", List.of())
        ));
    }
 
    // Wait for all to complete
    for (var future : futures) {
        future.get();
    }
}

CPU-Bound Work

Use platform threads for operations that keep the CPU busy:

  • Cryptographic operations (signature verification, hashing)
  • Heavy JSON/RLP parsing
  • Data transformation

Example: Signature Verification

import sh.brane.rpc.BraneExecutors;
import java.util.List;
 
List<byte[]> signatures = getSignaturesToVerify();
 
try (var exec = BraneExecutors.newCpuBoundExecutor()) {
    var futures = signatures.stream()
        .map(sig -> exec.submit(() -> crypto.verify(sig)))
        .toList();
 
    for (var future : futures) {
        boolean valid = future.get();
        // Process result
    }
}

Custom Thread Count

By default, newCpuBoundExecutor() uses Runtime.availableProcessors() threads. Override if needed:

// Reserve 2 cores for other work
int threads = Runtime.getRuntime().availableProcessors() - 2;
try (var exec = BraneExecutors.newCpuBoundExecutor(threads)) {
    // ...
}

Decision Guide

WorkloadExecutorWhy
RPC calls (send, sendAsync)newIoBoundExecutor()Waiting on network I/O
File reading (keystores)newIoBoundExecutor()Waiting on disk I/O
Signature verificationnewCpuBoundExecutor()Pure computation
ABI encoding (large batches)newCpuBoundExecutor()CPU-intensive
Transaction buildingNeither - use calling threadFast operation

Thread Naming

Platform threads created by newCpuBoundExecutor() are named brane-cpu-worker-N for easy identification in thread dumps and profilers:

"brane-cpu-worker-0" #23 daemon prio=5 ...
"brane-cpu-worker-1" #24 daemon prio=5 ...

All threads are daemon threads, so they won't prevent JVM shutdown.

WebSocket Subscription Executor

By default, WebSocket subscription callbacks run on virtual threads. You can customize this:

import sh.brane.rpc.WebSocketProvider;
import sh.brane.rpc.BraneExecutors;
 
var provider = WebSocketProvider.create("wss://...");
 
// Use a bounded pool for callbacks
provider.setSubscriptionExecutor(
    BraneExecutors.newCpuBoundExecutor(2)
);

Best Practices

  1. Always use try-with-resources - Executors implement AutoCloseable and should be properly shut down.

  2. Don't mix I/O and CPU work - Use separate executors for different workload types.

  3. Virtual threads are the default - Unless you know you have CPU-bound work, virtual threads are the right choice.

  4. Thread pools for CPU-bound work - Having more threads than cores just adds scheduling overhead for computational tasks.