diff --git a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java
new file mode 100644
index 000000000..3f2b6f259
--- /dev/null
+++ b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
+ *
+ * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this material except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.temporal.client;
+
+import io.temporal.common.Experimental;
+import io.temporal.common.SimplePlugin;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubs.ClientPluginCallback;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import java.util.function.Supplier;
+import javax.annotation.Nonnull;
+
+/**
+ * Plugin interface for customizing Temporal client configuration and lifecycle.
+ *
+ *
Plugins participate in two phases:
+ *
+ *
+ *
Configuration phase: Plugins are called in registration order to modify options
+ *
Connection phase: Plugins are called in reverse order to wrap service client
+ * creation
+ *
+ *
+ * @see io.temporal.worker.WorkerPlugin
+ * @see SimplePlugin
+ */
+@Experimental
+public interface ClientPlugin extends ClientPluginCallback {
+
+ /**
+ * Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended
+ * format: "organization.plugin-name" (e.g., "io.temporal.tracing")
+ *
+ * @return fully qualified plugin name
+ */
+ @Nonnull
+ String getName();
+
+ /**
+ * Allows the plugin to modify service stubs options before the service stubs are created. Called
+ * during configuration phase in forward (registration) order.
+ *
+ * @param builder the options builder to modify
+ */
+ @Override
+ void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder);
+
+ /**
+ * Allows the plugin to modify workflow client options before the client is created. Called during
+ * configuration phase in forward (registration) order.
+ *
+ * @param builder the options builder to modify
+ */
+ void configureClient(@Nonnull WorkflowClientOptions.Builder builder);
+
+ /**
+ * Allows the plugin to wrap service client connection. Called during connection phase in reverse
+ * order (first plugin wraps all others).
+ *
+ *
+ *
+ * @param options the final options being used for connection
+ * @param next supplier that creates the service stubs (calls next plugin or actual connection)
+ * @return the service stubs (possibly wrapped or decorated)
+ */
+ @Override
+ @Nonnull
+ WorkflowServiceStubs connectServiceClient(
+ @Nonnull WorkflowServiceStubsOptions options, @Nonnull Supplier next);
+}
diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java
index 9b9a36897..6f475520f 100644
--- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java
+++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java
@@ -65,7 +65,10 @@ public static WorkflowClient newInstance(
WorkflowClientInternalImpl(
WorkflowServiceStubs workflowServiceStubs, WorkflowClientOptions options) {
- options = WorkflowClientOptions.newBuilder(options).validateAndBuildWithDefaults();
+ // Apply plugin configuration phase (forward order), then validate
+ WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options);
+ applyClientPluginConfiguration(builder, options.getPlugins());
+ options = builder.validateAndBuildWithDefaults();
workflowServiceStubs =
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
this.options = options;
@@ -771,4 +774,18 @@ public NexusStartWorkflowResponse startNexus(
WorkflowInvocationHandler.closeAsyncInvocation();
}
}
+
+ /**
+ * Applies client plugin configuration phase. Plugins are called in forward (registration) order
+ * to modify the client options.
+ */
+ private static void applyClientPluginConfiguration(
+ WorkflowClientOptions.Builder builder, ClientPlugin[] plugins) {
+ if (plugins == null || plugins.length == 0) {
+ return;
+ }
+ for (ClientPlugin plugin : plugins) {
+ plugin.configureClient(builder);
+ }
+ }
}
diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java
index 944f395a4..6e4a7a067 100644
--- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java
+++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java
@@ -1,6 +1,7 @@
package io.temporal.client;
import io.temporal.api.enums.v1.QueryRejectCondition;
+import io.temporal.common.Experimental;
import io.temporal.common.context.ContextPropagator;
import io.temporal.common.converter.DataConverter;
import io.temporal.common.converter.GlobalDataConverter;
@@ -47,6 +48,7 @@ public static final class Builder {
private String binaryChecksum;
private List contextPropagators;
private QueryRejectCondition queryRejectCondition;
+ private ClientPlugin[] plugins;
private Builder() {}
@@ -61,6 +63,7 @@ private Builder(WorkflowClientOptions options) {
binaryChecksum = options.binaryChecksum;
contextPropagators = options.contextPropagators;
queryRejectCondition = options.queryRejectCondition;
+ plugins = options.plugins;
}
public Builder setNamespace(String namespace) {
@@ -132,6 +135,24 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition
return this;
}
+ /**
+ * Sets the plugins to use with this client. Plugins can modify client configuration, intercept
+ * connection, and wrap execution lifecycle.
+ *
+ *
Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically
+ * propagated to workers created from this client.
+ *
+ * @param plugins the client plugins to use
+ * @return this builder for chaining
+ * @see io.temporal.client.ClientPlugin
+ * @see io.temporal.worker.WorkerPlugin
+ */
+ @Experimental
+ public Builder setPlugins(ClientPlugin... plugins) {
+ this.plugins = Objects.requireNonNull(plugins);
+ return this;
+ }
+
public WorkflowClientOptions build() {
return new WorkflowClientOptions(
namespace,
@@ -140,7 +161,8 @@ public WorkflowClientOptions build() {
identity,
binaryChecksum,
contextPropagators,
- queryRejectCondition);
+ queryRejectCondition,
+ plugins);
}
public WorkflowClientOptions validateAndBuildWithDefaults() {
@@ -154,7 +176,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() {
contextPropagators == null ? EMPTY_CONTEXT_PROPAGATORS : contextPropagators,
queryRejectCondition == null
? QueryRejectCondition.QUERY_REJECT_CONDITION_UNSPECIFIED
- : queryRejectCondition);
+ : queryRejectCondition,
+ plugins == null ? EMPTY_PLUGINS : plugins);
}
}
@@ -163,6 +186,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() {
private static final List EMPTY_CONTEXT_PROPAGATORS = Collections.emptyList();
+ private static final ClientPlugin[] EMPTY_PLUGINS = new ClientPlugin[0];
+
private final String namespace;
private final DataConverter dataConverter;
@@ -177,6 +202,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() {
private final QueryRejectCondition queryRejectCondition;
+ private final ClientPlugin[] plugins;
+
private WorkflowClientOptions(
String namespace,
DataConverter dataConverter,
@@ -184,7 +211,8 @@ private WorkflowClientOptions(
String identity,
String binaryChecksum,
List contextPropagators,
- QueryRejectCondition queryRejectCondition) {
+ QueryRejectCondition queryRejectCondition,
+ ClientPlugin[] plugins) {
this.namespace = namespace;
this.dataConverter = dataConverter;
this.interceptors = interceptors;
@@ -192,6 +220,7 @@ private WorkflowClientOptions(
this.binaryChecksum = binaryChecksum;
this.contextPropagators = contextPropagators;
this.queryRejectCondition = queryRejectCondition;
+ this.plugins = plugins;
}
/**
@@ -236,6 +265,19 @@ public QueryRejectCondition getQueryRejectCondition() {
return queryRejectCondition;
}
+ /**
+ * Returns the client plugins configured for this client.
+ *
+ *
Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically
+ * propagated to workers created from this client.
+ *
+ * @return the array of client plugins, never null
+ */
+ @Experimental
+ public ClientPlugin[] getPlugins() {
+ return plugins;
+ }
+
@Override
public String toString() {
return "WorkflowClientOptions{"
@@ -256,6 +298,8 @@ public String toString() {
+ contextPropagators
+ ", queryRejectCondition="
+ queryRejectCondition
+ + ", plugins="
+ + Arrays.toString(plugins)
+ '}';
}
@@ -270,7 +314,8 @@ public boolean equals(Object o) {
&& com.google.common.base.Objects.equal(identity, that.identity)
&& com.google.common.base.Objects.equal(binaryChecksum, that.binaryChecksum)
&& com.google.common.base.Objects.equal(contextPropagators, that.contextPropagators)
- && queryRejectCondition == that.queryRejectCondition;
+ && queryRejectCondition == that.queryRejectCondition
+ && Arrays.equals(plugins, that.plugins);
}
@Override
@@ -282,6 +327,7 @@ public int hashCode() {
identity,
binaryChecksum,
contextPropagators,
- queryRejectCondition);
+ queryRejectCondition,
+ Arrays.hashCode(plugins));
}
}
diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java
new file mode 100644
index 000000000..1be916f01
--- /dev/null
+++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
+ *
+ * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this material except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.temporal.common;
+
+import io.temporal.client.ClientPlugin;
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.common.context.ContextPropagator;
+import io.temporal.common.interceptors.WorkerInterceptor;
+import io.temporal.common.interceptors.WorkflowClientInterceptor;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+import io.temporal.worker.WorkerFactoryOptions;
+import io.temporal.worker.WorkerOptions;
+import io.temporal.worker.WorkerPlugin;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import javax.annotation.Nonnull;
+
+/**
+ * A plugin that implements both {@link ClientPlugin} and {@link WorkerPlugin}. This class can be
+ * used in two ways:
+ *
+ *
+ *
Builder pattern: Use {@link #newBuilder(String)} to declaratively configure a plugin
+ * with callbacks
+ *
Subclassing: Extend this class and override specific methods for custom behavior
+ *
+ *
+ * @param initializer a consumer that receives the task queue name and worker
+ * @return this builder for chaining
+ */
+ public Builder initializeWorker(@Nonnull BiConsumer initializer) {
+ workerInitializers.add(Objects.requireNonNull(initializer));
+ return this;
+ }
+
+ /**
+ * Adds a callback that is invoked when a worker starts. This can be used to start per-worker
+ * resources or record metrics.
+ *
+ *
Note: For registering workflows and activities, use {@link #initializeWorker} instead, as
+ * registrations must happen before the worker starts polling.
+ *
+ *
+ *
+ * @param callback a consumer that receives the task queue name and worker when the worker
+ * starts
+ * @return this builder for chaining
+ */
+ public Builder onWorkerStart(@Nonnull BiConsumer callback) {
+ workerStartCallbacks.add(Objects.requireNonNull(callback));
+ return this;
+ }
+
+ /**
+ * Adds a callback that is invoked when a worker shuts down. This can be used to clean up
+ * per-worker resources initialized in {@link #initializeWorker} or {@link #onWorkerStart}.
+ *
+ *
+ *
+ * @param callback a consumer that receives the task queue name and worker when the worker shuts
+ * down
+ * @return this builder for chaining
+ */
+ public Builder onWorkerShutdown(@Nonnull BiConsumer callback) {
+ workerShutdownCallbacks.add(Objects.requireNonNull(callback));
+ return this;
+ }
+
+ /**
+ * Adds a callback that is invoked when the worker factory starts. This can be used to
+ * initialize factory-level resources or record metrics.
+ *
+ *
+ *
+ * @param callback a consumer that receives the worker factory when it starts
+ * @return this builder for chaining
+ */
+ public Builder onWorkerFactoryStart(@Nonnull Consumer callback) {
+ workerFactoryStartCallbacks.add(Objects.requireNonNull(callback));
+ return this;
+ }
+
+ /**
+ * Adds a callback that is invoked when the worker factory shuts down. This can be used to clean
+ * up factory-level resources.
+ *
+ *
+ *
+ * @param callback a consumer that receives the worker factory when it shuts down
+ * @return this builder for chaining
+ */
+ public Builder onWorkerFactoryShutdown(@Nonnull Consumer callback) {
+ workerFactoryShutdownCallbacks.add(Objects.requireNonNull(callback));
+ return this;
+ }
+
+ /**
+ * Adds worker interceptors. Interceptors are appended to any existing interceptors in the
+ * configuration.
+ *
+ * @param interceptors the interceptors to add
+ * @return this builder for chaining
+ */
+ public Builder addWorkerInterceptors(WorkerInterceptor... interceptors) {
+ workerInterceptors.addAll(Arrays.asList(interceptors));
+ return this;
+ }
+
+ /**
+ * Adds client interceptors. Interceptors are appended to any existing interceptors in the
+ * configuration.
+ *
+ * @param interceptors the interceptors to add
+ * @return this builder for chaining
+ */
+ public Builder addClientInterceptors(WorkflowClientInterceptor... interceptors) {
+ clientInterceptors.addAll(Arrays.asList(interceptors));
+ return this;
+ }
+
+ /**
+ * Adds context propagators. Propagators are appended to any existing propagators in the
+ * configuration.
+ *
+ * @param propagators the propagators to add
+ * @return this builder for chaining
+ */
+ public Builder addContextPropagators(ContextPropagator... propagators) {
+ contextPropagators.addAll(Arrays.asList(propagators));
+ return this;
+ }
+
+ /**
+ * Builds the plugin with the configured settings.
+ *
+ * @return a new plugin instance
+ */
+ public SimplePlugin build() {
+ return new SimplePlugin(this);
+ }
+ }
+}
diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java
index 20540f4a9..c9ae8b82a 100644
--- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java
+++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java
@@ -15,7 +15,10 @@
import io.temporal.internal.worker.WorkflowExecutorCache;
import io.temporal.internal.worker.WorkflowRunLockManager;
import io.temporal.serviceclient.MetricsTag;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@@ -46,6 +49,9 @@ public final class WorkerFactory {
private final @Nonnull WorkflowExecutorCache cache;
+ /** Plugins propagated from the client and applied to this factory. */
+ private final List plugins;
+
private State state = State.Initial;
private final String statusErrorMessage =
@@ -72,6 +78,12 @@ private WorkerFactory(WorkflowClient workflowClient, WorkerFactoryOptions factor
WorkflowClientOptions workflowClientOptions = workflowClient.getOptions();
String namespace = workflowClientOptions.getNamespace();
+ // Extract worker plugins from client (auto-propagation)
+ this.plugins = extractWorkerPlugins(workflowClientOptions.getPlugins());
+
+ // Apply plugin configuration to factory options (forward order)
+ factoryOptions = applyPluginConfiguration(factoryOptions, this.plugins);
+
this.factoryOptions =
WorkerFactoryOptions.newBuilder(factoryOptions).validateAndBuildWithDefaults();
@@ -137,6 +149,9 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) {
state == State.Initial,
String.format(statusErrorMessage, "create new worker", state.name(), State.Initial.name()));
+ // Apply plugin configuration to worker options (forward order)
+ options = applyWorkerPluginConfiguration(taskQueue, options, this.plugins);
+
// Only one worker can exist for a task queue
Worker existingWorker = workers.get(taskQueue);
if (existingWorker == null) {
@@ -153,6 +168,13 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) {
workflowThreadExecutor,
workflowClient.getOptions().getContextPropagators());
workers.put(taskQueue, worker);
+
+ // Go through the plugins to call plugin initializeWorker hooks (e.g. register workflows,
+ // activities, etc.)
+ for (WorkerPlugin plugin : plugins) {
+ plugin.initializeWorker(taskQueue, worker);
+ }
+
return worker;
} else {
log.warn(
@@ -211,8 +233,59 @@ public synchronized void start() {
.setNamespace(workflowClient.getOptions().getNamespace())
.build());
- for (Worker worker : workers.values()) {
- worker.start();
+ // Build plugin execution chain (reverse order for proper nesting)
+ Runnable startChain = this::doStart;
+ for (int i = plugins.size() - 1; i >= 0; i--) {
+ final Runnable next = startChain;
+ final WorkerPlugin workerPlugin = plugins.get(i);
+ startChain =
+ () -> {
+ try {
+ workerPlugin.startWorkerFactory(this, next);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Plugin " + workerPlugin.getName() + " failed during startup", e);
+ }
+ };
+ }
+
+ // Execute the chain
+ startChain.run();
+ }
+
+ /** Internal method that actually starts the workers. Called from the plugin chain. */
+ private void doStart() {
+ // Start each worker with plugin hooks
+ for (Map.Entry entry : workers.entrySet()) {
+ String taskQueue = entry.getKey();
+ Worker worker = entry.getValue();
+
+ // Build plugin chain for this worker (reverse order for proper nesting)
+ Runnable startChain = worker::start;
+ for (int i = plugins.size() - 1; i >= 0; i--) {
+ final Runnable next = startChain;
+ final WorkerPlugin workerPlugin = plugins.get(i);
+ startChain =
+ () -> {
+ try {
+ workerPlugin.startWorker(taskQueue, worker, next);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Plugin "
+ + workerPlugin.getName()
+ + " failed during worker startup for task queue "
+ + taskQueue,
+ e);
+ }
+ };
+ }
+
+ // Execute the chain for this worker
+ startChain.run();
}
state = State.Started;
@@ -286,12 +359,73 @@ public synchronized void shutdownNow() {
private void shutdownInternal(boolean interruptUserTasks) {
state = State.Shutdown;
+
+ // Build plugin shutdown chain (reverse order for proper nesting)
+ Runnable shutdownChain = () -> doShutdown(interruptUserTasks);
+ for (int i = plugins.size() - 1; i >= 0; i--) {
+ final Runnable next = shutdownChain;
+ final WorkerPlugin workerPlugin = plugins.get(i);
+ shutdownChain =
+ () -> {
+ try {
+ workerPlugin.shutdownWorkerFactory(this, next);
+ } catch (Exception e) {
+ log.warn("Plugin {} failed during shutdown", workerPlugin.getName(), e);
+ // Still try to continue shutdown
+ next.run();
+ }
+ };
+ }
+
+ // Execute the chain
+ shutdownChain.run();
+ }
+
+ /** Internal method that actually shuts down workers. Called from the plugin chain. */
+ private void doShutdown(boolean interruptUserTasks) {
((WorkflowClientInternal) workflowClient.getInternal()).deregisterWorkerFactory(this);
ShutdownManager shutdownManager = new ShutdownManager();
- CompletableFuture.allOf(
- workers.values().stream()
- .map(worker -> worker.shutdown(shutdownManager, interruptUserTasks))
- .toArray(CompletableFuture[]::new))
+
+ // Shutdown each worker with plugin hooks
+ List> shutdownFutures = new ArrayList<>();
+ for (Map.Entry entry : workers.entrySet()) {
+ String taskQueue = entry.getKey();
+ Worker worker = entry.getValue();
+
+ // Build plugin chain for this worker's shutdown (reverse order for proper nesting)
+ // We use a holder to capture the future from the terminal action
+ @SuppressWarnings("unchecked")
+ CompletableFuture[] futureHolder = new CompletableFuture[1];
+ Runnable shutdownChain =
+ () -> futureHolder[0] = worker.shutdown(shutdownManager, interruptUserTasks);
+
+ for (int i = plugins.size() - 1; i >= 0; i--) {
+ final Runnable next = shutdownChain;
+ final WorkerPlugin workerPlugin = plugins.get(i);
+ shutdownChain =
+ () -> {
+ try {
+ workerPlugin.shutdownWorker(taskQueue, worker, next);
+ } catch (Exception e) {
+ log.warn(
+ "Plugin {} failed during worker shutdown for task queue {}",
+ workerPlugin.getName(),
+ taskQueue,
+ e);
+ // Still try to continue shutdown
+ next.run();
+ }
+ };
+ }
+
+ // Execute the shutdown chain for this worker
+ shutdownChain.run();
+ if (futureHolder[0] != null) {
+ shutdownFutures.add(futureHolder[0]);
+ }
+ }
+
+ CompletableFuture.allOf(shutdownFutures.toArray(new CompletableFuture[0]))
.thenApply(
r -> {
cache.invalidateAll();
@@ -359,6 +493,65 @@ public String toString() {
return String.format("WorkerFactory{identity=%s}", workflowClient.getOptions().getIdentity());
}
+ /**
+ * Extracts worker plugins from the client plugins array. Only plugins that also implement {@link
+ * WorkerPlugin} are included.
+ */
+ private static List extractWorkerPlugins(
+ io.temporal.client.ClientPlugin[] clientPlugins) {
+ if (clientPlugins == null || clientPlugins.length == 0) {
+ return Collections.emptyList();
+ }
+
+ List workerPlugins = new ArrayList<>();
+ for (io.temporal.client.ClientPlugin plugin : clientPlugins) {
+ if (plugin instanceof WorkerPlugin) {
+ workerPlugins.add((WorkerPlugin) plugin);
+ }
+ }
+ return Collections.unmodifiableList(workerPlugins);
+ }
+
+ /**
+ * Applies plugin configuration to worker factory options. Plugins are called in forward
+ * (registration) order.
+ */
+ private static WorkerFactoryOptions applyPluginConfiguration(
+ WorkerFactoryOptions options, List plugins) {
+ if (plugins == null || plugins.isEmpty()) {
+ return options;
+ }
+
+ WorkerFactoryOptions.Builder builder =
+ options == null
+ ? WorkerFactoryOptions.newBuilder()
+ : WorkerFactoryOptions.newBuilder(options);
+
+ for (WorkerPlugin plugin : plugins) {
+ plugin.configureWorkerFactory(builder);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Applies plugin configuration to worker options. Plugins are called in forward (registration)
+ * order.
+ */
+ private static WorkerOptions applyWorkerPluginConfiguration(
+ String taskQueue, WorkerOptions options, List plugins) {
+ if (plugins == null || plugins.isEmpty()) {
+ return options;
+ }
+
+ WorkerOptions.Builder builder =
+ options == null ? WorkerOptions.newBuilder() : WorkerOptions.newBuilder(options);
+
+ for (WorkerPlugin plugin : plugins) {
+ plugin.configureWorker(taskQueue, builder);
+ }
+ return builder.build();
+ }
+
enum State {
Initial,
Started,
diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java
index c50da81cd..8fda76d20 100644
--- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java
+++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java
@@ -37,6 +37,7 @@ public static class Builder {
private int workflowCacheSize;
private int maxWorkflowThreadCount;
private WorkerInterceptor[] workerInterceptors;
+ private WorkerPlugin[] plugins;
private boolean enableLoggingInReplay;
private boolean usingVirtualWorkflowThreads;
private ExecutorService overrideLocalActivityTaskExecutor;
@@ -52,6 +53,7 @@ private Builder(WorkerFactoryOptions options) {
this.workflowCacheSize = options.workflowCacheSize;
this.maxWorkflowThreadCount = options.maxWorkflowThreadCount;
this.workerInterceptors = options.workerInterceptors;
+ this.plugins = options.plugins;
this.enableLoggingInReplay = options.enableLoggingInReplay;
this.usingVirtualWorkflowThreads = options.usingVirtualWorkflowThreads;
this.overrideLocalActivityTaskExecutor = options.overrideLocalActivityTaskExecutor;
@@ -101,6 +103,24 @@ public Builder setWorkerInterceptors(WorkerInterceptor... workerInterceptors) {
return this;
}
+ /**
+ * Sets the worker plugins to use with workers created by this factory. Plugins can modify worker
+ * configuration and wrap worker lifecycle.
+ *
+ *
Note: Plugins that implement both {@link io.temporal.client.ClientPlugin} and {@link
+ * WorkerPlugin} are automatically propagated from the client. Use this method for worker-only
+ * plugins that don't need client-side configuration.
+ *
+ * @param plugins the worker plugins to use
+ * @return this builder for chaining
+ * @see WorkerPlugin
+ */
+ @Experimental
+ public Builder setPlugins(WorkerPlugin... plugins) {
+ this.plugins = plugins;
+ return this;
+ }
+
public Builder setEnableLoggingInReplay(boolean enableLoggingInReplay) {
this.enableLoggingInReplay = enableLoggingInReplay;
return this;
@@ -141,6 +161,7 @@ public WorkerFactoryOptions build() {
maxWorkflowThreadCount,
workflowHostLocalTaskQueueScheduleToStartTimeout,
workerInterceptors,
+ plugins,
enableLoggingInReplay,
usingVirtualWorkflowThreads,
overrideLocalActivityTaskExecutor,
@@ -153,6 +174,7 @@ public WorkerFactoryOptions validateAndBuildWithDefaults() {
maxWorkflowThreadCount,
workflowHostLocalTaskQueueScheduleToStartTimeout,
workerInterceptors == null ? new WorkerInterceptor[0] : workerInterceptors,
+ plugins == null ? new WorkerPlugin[0] : plugins,
enableLoggingInReplay,
usingVirtualWorkflowThreads,
overrideLocalActivityTaskExecutor,
@@ -164,6 +186,7 @@ public WorkerFactoryOptions validateAndBuildWithDefaults() {
private final int maxWorkflowThreadCount;
private final @Nullable Duration workflowHostLocalTaskQueueScheduleToStartTimeout;
private final WorkerInterceptor[] workerInterceptors;
+ private final WorkerPlugin[] plugins;
private final boolean enableLoggingInReplay;
private final boolean usingVirtualWorkflowThreads;
private final ExecutorService overrideLocalActivityTaskExecutor;
@@ -173,6 +196,7 @@ private WorkerFactoryOptions(
int maxWorkflowThreadCount,
@Nullable Duration workflowHostLocalTaskQueueScheduleToStartTimeout,
WorkerInterceptor[] workerInterceptors,
+ WorkerPlugin[] plugins,
boolean enableLoggingInReplay,
boolean usingVirtualWorkflowThreads,
ExecutorService overrideLocalActivityTaskExecutor,
@@ -195,12 +219,16 @@ private WorkerFactoryOptions(
if (workerInterceptors == null) {
workerInterceptors = new WorkerInterceptor[0];
}
+ if (plugins == null) {
+ plugins = new WorkerPlugin[0];
+ }
}
this.workflowCacheSize = workflowCacheSize;
this.maxWorkflowThreadCount = maxWorkflowThreadCount;
this.workflowHostLocalTaskQueueScheduleToStartTimeout =
workflowHostLocalTaskQueueScheduleToStartTimeout;
this.workerInterceptors = workerInterceptors;
+ this.plugins = plugins;
this.enableLoggingInReplay = enableLoggingInReplay;
this.usingVirtualWorkflowThreads = usingVirtualWorkflowThreads;
this.overrideLocalActivityTaskExecutor = overrideLocalActivityTaskExecutor;
@@ -223,6 +251,16 @@ public WorkerInterceptor[] getWorkerInterceptors() {
return workerInterceptors;
}
+ /**
+ * Returns the worker plugins configured for this factory.
+ *
+ * @return the array of worker plugins, never null
+ */
+ @Experimental
+ public WorkerPlugin[] getPlugins() {
+ return plugins;
+ }
+
public boolean isEnableLoggingInReplay() {
return enableLoggingInReplay;
}
diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java
new file mode 100644
index 000000000..5446645f0
--- /dev/null
+++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
+ *
+ * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Modifications copyright (C) 2017 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this material except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.temporal.worker;
+
+import io.temporal.common.Experimental;
+import io.temporal.common.SimplePlugin;
+import javax.annotation.Nonnull;
+
+/**
+ * Plugin interface for customizing Temporal worker configuration and lifecycle.
+ *
+ *
Plugins that also implement {@link io.temporal.client.ClientPlugin} are automatically
+ * propagated from the client to workers created from that client.
+ *
+ *
+ *
+ * @see io.temporal.client.ClientPlugin
+ * @see SimplePlugin
+ */
+@Experimental
+public interface WorkerPlugin {
+
+ /**
+ * Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended
+ * format: "organization.plugin-name" (e.g., "io.temporal.tracing")
+ *
+ * @return fully qualified plugin name
+ */
+ @Nonnull
+ String getName();
+
+ /**
+ * Allows the plugin to modify worker factory options before the factory is created. Called during
+ * configuration phase in forward (registration) order.
+ *
+ * @param builder the options builder to modify
+ */
+ void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder);
+
+ /**
+ * Allows the plugin to modify worker options before a worker is created. Called during
+ * configuration phase in forward (registration) order.
+ *
+ * @param taskQueue the task queue name for the worker being created
+ * @param builder the options builder to modify
+ */
+ void configureWorker(@Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder);
+
+ /**
+ * Called after a worker is created, allowing plugins to register workflows, activities, Nexus
+ * services, and other components on the worker.
+ *
+ *
This method is called in forward (registration) order immediately after the worker is
+ * created in {@link WorkerFactory#newWorker}. This is the appropriate place for registrations
+ * because it is called before the worker starts polling.
+ *
+ *
+ *
+ * @param taskQueue the task queue name for the worker
+ * @param worker the newly created worker
+ */
+ void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker);
+
+ /**
+ * Allows the plugin to wrap individual worker startup. Called during execution phase in reverse
+ * order (first plugin wraps all others) when {@link WorkerFactory#start()} is invoked.
+ *
+ *
This method is called for each worker when the factory starts. Use this for per-worker
+ * resource initialization, logging, or metrics. Note that workflow/activity registration should
+ * be done in {@link #initializeWorker} instead, as this method is called after registrations are
+ * finalized.
+ *
+ *
+ *
+ * @param taskQueue the task queue name for the worker
+ * @param worker the worker being started
+ * @param next runnable that starts the next in chain (eventually starts the actual worker)
+ * @throws Exception if startup fails
+ */
+ void startWorker(@Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next)
+ throws Exception;
+
+ /**
+ * Allows the plugin to wrap individual worker shutdown. Called during shutdown phase in reverse
+ * order (first plugin wraps all others) when {@link WorkerFactory#shutdown()} or {@link
+ * WorkerFactory#shutdownNow()} is invoked.
+ *
+ *
This method is called for each worker when the factory shuts down. Use this for per-worker
+ * resource cleanup that was initialized in {@link #startWorker} or {@link #initializeWorker}.
+ *
+ *
+ *
+ * @param taskQueue the task queue name for the worker
+ * @param worker the worker being shut down
+ * @param next runnable that shuts down the next in chain (eventually shuts down the actual
+ * worker)
+ */
+ void shutdownWorker(@Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next);
+
+ /**
+ * Allows the plugin to wrap worker factory startup. Called during execution phase in reverse
+ * order (first plugin wraps all others).
+ *
+ *
This method is called when {@link WorkerFactory#start()} is invoked. The plugin can perform
+ * setup before starting and cleanup logic.
+ *
+ *
+ *
+ * @param factory the worker factory being started
+ * @param next runnable that starts the next in chain (eventually starts actual workers)
+ * @throws Exception if startup fails
+ */
+ void startWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) throws Exception;
+
+ /**
+ * Allows the plugin to wrap worker factory shutdown. Called during shutdown phase in reverse
+ * order (first plugin wraps all others).
+ *
+ *
This method is called when {@link WorkerFactory#shutdown()} or {@link
+ * WorkerFactory#shutdownNow()} is invoked. The plugin can perform actions before and after the
+ * actual shutdown occurs.
+ *
+ *