Interface ExecutorProvider
SPI for supplying the
The return type is the minimal
Executor that LangChain4j uses to offload blocking work and run its asynchronous
continuations — concurrent tool execution, blocking RAG / retrieval and embedding-store calls, moderation,
in-process embedding models, retry backoff scheduling, and similar.
Why implement this
LangChain4j hops threads at these offload points and does not propagate thread-local context (tracing spans, SLF4J MDC, security, CDI / request scope, …) across them by default. Registering anExecutorProvider that returns a context-propagating executor makes that context follow the
work. Typical wrappers:
- Quarkus / MicroProfile — a
ManagedExecutor - Spring — an
Executorwrapped with aTaskDecorator, or aContextExecutorService - OpenTelemetry —
Context.taskWrapping(executor) - Micrometer —
ContextSnapshot.wrap(executor)
Executor, not ExecutorService
The return type is the minimal Executor: LangChain4j only needs to run tasks and never owns
the executor's lifecycle (it will not call shutdown()). This accepts the widest range of host
executors — every managed / context-propagating pool (Quarkus ManagedExecutor, Jakarta
ManagedExecutorService, Guava ListeningExecutorService, a Spring TaskExecutor, …) is
an Executor. Every offload point is driven through this single Executor; components that need a
Future handle (to cancel or time-bound a task) obtain it via
CompletableFuture.supplyAsync(..., executor) rather than requiring an ExecutorService.
Discovery and precedence
A provider is registered either by implementing this interface and declaring it forServiceLoader (the standard way; frameworks typically do this for you), or programmatically via
set(ExecutorProvider) (a convenience for tests and non-DI applications). The effective executor is
resolved as:
- a provider set programmatically via
set(ExecutorProvider); - the first
ExecutorProviderfound on the classpath viaServiceLoader; - a built-in default: a virtual-thread-per-task executor on Java 21 or later, falling back to an unbounded platform-thread pool on Java 17-20.
executor(...) builder option, …) take precedence over this global default when explicitly set.
Contract
executor() is called at each offload, so it must return a shared, long-lived executor rather
than create a new one per call.
This SPI intentionally exposes a single, global executor. Should per-purpose executors (e.g. separate pools
for CPU-bound vs. blocking work) ever be needed, they can be added here as default methods that fall
back to executor(), without breaking existing implementations.
- Since:
- 1.20.0
-
Method Summary
Modifier and TypeMethodDescriptionexecutor()static ExecutorProviderget()static voidset(ExecutorProvider provider) Registers a process-wideExecutorProviderprogrammatically, taking precedence over anyServiceLoader-discovered provider and the built-in default.
-
Method Details
-
executor
-
set
Registers a process-wideExecutorProviderprogrammatically, taking precedence over anyServiceLoader-discovered provider and the built-in default. This is a convenience for tests and non-DI applications; framework integrations typically register viaServiceLoaderinstead.Example — make OpenTelemetry spans and SLF4J MDC follow every LangChain4j offload:
ExecutorService base = Executors.newVirtualThreadPerTaskExecutor(); Executor contextAware = Context.taskWrapping(base); // OpenTelemetry ExecutorProvider.set(() -> contextAware);- Parameters:
provider- the provider to register, ornullto clear a previously-set one (falling back to theServiceLoaderprovider, then the built-in default).- Since:
- 1.20.0
-
get
- Returns:
- the
ExecutorProviderpreviously registered viaset(ExecutorProvider), ornullif none was set (in which case aServiceLoaderprovider or the built-in default is in effect). Does not return theServiceLoader-discovered provider. - Since:
- 1.20.0
-