From 95e3c518b3d9f207dc2e0e4d303b30ea71c1d03d Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Wed, 14 Jan 2026 09:21:16 -0500 Subject: [PATCH 01/28] Initial version --- PR_NOTES.md | 185 +++++++++++ .../client/WorkflowClientInternalImpl.java | 23 ++ .../client/WorkflowClientOptions.java | 82 ++++- .../temporal/common/plugin/ClientPlugin.java | 141 +++++++++ .../io/temporal/common/plugin/PluginBase.java | 91 ++++++ .../common/plugin/PluginDiscovery.java | 165 ++++++++++ .../common/plugin/SimplePluginBuilder.java | 296 ++++++++++++++++++ .../temporal/common/plugin/WorkerPlugin.java | 145 +++++++++ .../io/temporal/worker/WorkerFactory.java | 121 +++++++ .../io/temporal/common/plugin/PluginTest.java | 197 ++++++++++++ .../plugin/SimplePluginBuilderTest.java | 204 ++++++++++++ .../WorkflowClientOptionsPluginTest.java | 143 +++++++++ .../serviceclient/WorkflowServiceStubs.java | 96 ++++++ 13 files changed, 1884 insertions(+), 5 deletions(-) create mode 100644 PR_NOTES.md create mode 100644 temporal-sdk/src/main/java/io/temporal/common/plugin/ClientPlugin.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/plugin/PluginBase.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java create mode 100644 temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/common/plugin/WorkflowClientOptionsPluginTest.java diff --git a/PR_NOTES.md b/PR_NOTES.md new file mode 100644 index 0000000000..f69db10fad --- /dev/null +++ b/PR_NOTES.md @@ -0,0 +1,185 @@ +# Plugin System for Java Temporal SDK + +## Overview + +This PR implements a plugin system for the Java Temporal SDK, modeled after the Python SDK's plugin architecture but adapted to Java idioms and the existing SDK design patterns. + +The plugin system provides a higher-level abstraction over the existing interceptor infrastructure, enabling users to: +- Modify configuration during client/worker creation +- Wrap execution lifecycles with setup/teardown logic +- Auto-propagate plugins from client to worker +- Bundle multiple customizations (interceptors, context propagators, etc.) into reusable units + +## Design Decisions + +### 1. No Base `Plugin` Interface + +**Decision:** `ClientPlugin` and `WorkerPlugin` each define their own `getName()` method independently, rather than sharing a base `Plugin` interface. + +**Rationale:** This matches the Python SDK's design. Python has separate `ClientPlugin` and `WorkerPlugin` with `name()` on each. We initially had a base `Plugin` interface but removed it to simplify. + +### 2. `ClientPluginCallback` Interface (Module Boundary) + +**Decision:** A `ClientPluginCallback` interface exists in `temporal-serviceclient`, which `ClientPlugin` (in `temporal-sdk`) extends. + +**Rationale:** This is required due to Java's module architecture: +- `temporal-serviceclient` contains `WorkflowServiceStubs` +- `temporal-sdk` depends on `temporal-serviceclient` (not vice versa) +- `WorkflowServiceStubs.newServiceStubs(options, plugins)` needs to call plugin methods +- Since serviceclient cannot import from sdk, we define a minimal callback interface in serviceclient + +This is the one structural difference from Python, which uses a single-package architecture where everything can import everything else. + +### 3. `PluginBase` Convenience Class + +**Decision:** Provide an abstract `PluginBase` class that implements both `ClientPlugin` and `WorkerPlugin`. + +**Rationale:** Common Java pattern (like `AbstractList` for `List`). Reduces boilerplate for users writing custom plugins: +```java +// Without PluginBase +public class MyPlugin implements ClientPlugin, WorkerPlugin { + private final String name = "my-plugin"; + @Override public String getName() { return name; } + // ... actual logic +} + +// With PluginBase +public class MyPlugin extends PluginBase { + public MyPlugin() { super("my-plugin"); } + // ... actual logic (getName() inherited) +} +``` + +### 4. `SimplePluginBuilder` with Private `SimplePlugin` + +**Decision:** Provide a builder for creating plugins declaratively, with the implementation class kept private. + +**Rationale:** +- Builder pattern is more natural in Java than Python's constructor with many parameters +- Private `SimplePlugin` is an implementation detail - users interact with the builder +- Allows changing implementation without breaking API + +```java +PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin") + .addWorkerInterceptors(new TracingInterceptor()) + .customizeClient(b -> b.setIdentity("custom")) + .build(); +``` + +### 5. `PluginDiscovery` - Optional ServiceLoader Discovery + +**Decision:** Include a `PluginDiscovery` class that uses Java's `ServiceLoader` for auto-discovery. + +**Status:** This is optional and currently **untested**. May be removed before merging. + +**Rationale for including:** ServiceLoader is a standard Java pattern used by JDBC, logging frameworks, etc. + +**Rationale for removing:** +- Python doesn't have this - just uses explicit `plugins=[]` +- Adds complexity with questionable value +- "Magic" discovery is harder to debug than explicit configuration +- No test coverage + +### 6. Plugin Storage Type + +**Decision:** `WorkflowClientOptions.getPlugins()` returns `List` rather than a typed list. + +**Rationale:** Without a common base interface, we need to store plugins that could be `ClientPlugin`, `WorkerPlugin`, or both. Using `List` (or `List` internally) allows this flexibility. Users cast to the appropriate interface when needed. + +## Files Changed + +### New Files (`temporal-sdk/src/main/java/io/temporal/common/plugin/`) +- `ClientPlugin.java` - Client-side plugin interface +- `WorkerPlugin.java` - Worker-side plugin interface +- `PluginBase.java` - Convenience base class implementing both +- `SimplePluginBuilder.java` - Builder for declarative plugin creation +- `PluginDiscovery.java` - Optional ServiceLoader discovery (may remove) + +### Modified Files +- `WorkflowServiceStubs.java` - Added `newServiceStubs(options, plugins)` and `ClientPluginCallback` interface +- `WorkflowClientOptions.java` - Added `plugins` field with builder methods +- `WorkflowClientInternalImpl.java` - Applies `ClientPlugin.configureClient()` during creation +- `WorkerFactory.java` - Full plugin lifecycle (configuration, execution, shutdown) + +### Test Files (`temporal-sdk/src/test/java/io/temporal/common/plugin/`) +- `PluginTest.java` - Core plugin interface tests +- `SimplePluginBuilderTest.java` - Builder API tests +- `WorkflowClientOptionsPluginTest.java` - Options integration tests + +## Plugin Lifecycle + +### Configuration Phase (forward order) +``` +Plugin A.configureServiceStubs() → Plugin B → Plugin C +Plugin A.configureClient() → Plugin B → Plugin C +Plugin A.configureWorkerFactory() → Plugin B → Plugin C +Plugin A.configureWorker() → Plugin B → Plugin C +``` + +### Execution Phase (reverse order for proper nesting) +``` +Plugin A wraps ( + Plugin B wraps ( + Plugin C wraps ( + actual operation + ) + ) +) +``` + +### Shutdown Phase (forward order) +``` +Plugin A.onWorkerFactoryShutdown() → Plugin B → Plugin C +``` + +## Example Usage + +### Custom Plugin +```java +public class TracingPlugin extends PluginBase { + private final Tracer tracer; + + public TracingPlugin(Tracer tracer) { + super("my-org.tracing"); + this.tracer = tracer; + } + + @Override + public WorkflowClientOptions.Builder configureClient( + WorkflowClientOptions.Builder builder) { + return builder.setInterceptors(new TracingClientInterceptor(tracer)); + } + + @Override + public WorkerFactoryOptions.Builder configureWorkerFactory( + WorkerFactoryOptions.Builder builder) { + return builder.setWorkerInterceptors(new TracingWorkerInterceptor(tracer)); + } +} +``` + +### Using SimplePluginBuilder +```java +PluginBase metricsPlugin = SimplePluginBuilder.newBuilder("my-org.metrics") + .customizeServiceStubs(b -> b.setMetricsScope(myScope)) + .addWorkerInterceptors(new MetricsInterceptor()) + .build(); +``` + +### Client/Worker with Plugins +```java +WorkflowClientOptions clientOptions = WorkflowClientOptions.newBuilder() + .setNamespace("default") + .addPlugin(new TracingPlugin(tracer)) + .addPlugin(metricsPlugin) + .build(); + +// Plugins that implement WorkerPlugin auto-propagate to workers +WorkerFactory factory = WorkerFactory.newInstance(client); +``` + +## Open Questions + +1. **Remove `PluginDiscovery`?** - It's untested and adds complexity. Python's explicit approach works fine. + +2. **Mark as `@Experimental`?** - All public APIs are marked `@Experimental` to allow iteration. 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 9b9a36897c..65eb49822c 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -15,6 +15,7 @@ import io.temporal.common.WorkflowExecutionHistory; import io.temporal.common.interceptors.WorkflowClientCallsInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; +import io.temporal.common.plugin.ClientPlugin; import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.client.*; import io.temporal.internal.client.NexusStartWorkflowResponse; @@ -65,6 +66,8 @@ public static WorkflowClient newInstance( WorkflowClientInternalImpl( WorkflowServiceStubs workflowServiceStubs, WorkflowClientOptions options) { + // Apply plugin configuration phase (forward order) + options = applyClientPluginConfiguration(options); options = WorkflowClientOptions.newBuilder(options).validateAndBuildWithDefaults(); workflowServiceStubs = new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace()); @@ -771,4 +774,24 @@ public NexusStartWorkflowResponse startNexus( WorkflowInvocationHandler.closeAsyncInvocation(); } } + + /** + * Applies client plugin configuration phase. Plugins are called in forward (registration) order + * to modify the client options. + */ + private static WorkflowClientOptions applyClientPluginConfiguration( + WorkflowClientOptions options) { + List plugins = options.getPlugins(); + if (plugins == null || plugins.isEmpty()) { + return options; + } + + WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options); + for (Object plugin : plugins) { + if (plugin instanceof ClientPlugin) { + builder = ((ClientPlugin) plugin).configureClient(builder); + } + } + return builder.build(); + } } 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 944f395a48..157a6c3837 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java @@ -1,11 +1,13 @@ 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; import io.temporal.common.interceptors.WorkflowClientInterceptor; import java.lang.management.ManagementFactory; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -47,6 +49,7 @@ public static final class Builder { private String binaryChecksum; private List contextPropagators; private QueryRejectCondition queryRejectCondition; + private List plugins; private Builder() {} @@ -61,6 +64,7 @@ private Builder(WorkflowClientOptions options) { binaryChecksum = options.binaryChecksum; contextPropagators = options.contextPropagators; queryRejectCondition = options.queryRejectCondition; + plugins = options.plugins != null ? new ArrayList<>(options.plugins) : null; } public Builder setNamespace(String namespace) { @@ -132,6 +136,48 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition return this; } + /** + * Sets the plugins to use with this client. Plugins can modify client and worker configuration, + * intercept connection, and wrap execution lifecycle. + * + *

Each plugin should implement {@link io.temporal.common.plugin.ClientPlugin} and/or {@link + * io.temporal.common.plugin.WorkerPlugin}. Plugins that implement both interfaces are + * automatically propagated to workers created from this client. + * + * @param plugins the list of plugins to use (each should implement ClientPlugin and/or + * WorkerPlugin) + * @return this builder for chaining + * @see io.temporal.common.plugin.ClientPlugin + * @see io.temporal.common.plugin.WorkerPlugin + */ + @Experimental + public Builder setPlugins(List plugins) { + this.plugins = plugins != null ? new ArrayList<>(plugins) : null; + return this; + } + + /** + * Adds a plugin to use with this client. Plugins can modify client and worker configuration, + * intercept connection, and wrap execution lifecycle. + * + *

The plugin should implement {@link io.temporal.common.plugin.ClientPlugin} and/or {@link + * io.temporal.common.plugin.WorkerPlugin}. Plugins that implement both interfaces are + * automatically propagated to workers created from this client. + * + * @param plugin the plugin to add (should implement ClientPlugin and/or WorkerPlugin) + * @return this builder for chaining + * @see io.temporal.common.plugin.ClientPlugin + * @see io.temporal.common.plugin.WorkerPlugin + */ + @Experimental + public Builder addPlugin(Object plugin) { + if (this.plugins == null) { + this.plugins = new ArrayList<>(); + } + this.plugins.add(Objects.requireNonNull(plugin, "Plugin cannot be null")); + return this; + } + public WorkflowClientOptions build() { return new WorkflowClientOptions( namespace, @@ -140,7 +186,8 @@ public WorkflowClientOptions build() { identity, binaryChecksum, contextPropagators, - queryRejectCondition); + queryRejectCondition, + plugins); } public WorkflowClientOptions validateAndBuildWithDefaults() { @@ -154,7 +201,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 +211,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private static final List EMPTY_CONTEXT_PROPAGATORS = Collections.emptyList(); + private static final List EMPTY_PLUGINS = Collections.emptyList(); + private final String namespace; private final DataConverter dataConverter; @@ -177,6 +227,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private final QueryRejectCondition queryRejectCondition; + private final List plugins; + private WorkflowClientOptions( String namespace, DataConverter dataConverter, @@ -184,7 +236,8 @@ private WorkflowClientOptions( String identity, String binaryChecksum, List contextPropagators, - QueryRejectCondition queryRejectCondition) { + QueryRejectCondition queryRejectCondition, + List plugins) { this.namespace = namespace; this.dataConverter = dataConverter; this.interceptors = interceptors; @@ -192,6 +245,7 @@ private WorkflowClientOptions( this.binaryChecksum = binaryChecksum; this.contextPropagators = contextPropagators; this.queryRejectCondition = queryRejectCondition; + this.plugins = plugins; } /** @@ -236,6 +290,20 @@ public QueryRejectCondition getQueryRejectCondition() { return queryRejectCondition; } + /** + * Returns the list of plugins configured for this client. + * + *

Each plugin implements {@link io.temporal.common.plugin.ClientPlugin} and/or {@link + * io.temporal.common.plugin.WorkerPlugin}. Plugins that implement both interfaces are + * automatically propagated to workers created from this client. + * + * @return an unmodifiable list of plugins, never null + */ + @Experimental + public List getPlugins() { + return plugins != null ? Collections.unmodifiableList(plugins) : Collections.emptyList(); + } + @Override public String toString() { return "WorkflowClientOptions{" @@ -256,6 +324,8 @@ public String toString() { + contextPropagators + ", queryRejectCondition=" + queryRejectCondition + + ", plugins=" + + plugins + '}'; } @@ -270,7 +340,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 + && com.google.common.base.Objects.equal(plugins, that.plugins); } @Override @@ -282,6 +353,7 @@ public int hashCode() { identity, binaryChecksum, contextPropagators, - queryRejectCondition); + queryRejectCondition, + plugins); } } diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/ClientPlugin.java new file mode 100644 index 0000000000..c4d486c787 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/ClientPlugin.java @@ -0,0 +1,141 @@ +/* + * 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.plugin; + +import io.temporal.client.WorkflowClientOptions; +import io.temporal.common.Experimental; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubs.ClientPluginCallback; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +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 + *
+ * + *

Example implementation: + * + *

{@code
+ * public class LoggingPlugin extends PluginBase {
+ *     public LoggingPlugin() {
+ *         super("my-org.logging");
+ *     }
+ *
+ *     @Override
+ *     public WorkflowClientOptions.Builder configureClient(
+ *             WorkflowClientOptions.Builder builder) {
+ *         // Add custom interceptor
+ *         return builder.setInterceptors(new LoggingInterceptor());
+ *     }
+ *
+ *     @Override
+ *     public WorkflowServiceStubs connectServiceClient(
+ *             WorkflowServiceStubsOptions options,
+ *             ServiceStubsSupplier next) throws Exception {
+ *         logger.info("Connecting to Temporal at {}", options.getTarget());
+ *         WorkflowServiceStubs stubs = next.get();
+ *         logger.info("Connected successfully");
+ *         return stubs;
+ *     }
+ * }
+ * }
+ * + * @see WorkerPlugin + * @see PluginBase + */ +@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 + * @return the modified builder (may return same instance or new builder) + */ + @Override + @Nonnull + default WorkflowServiceStubsOptions.Builder configureServiceStubs( + @Nonnull WorkflowServiceStubsOptions.Builder builder) { + return 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 + * @return the modified builder + */ + @Nonnull + default WorkflowClientOptions.Builder configureClient( + @Nonnull WorkflowClientOptions.Builder builder) { + return builder; + } + + /** + * Allows the plugin to wrap service client connection. Called during connection phase in reverse + * order (first plugin wraps all others). + * + *

Example: + * + *

{@code
+   * @Override
+   * public WorkflowServiceStubs connectServiceClient(
+   *         WorkflowServiceStubsOptions options,
+   *         ClientPluginCallback.ServiceStubsSupplier next) throws Exception {
+   *     logger.info("Connecting to Temporal...");
+   *     WorkflowServiceStubs stubs = next.get();
+   *     logger.info("Connected successfully");
+   *     return stubs;
+   * }
+   * }
+ * + * @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) + * @throws Exception if connection fails + */ + @Override + @Nonnull + default WorkflowServiceStubs connectServiceClient( + @Nonnull WorkflowServiceStubsOptions options, + @Nonnull ClientPluginCallback.ServiceStubsSupplier next) + throws Exception { + return next.get(); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginBase.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginBase.java new file mode 100644 index 0000000000..346dbc0884 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginBase.java @@ -0,0 +1,91 @@ +/* + * 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.plugin; + +import io.temporal.common.Experimental; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Convenience base class for plugins that implement both {@link ClientPlugin} and {@link + * WorkerPlugin}. All methods have default no-op implementations. + * + *

This is the recommended way to create plugins that need to customize both client and worker + * behavior. Plugins that extend this class will automatically be propagated from the client to + * workers. + * + *

Example: + * + *

{@code
+ * public class TracingPlugin extends PluginBase {
+ *     private final Tracer tracer;
+ *
+ *     public TracingPlugin(Tracer tracer) {
+ *         super("io.temporal.tracing");
+ *         this.tracer = tracer;
+ *     }
+ *
+ *     @Override
+ *     public WorkflowClientOptions.Builder configureClient(
+ *             WorkflowClientOptions.Builder builder) {
+ *         // Add tracing interceptor to client
+ *         return builder.setInterceptors(new TracingClientInterceptor(tracer));
+ *     }
+ *
+ *     @Override
+ *     public WorkerFactoryOptions.Builder configureWorkerFactory(
+ *             WorkerFactoryOptions.Builder builder) {
+ *         // Add tracing interceptor to workers
+ *         return builder.setWorkerInterceptors(new TracingWorkerInterceptor(tracer));
+ *     }
+ * }
+ * }
+ * + * @see ClientPlugin + * @see WorkerPlugin + */ +@Experimental +public abstract class PluginBase implements ClientPlugin, WorkerPlugin { + + private final String name; + + /** + * Creates a new plugin with the specified name. + * + * @param name a unique name for this plugin, used for logging and duplicate detection. + * Recommended format: "organization.plugin-name" (e.g., "io.temporal.tracing") + * @throws NullPointerException if name is null + */ + protected PluginBase(@Nonnull String name) { + this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); + } + + @Override + @Nonnull + public String getName() { + return name; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{name='" + name + "'}"; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java new file mode 100644 index 0000000000..568c950e54 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java @@ -0,0 +1,165 @@ +/* + * 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.plugin; + +import io.temporal.common.Experimental; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; +import javax.annotation.Nonnull; + +/** + * Discovers plugins using Java's {@link ServiceLoader} mechanism. + * + *

To register a plugin for automatic discovery, create files at: + * + *

    + *
  • {@code META-INF/services/io.temporal.common.plugin.ClientPlugin} + *
  • {@code META-INF/services/io.temporal.common.plugin.WorkerPlugin} + *
+ * + *

containing the fully qualified class names of plugin implementations, one per line. + * + *

Example file content: + * + *

+ * com.mycompany.temporal.TracingPlugin
+ * com.mycompany.temporal.MetricsPlugin
+ * 
+ * + *

Usage: + * + *

{@code
+ * // Discover all client plugins
+ * List clientPlugins = PluginDiscovery.discoverClientPlugins();
+ *
+ * // Discover all worker plugins
+ * List workerPlugins = PluginDiscovery.discoverWorkerPlugins();
+ *
+ * // Discover all plugins (both types, deduplicated)
+ * List allPlugins = PluginDiscovery.discoverAllPlugins();
+ *
+ * // Use with client options
+ * WorkflowClientOptions options = WorkflowClientOptions.newBuilder()
+ *     .setPlugins(allPlugins)
+ *     .build();
+ * }
+ *
+ * @see ServiceLoader
+ * @see ClientPlugin
+ * @see WorkerPlugin
+ */
+@Experimental
+public final class PluginDiscovery {
+
+  private PluginDiscovery() {}
+
+  /**
+   * Discovers all available {@link ClientPlugin}s using the thread's context class loader.
+   *
+   * @return an unmodifiable list of discovered client plugins
+   */
+  @Nonnull
+  public static List discoverClientPlugins() {
+    return discoverClientPlugins(Thread.currentThread().getContextClassLoader());
+  }
+
+  /**
+   * Discovers all available {@link ClientPlugin}s using the specified class loader.
+   *
+   * @param classLoader the class loader to use for discovery
+   * @return an unmodifiable list of discovered client plugins
+   */
+  @Nonnull
+  public static List discoverClientPlugins(@Nonnull ClassLoader classLoader) {
+    ServiceLoader loader = ServiceLoader.load(ClientPlugin.class, classLoader);
+    List plugins = new ArrayList<>();
+    for (ClientPlugin plugin : loader) {
+      plugins.add(plugin);
+    }
+    return Collections.unmodifiableList(plugins);
+  }
+
+  /**
+   * Discovers all available {@link WorkerPlugin}s using the thread's context class loader.
+   *
+   * @return an unmodifiable list of discovered worker plugins
+   */
+  @Nonnull
+  public static List discoverWorkerPlugins() {
+    return discoverWorkerPlugins(Thread.currentThread().getContextClassLoader());
+  }
+
+  /**
+   * Discovers all available {@link WorkerPlugin}s using the specified class loader.
+   *
+   * @param classLoader the class loader to use for discovery
+   * @return an unmodifiable list of discovered worker plugins
+   */
+  @Nonnull
+  public static List discoverWorkerPlugins(@Nonnull ClassLoader classLoader) {
+    ServiceLoader loader = ServiceLoader.load(WorkerPlugin.class, classLoader);
+    List plugins = new ArrayList<>();
+    for (WorkerPlugin plugin : loader) {
+      plugins.add(plugin);
+    }
+    return Collections.unmodifiableList(plugins);
+  }
+
+  /**
+   * Discovers all available plugins (both {@link ClientPlugin} and {@link WorkerPlugin}) using the
+   * thread's context class loader. Plugins that implement both interfaces are only included once.
+   *
+   * @return an unmodifiable list of discovered plugins
+   */
+  @Nonnull
+  public static List discoverAllPlugins() {
+    return discoverAllPlugins(Thread.currentThread().getContextClassLoader());
+  }
+
+  /**
+   * Discovers all available plugins (both {@link ClientPlugin} and {@link WorkerPlugin}) using the
+   * specified class loader. Plugins that implement both interfaces are only included once.
+   *
+   * @param classLoader the class loader to use for discovery
+   * @return an unmodifiable list of discovered plugins
+   */
+  @Nonnull
+  public static List discoverAllPlugins(@Nonnull ClassLoader classLoader) {
+    List plugins = new ArrayList<>();
+
+    // Add all ClientPlugins
+    for (ClientPlugin plugin : discoverClientPlugins(classLoader)) {
+      plugins.add(plugin);
+    }
+
+    // Add WorkerPlugins that aren't already in the list (avoid duplicates for dual-interface
+    // plugins)
+    for (WorkerPlugin plugin : discoverWorkerPlugins(classLoader)) {
+      if (!plugins.contains(plugin)) {
+        plugins.add(plugin);
+      }
+    }
+
+    return Collections.unmodifiableList(plugins);
+  }
+}
diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java
new file mode 100644
index 0000000000..4ae061ed83
--- /dev/null
+++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java
@@ -0,0 +1,296 @@
+/*
+ * 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.plugin;
+
+import io.temporal.client.WorkflowClientOptions;
+import io.temporal.common.Experimental;
+import io.temporal.common.context.ContextPropagator;
+import io.temporal.common.interceptors.WorkerInterceptor;
+import io.temporal.common.interceptors.WorkflowClientInterceptor;
+import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import io.temporal.worker.WorkerFactoryOptions;
+import io.temporal.worker.WorkerOptions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import javax.annotation.Nonnull;
+
+/**
+ * Builder for creating simple plugins that only need to modify configuration.
+ *
+ * 

This builder provides a declarative way to create plugins for common use cases without + * subclassing {@link PluginBase}. The resulting plugin implements both {@link ClientPlugin} and + * {@link WorkerPlugin}. + * + *

Example: + * + *

{@code
+ * PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin")
+ *     .addWorkerInterceptors(new TracingInterceptor())
+ *     .addClientInterceptors(new LoggingInterceptor())
+ *     .customizeClient(b -> b.setIdentity("custom-identity"))
+ *     .build();
+ *
+ * WorkflowClientOptions options = WorkflowClientOptions.newBuilder()
+ *     .addPlugin(myPlugin)
+ *     .build();
+ * }
+ * + * @see PluginBase + * @see ClientPlugin + * @see WorkerPlugin + */ +@Experimental +public final class SimplePluginBuilder { + + private final String name; + private final List> stubsCustomizers = + new ArrayList<>(); + private final List> clientCustomizers = new ArrayList<>(); + private final List> factoryCustomizers = new ArrayList<>(); + private final List> workerCustomizers = new ArrayList<>(); + private final List workerInterceptors = new ArrayList<>(); + private final List clientInterceptors = new ArrayList<>(); + private final List contextPropagators = new ArrayList<>(); + + private SimplePluginBuilder(@Nonnull String name) { + this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); + } + + /** + * Creates a new builder with the specified plugin name. + * + * @param name a unique name for the plugin, used for logging and duplicate detection. Recommended + * format: "organization.plugin-name" (e.g., "my-org.tracing") + * @return a new builder instance + */ + public static SimplePluginBuilder newBuilder(@Nonnull String name) { + return new SimplePluginBuilder(name); + } + + /** + * Adds a customizer for {@link WorkflowServiceStubsOptions}. Multiple customizers are applied in + * the order they are added. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public SimplePluginBuilder customizeServiceStubs( + @Nonnull Consumer customizer) { + stubsCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds a customizer for {@link WorkflowClientOptions}. Multiple customizers are applied in the + * order they are added. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public SimplePluginBuilder customizeClient( + @Nonnull Consumer customizer) { + clientCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds a customizer for {@link WorkerFactoryOptions}. Multiple customizers are applied in the + * order they are added. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public SimplePluginBuilder customizeWorkerFactory( + @Nonnull Consumer customizer) { + factoryCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds a customizer for {@link WorkerOptions}. Multiple customizers are applied in the order they + * are added. The customizer is applied to all workers created by the factory. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public SimplePluginBuilder customizeWorker(@Nonnull Consumer customizer) { + workerCustomizers.add(Objects.requireNonNull(customizer)); + 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 SimplePluginBuilder 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 SimplePluginBuilder 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 SimplePluginBuilder addContextPropagators(ContextPropagator... propagators) { + contextPropagators.addAll(Arrays.asList(propagators)); + return this; + } + + /** + * Builds the plugin with the configured settings. + * + * @return a new plugin instance that implements both {@link ClientPlugin} and {@link + * WorkerPlugin} + */ + public PluginBase build() { + return new SimplePlugin( + name, + new ArrayList<>(stubsCustomizers), + new ArrayList<>(clientCustomizers), + new ArrayList<>(factoryCustomizers), + new ArrayList<>(workerCustomizers), + new ArrayList<>(workerInterceptors), + new ArrayList<>(clientInterceptors), + new ArrayList<>(contextPropagators)); + } + + /** Internal implementation of the simple plugin. */ + private static final class SimplePlugin extends PluginBase { + private final List> stubsCustomizers; + private final List> clientCustomizers; + private final List> factoryCustomizers; + private final List> workerCustomizers; + private final List workerInterceptors; + private final List clientInterceptors; + private final List contextPropagators; + + SimplePlugin( + String name, + List> stubsCustomizers, + List> clientCustomizers, + List> factoryCustomizers, + List> workerCustomizers, + List workerInterceptors, + List clientInterceptors, + List contextPropagators) { + super(name); + this.stubsCustomizers = stubsCustomizers; + this.clientCustomizers = clientCustomizers; + this.factoryCustomizers = factoryCustomizers; + this.workerCustomizers = workerCustomizers; + this.workerInterceptors = workerInterceptors; + this.clientInterceptors = clientInterceptors; + this.contextPropagators = contextPropagators; + } + + @Override + @Nonnull + public WorkflowServiceStubsOptions.Builder configureServiceStubs( + @Nonnull WorkflowServiceStubsOptions.Builder builder) { + for (Consumer customizer : stubsCustomizers) { + customizer.accept(builder); + } + return builder; + } + + @Override + @Nonnull + public WorkflowClientOptions.Builder configureClient( + @Nonnull WorkflowClientOptions.Builder builder) { + // Apply customizers + for (Consumer customizer : clientCustomizers) { + customizer.accept(builder); + } + + // Add client interceptors + if (!clientInterceptors.isEmpty()) { + WorkflowClientInterceptor[] existing = builder.build().getInterceptors(); + List combined = + new ArrayList<>(existing != null ? Arrays.asList(existing) : new ArrayList<>()); + combined.addAll(clientInterceptors); + builder.setInterceptors(combined.toArray(new WorkflowClientInterceptor[0])); + } + + // Add context propagators + if (!contextPropagators.isEmpty()) { + List existing = builder.build().getContextPropagators(); + List combined = + new ArrayList<>(existing != null ? existing : new ArrayList<>()); + combined.addAll(contextPropagators); + builder.setContextPropagators(combined); + } + + return builder; + } + + @Override + @Nonnull + public WorkerFactoryOptions.Builder configureWorkerFactory( + @Nonnull WorkerFactoryOptions.Builder builder) { + // Apply customizers + for (Consumer customizer : factoryCustomizers) { + customizer.accept(builder); + } + + // Add worker interceptors + if (!workerInterceptors.isEmpty()) { + WorkerInterceptor[] existing = builder.build().getWorkerInterceptors(); + List combined = + new ArrayList<>(existing != null ? Arrays.asList(existing) : new ArrayList<>()); + combined.addAll(workerInterceptors); + builder.setWorkerInterceptors(combined.toArray(new WorkerInterceptor[0])); + } + + return builder; + } + + @Override + @Nonnull + public WorkerOptions.Builder configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { + for (Consumer customizer : workerCustomizers) { + customizer.accept(builder); + } + return builder; + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java new file mode 100644 index 0000000000..ec2a730037 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java @@ -0,0 +1,145 @@ +/* + * 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.plugin; + +import io.temporal.common.Experimental; +import io.temporal.worker.WorkerFactory; +import io.temporal.worker.WorkerFactoryOptions; +import io.temporal.worker.WorkerOptions; +import javax.annotation.Nonnull; + +/** + * Plugin interface for customizing Temporal worker configuration and lifecycle. + * + *

WorkerPlugins that also implement {@link ClientPlugin} are automatically propagated from the + * client to workers created from that client. + * + *

Example implementation: + * + *

{@code
+ * public class MetricsPlugin extends PluginBase {
+ *     private final MetricsRegistry registry;
+ *
+ *     public MetricsPlugin(MetricsRegistry registry) {
+ *         super("my-org.metrics");
+ *         this.registry = registry;
+ *     }
+ *
+ *     @Override
+ *     public WorkerFactoryOptions.Builder configureWorkerFactory(
+ *             WorkerFactoryOptions.Builder builder) {
+ *         return builder.setWorkerInterceptors(new MetricsWorkerInterceptor(registry));
+ *     }
+ *
+ *     @Override
+ *     public void runWorkerFactory(WorkerFactory factory, Runnable next) throws Exception {
+ *         registry.recordWorkerStart();
+ *         try {
+ *             next.run();
+ *         } finally {
+ *             registry.recordWorkerStop();
+ *         }
+ *     }
+ * }
+ * }
+ * + * @see ClientPlugin + * @see PluginBase + */ +@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 + * @return the modified builder + */ + @Nonnull + default WorkerFactoryOptions.Builder configureWorkerFactory( + @Nonnull WorkerFactoryOptions.Builder builder) { + return 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 + * @return the modified builder + */ + @Nonnull + default WorkerOptions.Builder configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { + return builder; + } + + /** + * 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. + * + *

Example: + * + *

{@code
+   * @Override
+   * public void runWorkerFactory(WorkerFactory factory, Runnable next) throws Exception {
+   *     logger.info("Starting workers...");
+   *     next.run();
+   *     logger.info("Workers started");
+   * }
+   * }
+ * + * @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 + */ + default void runWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) + throws Exception { + next.run(); + } + + /** + * Called when the worker factory is shutting down. Plugins are notified in forward (registration) + * order during shutdown. + * + *

This is called during both {@link WorkerFactory#shutdown()} and {@link + * WorkerFactory#shutdownNow()}. + * + * @param factory the worker factory being shut down + */ + default void onWorkerFactoryShutdown(@Nonnull WorkerFactory factory) { + // Default: no-op + } +} 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 20540f4a9a..daf10b1b78 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -8,6 +8,7 @@ import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.common.converter.DataConverter; +import io.temporal.common.plugin.WorkerPlugin; import io.temporal.internal.client.WorkflowClientInternal; import io.temporal.internal.sync.WorkflowThreadExecutor; import io.temporal.internal.task.VirtualThreadDelegate; @@ -15,7 +16,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 +50,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 +79,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 +150,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) { @@ -211,6 +227,34 @@ public synchronized void start() { .setNamespace(workflowClient.getOptions().getNamespace()) .build()); + // Build plugin execution chain (reverse order for proper nesting) + Runnable startChain = this::doStart; + List reversed = new ArrayList<>(plugins); + Collections.reverse(reversed); + for (Object plugin : reversed) { + if (plugin instanceof WorkerPlugin) { + final Runnable next = startChain; + final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; + startChain = + () -> { + try { + workerPlugin.runWorkerFactory(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() { for (Worker worker : workers.values()) { worker.start(); } @@ -286,6 +330,21 @@ public synchronized void shutdownNow() { private void shutdownInternal(boolean interruptUserTasks) { state = State.Shutdown; + + // Notify plugins of shutdown (forward order) + for (Object plugin : plugins) { + if (plugin instanceof WorkerPlugin) { + try { + ((WorkerPlugin) plugin).onWorkerFactoryShutdown(this); + } catch (Exception e) { + log.warn( + "Plugin {} failed during shutdown notification", + ((WorkerPlugin) plugin).getName(), + e); + } + } + } + ((WorkflowClientInternal) workflowClient.getInternal()).deregisterWorkerFactory(this); ShutdownManager shutdownManager = new ShutdownManager(); CompletableFuture.allOf( @@ -359,6 +418,68 @@ public String toString() { return String.format("WorkerFactory{identity=%s}", workflowClient.getOptions().getIdentity()); } + /** + * Extracts worker plugins from the client plugins list. Only plugins that implement {@link + * WorkerPlugin} are included. + */ + private static List extractWorkerPlugins(List clientPlugins) { + if (clientPlugins == null || clientPlugins.isEmpty()) { + return Collections.emptyList(); + } + + List workerPlugins = new ArrayList<>(); + for (Object plugin : clientPlugins) { + if (plugin instanceof WorkerPlugin) { + workerPlugins.add(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 (Object plugin : plugins) { + if (plugin instanceof WorkerPlugin) { + builder = ((WorkerPlugin) 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 (Object plugin : plugins) { + if (plugin instanceof WorkerPlugin) { + builder = ((WorkerPlugin) plugin).configureWorker(taskQueue, builder); + } + } + return builder.build(); + } + enum State { Initial, Started, diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java new file mode 100644 index 0000000000..7860b6c04e --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java @@ -0,0 +1,197 @@ +/* + * 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.plugin; + +import static org.junit.Assert.*; + +import io.temporal.client.WorkflowClientOptions; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.worker.WorkerFactoryOptions; +import io.temporal.worker.WorkerOptions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +public class PluginTest { + + @Test + public void testPluginBaseName() { + PluginBase plugin = new PluginBase("test-plugin") { + // empty implementation + }; + assertEquals("test-plugin", plugin.getName()); + } + + @Test + public void testPluginBaseToString() { + PluginBase plugin = new PluginBase("my-plugin") { + // empty implementation + }; + assertTrue(plugin.toString().contains("my-plugin")); + } + + @Test(expected = NullPointerException.class) + public void testPluginBaseNullName() { + new PluginBase(null) { + // empty implementation + }; + } + + @Test + public void testClientPluginDefaultMethods() throws Exception { + ClientPlugin plugin = + new ClientPlugin() { + @Override + public String getName() { + return "test"; + } + }; + + // Test default configureServiceStubs returns same builder + WorkflowServiceStubsOptions.Builder stubsBuilder = WorkflowServiceStubsOptions.newBuilder(); + assertSame(stubsBuilder, plugin.configureServiceStubs(stubsBuilder)); + + // Test default configureClient returns same builder + WorkflowClientOptions.Builder clientBuilder = WorkflowClientOptions.newBuilder(); + assertSame(clientBuilder, plugin.configureClient(clientBuilder)); + } + + @Test + public void testWorkerPluginDefaultMethods() throws Exception { + WorkerPlugin plugin = + new WorkerPlugin() { + @Override + public String getName() { + return "test"; + } + }; + + // Test default configureWorkerFactory returns same builder + WorkerFactoryOptions.Builder factoryBuilder = WorkerFactoryOptions.newBuilder(); + assertSame(factoryBuilder, plugin.configureWorkerFactory(factoryBuilder)); + + // Test default configureWorker returns same builder + WorkerOptions.Builder workerBuilder = WorkerOptions.newBuilder(); + assertSame(workerBuilder, plugin.configureWorker("test-queue", workerBuilder)); + + // Test runWorkerFactory calls next + final boolean[] called = {false}; + plugin.runWorkerFactory(null, () -> called[0] = true); + assertTrue("runWorkerFactory should call next", called[0]); + } + + @Test + public void testConfigurationPhaseOrder() { + List order = new ArrayList<>(); + + PluginBase pluginA = createTrackingPlugin("A", order); + PluginBase pluginB = createTrackingPlugin("B", order); + PluginBase pluginC = createTrackingPlugin("C", order); + + List plugins = Arrays.asList(pluginA, pluginB, pluginC); + + // Simulate configuration phase (forward order) + WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); + for (Object plugin : plugins) { + if (plugin instanceof ClientPlugin) { + builder = ((ClientPlugin) plugin).configureClient(builder); + } + } + + // Configuration should be in forward order + assertEquals(Arrays.asList("A-config", "B-config", "C-config"), order); + } + + @Test + public void testExecutionPhaseReverseOrder() throws Exception { + List order = new ArrayList<>(); + + PluginBase pluginA = createExecutionTrackingPlugin("A", order); + PluginBase pluginB = createExecutionTrackingPlugin("B", order); + PluginBase pluginC = createExecutionTrackingPlugin("C", order); + + List plugins = Arrays.asList(pluginA, pluginB, pluginC); + + // Build chain in reverse (like WorkerFactory does) + Runnable chain = + () -> { + order.add("terminal"); + }; + + List reversed = new ArrayList<>(plugins); + java.util.Collections.reverse(reversed); + for (Object plugin : reversed) { + if (plugin instanceof WorkerPlugin) { + final Runnable next = chain; + final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; + chain = + () -> { + order.add(workerPlugin.getName() + "-before"); + try { + workerPlugin.runWorkerFactory(null, next); + } catch (Exception e) { + throw new RuntimeException(e); + } + order.add(workerPlugin.getName() + "-after"); + }; + } + } + + // Execute the chain + chain.run(); + + // First plugin should wrap all others + assertEquals( + Arrays.asList( + "A-before", "B-before", "C-before", "terminal", "C-after", "B-after", "A-after"), + order); + } + + @Test + public void testPluginBaseImplementsBothInterfaces() { + PluginBase plugin = new PluginBase("dual-plugin") { + // empty implementation + }; + + assertTrue("PluginBase should implement ClientPlugin", plugin instanceof ClientPlugin); + assertTrue("PluginBase should implement WorkerPlugin", plugin instanceof WorkerPlugin); + } + + private PluginBase createTrackingPlugin(String name, List order) { + return new PluginBase(name) { + @Override + public WorkflowClientOptions.Builder configureClient(WorkflowClientOptions.Builder builder) { + order.add(name + "-config"); + return builder; + } + }; + } + + private PluginBase createExecutionTrackingPlugin(String name, List order) { + return new PluginBase(name) { + @Override + public void runWorkerFactory(io.temporal.worker.WorkerFactory factory, Runnable next) { + next.run(); + } + }; + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java new file mode 100644 index 0000000000..bd49eabf94 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java @@ -0,0 +1,204 @@ +/* + * 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.plugin; + +import static org.junit.Assert.*; + +import io.temporal.client.WorkflowClientOptions; +import io.temporal.common.interceptors.WorkerInterceptor; +import io.temporal.common.interceptors.WorkerInterceptorBase; +import io.temporal.common.interceptors.WorkflowClientInterceptor; +import io.temporal.common.interceptors.WorkflowClientInterceptorBase; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.worker.WorkerFactoryOptions; +import io.temporal.worker.WorkerOptions; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; + +public class SimplePluginBuilderTest { + + @Test + public void testSimplePluginName() { + PluginBase plugin = SimplePluginBuilder.newBuilder("test-plugin").build(); + assertEquals("test-plugin", plugin.getName()); + } + + @Test + public void testSimplePluginImplementsBothInterfaces() { + PluginBase plugin = SimplePluginBuilder.newBuilder("test").build(); + assertTrue("Should implement ClientPlugin", plugin instanceof ClientPlugin); + assertTrue("Should implement WorkerPlugin", plugin instanceof WorkerPlugin); + } + + @Test + public void testCustomizeServiceStubs() { + AtomicBoolean customized = new AtomicBoolean(false); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .customizeServiceStubs( + builder -> { + customized.set(true); + }) + .build(); + + WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(); + ((ClientPlugin) plugin).configureServiceStubs(builder); + + assertTrue("Customizer should have been called", customized.get()); + } + + @Test + public void testCustomizeClient() { + AtomicBoolean customized = new AtomicBoolean(false); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .customizeClient( + builder -> { + customized.set(true); + builder.setIdentity("custom-identity"); + }) + .build(); + + WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); + ((ClientPlugin) plugin).configureClient(builder); + + assertTrue("Customizer should have been called", customized.get()); + assertEquals("custom-identity", builder.build().getIdentity()); + } + + @Test + public void testCustomizeWorkerFactory() { + AtomicBoolean customized = new AtomicBoolean(false); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .customizeWorkerFactory( + builder -> { + customized.set(true); + builder.setWorkflowCacheSize(100); + }) + .build(); + + WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder(); + ((WorkerPlugin) plugin).configureWorkerFactory(builder); + + assertTrue("Customizer should have been called", customized.get()); + assertEquals(100, builder.build().getWorkflowCacheSize()); + } + + @Test + public void testCustomizeWorker() { + AtomicBoolean customized = new AtomicBoolean(false); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .customizeWorker( + builder -> { + customized.set(true); + builder.setMaxConcurrentActivityExecutionSize(50); + }) + .build(); + + WorkerOptions.Builder builder = WorkerOptions.newBuilder(); + ((WorkerPlugin) plugin).configureWorker("test-queue", builder); + + assertTrue("Customizer should have been called", customized.get()); + assertEquals(50, builder.build().getMaxConcurrentActivityExecutionSize()); + } + + @Test + public void testMultipleCustomizers() { + AtomicInteger callCount = new AtomicInteger(0); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .customizeClient(builder -> callCount.incrementAndGet()) + .customizeClient(builder -> callCount.incrementAndGet()) + .customizeClient(builder -> callCount.incrementAndGet()) + .build(); + + WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); + ((ClientPlugin) plugin).configureClient(builder); + + assertEquals("All customizers should be called", 3, callCount.get()); + } + + @Test + public void testAddWorkerInterceptors() { + WorkerInterceptor interceptor = new WorkerInterceptorBase() {}; + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test").addWorkerInterceptors(interceptor).build(); + + WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder(); + ((WorkerPlugin) plugin).configureWorkerFactory(builder); + + WorkerInterceptor[] interceptors = builder.build().getWorkerInterceptors(); + assertEquals(1, interceptors.length); + assertSame(interceptor, interceptors[0]); + } + + @Test + public void testAddClientInterceptors() { + WorkflowClientInterceptor interceptor = new WorkflowClientInterceptorBase() {}; + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test").addClientInterceptors(interceptor).build(); + + WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); + ((ClientPlugin) plugin).configureClient(builder); + + WorkflowClientInterceptor[] interceptors = builder.build().getInterceptors(); + assertEquals(1, interceptors.length); + assertSame(interceptor, interceptors[0]); + } + + @Test + public void testInterceptorsAppendToExisting() { + WorkerInterceptor existingInterceptor = new WorkerInterceptorBase() {}; + WorkerInterceptor newInterceptor = new WorkerInterceptorBase() {}; + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test").addWorkerInterceptors(newInterceptor).build(); + + WorkerFactoryOptions.Builder builder = + WorkerFactoryOptions.newBuilder().setWorkerInterceptors(existingInterceptor); + ((WorkerPlugin) plugin).configureWorkerFactory(builder); + + WorkerInterceptor[] interceptors = builder.build().getWorkerInterceptors(); + assertEquals(2, interceptors.length); + assertSame(existingInterceptor, interceptors[0]); + assertSame(newInterceptor, interceptors[1]); + } + + @Test(expected = NullPointerException.class) + public void testNullName() { + SimplePluginBuilder.newBuilder(null); + } + + @Test(expected = NullPointerException.class) + public void testNullCustomizer() { + SimplePluginBuilder.newBuilder("test").customizeClient(null); + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/plugin/WorkflowClientOptionsPluginTest.java new file mode 100644 index 0000000000..282762fc2b --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/common/plugin/WorkflowClientOptionsPluginTest.java @@ -0,0 +1,143 @@ +/* + * 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.plugin; + +import static org.junit.Assert.*; + +import io.temporal.client.WorkflowClientOptions; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +public class WorkflowClientOptionsPluginTest { + + @Test + public void testDefaultPluginsEmpty() { + WorkflowClientOptions options = WorkflowClientOptions.newBuilder().build(); + assertTrue("Default plugins should be empty", options.getPlugins().isEmpty()); + } + + @Test + public void testSetPlugins() { + PluginBase plugin1 = new TestPlugin("plugin1"); + PluginBase plugin2 = new TestPlugin("plugin2"); + + WorkflowClientOptions options = + WorkflowClientOptions.newBuilder().setPlugins(Arrays.asList(plugin1, plugin2)).build(); + + List plugins = options.getPlugins(); + assertEquals(2, plugins.size()); + assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); + assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); + } + + @Test + public void testAddPlugin() { + PluginBase plugin1 = new TestPlugin("plugin1"); + PluginBase plugin2 = new TestPlugin("plugin2"); + + WorkflowClientOptions options = + WorkflowClientOptions.newBuilder().addPlugin(plugin1).addPlugin(plugin2).build(); + + List plugins = options.getPlugins(); + assertEquals(2, plugins.size()); + assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); + assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); + } + + @Test + @SuppressWarnings("unchecked") + public void testPluginsAreImmutable() { + PluginBase plugin = new TestPlugin("plugin"); + + WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + + List plugins = (List) options.getPlugins(); + try { + plugins.add(new TestPlugin("another")); + fail("Should not be able to modify plugins list"); + } catch (UnsupportedOperationException e) { + // expected + } + } + + @Test + public void testSetPluginsNull() { + WorkflowClientOptions options = WorkflowClientOptions.newBuilder().setPlugins(null).build(); + assertTrue("Null plugins should result in empty list", options.getPlugins().isEmpty()); + } + + @Test(expected = NullPointerException.class) + public void testAddPluginNull() { + WorkflowClientOptions.newBuilder().addPlugin(null); + } + + @Test + public void testToBuilder() { + PluginBase plugin = new TestPlugin("plugin"); + + WorkflowClientOptions original = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + + WorkflowClientOptions copy = original.toBuilder().build(); + + assertEquals(1, copy.getPlugins().size()); + assertEquals("plugin", ((ClientPlugin) copy.getPlugins().get(0)).getName()); + } + + @Test + public void testValidateAndBuildWithDefaults() { + PluginBase plugin = new TestPlugin("plugin"); + + WorkflowClientOptions options = + WorkflowClientOptions.newBuilder().addPlugin(plugin).validateAndBuildWithDefaults(); + + assertEquals(1, options.getPlugins().size()); + assertEquals("plugin", ((ClientPlugin) options.getPlugins().get(0)).getName()); + } + + @Test + public void testEqualsWithPlugins() { + PluginBase plugin = new TestPlugin("plugin"); + + WorkflowClientOptions options1 = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + + WorkflowClientOptions options2 = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + + assertEquals(options1, options2); + assertEquals(options1.hashCode(), options2.hashCode()); + } + + @Test + public void testToStringWithPlugins() { + PluginBase plugin = new TestPlugin("my-plugin"); + + WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + + String str = options.toString(); + assertTrue("toString should contain plugins", str.contains("plugins")); + } + + private static class TestPlugin extends PluginBase { + TestPlugin(String name) { + super(name); + } + } +} diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index 2ce76512a8..acd18473aa 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -6,6 +6,10 @@ import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.testservice.InProcessGRPCServer; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** Initializes and holds gRPC blocking and future stubs. */ @@ -122,5 +126,97 @@ static WorkflowServiceStubs newInstance( new WorkflowServiceStubsImpl(service, options), WorkflowServiceStubs.class); } + /** + * Creates WorkflowService gRPC stubs with plugin support. + * + *

This method applies plugins in two phases: + * + *

    + *
  1. Configuration phase: Each plugin's {@code configureServiceStubs} method is called + * in forward (registration) order to modify the options builder + *
  2. Connection phase: Each plugin's {@code connectServiceClient} method is called in + * reverse order to wrap the connection (first plugin wraps all others) + *
+ * + *

This method creates stubs with "lazy" connectivity. The connection is not performed during + * the creation time and happens on the first request. + * + * @param options stub options to use + * @param plugins list of plugins to apply (plugins implementing ClientPlugin are processed) + * @return the workflow service stubs + */ + static WorkflowServiceStubs newServiceStubs( + @Nonnull WorkflowServiceStubsOptions options, @Nonnull List plugins) { + enforceNonWorkflowThread(); + + // Apply plugin configuration phase (forward order) + WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(options); + for (Object plugin : plugins) { + if (plugin instanceof ClientPluginCallback) { + builder = ((ClientPluginCallback) plugin).configureServiceStubs(builder); + } + } + WorkflowServiceStubsOptions finalOptions = builder.validateAndBuildWithDefaults(); + + // Build connection chain (reverse order for proper nesting) + ClientPluginCallback.ServiceStubsSupplier connectionChain = + () -> + WorkflowThreadMarker.protectFromWorkflowThread( + new WorkflowServiceStubsImpl(null, finalOptions), WorkflowServiceStubs.class); + + List reversed = new ArrayList<>(plugins); + Collections.reverse(reversed); + for (Object plugin : reversed) { + if (plugin instanceof ClientPluginCallback) { + final ClientPluginCallback.ServiceStubsSupplier next = connectionChain; + final ClientPluginCallback callback = (ClientPluginCallback) plugin; + connectionChain = () -> callback.connectServiceClient(finalOptions, next); + } + } + + try { + return connectionChain.get(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Failed to create service stubs with plugins", e); + } + } + + /** + * Callback interface for client plugins to participate in service stubs creation. This interface + * is implemented by {@code ClientPlugin} in the temporal-sdk module. + */ + interface ClientPluginCallback { + /** + * Allows the plugin to modify service stubs options before the service stubs are created. + * + * @param builder the options builder to modify + * @return the modified builder + */ + @Nonnull + WorkflowServiceStubsOptions.Builder configureServiceStubs( + @Nonnull WorkflowServiceStubsOptions.Builder builder); + + /** + * Allows the plugin to wrap service client connection. + * + * @param options the final options being used for connection + * @param next supplier that creates the service stubs + * @return the service stubs + * @throws Exception if connection fails + */ + @Nonnull + WorkflowServiceStubs connectServiceClient( + @Nonnull WorkflowServiceStubsOptions options, @Nonnull ServiceStubsSupplier next) + throws Exception; + + /** Functional interface for the connection chain. */ + @FunctionalInterface + interface ServiceStubsSupplier { + WorkflowServiceStubs get() throws Exception; + } + } + WorkflowServiceStubsOptions getOptions(); } From 569286ac9ff72789d7e476139645b67e7e22072e Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Wed, 14 Jan 2026 09:30:08 -0500 Subject: [PATCH 02/28] remove discovery, update notes --- PR_NOTES.md | 75 ++------ .../common/plugin/PluginDiscovery.java | 165 ------------------ 2 files changed, 14 insertions(+), 226 deletions(-) delete mode 100644 temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java diff --git a/PR_NOTES.md b/PR_NOTES.md index f69db10fad..edadc56995 100644 --- a/PR_NOTES.md +++ b/PR_NOTES.md @@ -16,7 +16,9 @@ The plugin system provides a higher-level abstraction over the existing intercep **Decision:** `ClientPlugin` and `WorkerPlugin` each define their own `getName()` method independently, rather than sharing a base `Plugin` interface. -**Rationale:** This matches the Python SDK's design. Python has separate `ClientPlugin` and `WorkerPlugin` with `name()` on each. We initially had a base `Plugin` interface but removed it to simplify. +**Rationale:** This matches the Python SDK's design. Python has separate `ClientPlugin` and `WorkerPlugin` with `name()` on each. I initially had a base `Plugin` interface but removed it to simplify. + +**Alternative considered:** I could add a shared `Plugin` interface with just `getName()`. This would allow `List` instead of `List` for storage. However, this adds an interface that serves no purpose other than type convenience, and Python doesn't have it. ### 2. `ClientPluginCallback` Interface (Module Boundary) @@ -26,33 +28,20 @@ The plugin system provides a higher-level abstraction over the existing intercep - `temporal-serviceclient` contains `WorkflowServiceStubs` - `temporal-sdk` depends on `temporal-serviceclient` (not vice versa) - `WorkflowServiceStubs.newServiceStubs(options, plugins)` needs to call plugin methods -- Since serviceclient cannot import from sdk, we define a minimal callback interface in serviceclient +- Since serviceclient cannot import from sdk, I define a minimal callback interface in serviceclient This is the one structural difference from Python, which uses a single-package architecture where everything can import everything else. ### 3. `PluginBase` Convenience Class -**Decision:** Provide an abstract `PluginBase` class that implements both `ClientPlugin` and `WorkerPlugin`. +**Decision:** I provide an abstract `PluginBase` class that implements both `ClientPlugin` and `WorkerPlugin`. -**Rationale:** Common Java pattern (like `AbstractList` for `List`). Reduces boilerplate for users writing custom plugins: -```java -// Without PluginBase -public class MyPlugin implements ClientPlugin, WorkerPlugin { - private final String name = "my-plugin"; - @Override public String getName() { return name; } - // ... actual logic -} +**Rationale:** This is a common Java pattern (like `AbstractList` for `List`). -// With PluginBase -public class MyPlugin extends PluginBase { - public MyPlugin() { super("my-plugin"); } - // ... actual logic (getName() inherited) -} -``` ### 4. `SimplePluginBuilder` with Private `SimplePlugin` -**Decision:** Provide a builder for creating plugins declaratively, with the implementation class kept private. +**Decision:** I provide a builder for creating plugins declaratively, with the implementation class kept private. **Rationale:** - Builder pattern is more natural in Java than Python's constructor with many parameters @@ -66,25 +55,18 @@ PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin") .build(); ``` -### 5. `PluginDiscovery` - Optional ServiceLoader Discovery +### 5. No ServiceLoader Discovery -**Decision:** Include a `PluginDiscovery` class that uses Java's `ServiceLoader` for auto-discovery. +**Decision:** I do not include ServiceLoader-based plugin discovery. -**Status:** This is optional and currently **untested**. May be removed before merging. - -**Rationale for including:** ServiceLoader is a standard Java pattern used by JDBC, logging frameworks, etc. - -**Rationale for removing:** +**Rationale:** - Python doesn't have this - just uses explicit `plugins=[]` -- Adds complexity with questionable value +- ServiceLoader requires classes with no-arg constructors, which doesn't integrate with `SimplePluginBuilder` - "Magic" discovery is harder to debug than explicit configuration -- No test coverage +- Explicit plugin configuration is clearer and sufficient -### 6. Plugin Storage Type +We could consider adding it in though if there is interest. -**Decision:** `WorkflowClientOptions.getPlugins()` returns `List` rather than a typed list. - -**Rationale:** Without a common base interface, we need to store plugins that could be `ClientPlugin`, `WorkerPlugin`, or both. Using `List` (or `List` internally) allows this flexibility. Users cast to the appropriate interface when needed. ## Files Changed @@ -93,7 +75,6 @@ PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin") - `WorkerPlugin.java` - Worker-side plugin interface - `PluginBase.java` - Convenience base class implementing both - `SimplePluginBuilder.java` - Builder for declarative plugin creation -- `PluginDiscovery.java` - Optional ServiceLoader discovery (may remove) ### Modified Files - `WorkflowServiceStubs.java` - Added `newServiceStubs(options, plugins)` and `ClientPluginCallback` interface @@ -106,32 +87,6 @@ PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin") - `SimplePluginBuilderTest.java` - Builder API tests - `WorkflowClientOptionsPluginTest.java` - Options integration tests -## Plugin Lifecycle - -### Configuration Phase (forward order) -``` -Plugin A.configureServiceStubs() → Plugin B → Plugin C -Plugin A.configureClient() → Plugin B → Plugin C -Plugin A.configureWorkerFactory() → Plugin B → Plugin C -Plugin A.configureWorker() → Plugin B → Plugin C -``` - -### Execution Phase (reverse order for proper nesting) -``` -Plugin A wraps ( - Plugin B wraps ( - Plugin C wraps ( - actual operation - ) - ) -) -``` - -### Shutdown Phase (forward order) -``` -Plugin A.onWorkerFactoryShutdown() → Plugin B → Plugin C -``` - ## Example Usage ### Custom Plugin @@ -180,6 +135,4 @@ WorkerFactory factory = WorkerFactory.newInstance(client); ## Open Questions -1. **Remove `PluginDiscovery`?** - It's untested and adds complexity. Python's explicit approach works fine. - -2. **Mark as `@Experimental`?** - All public APIs are marked `@Experimental` to allow iteration. +1. **Mark as `@Experimental`?** - I've marked all public APIs as `@Experimental` to allow iteration. Is this appropriate? diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java deleted file mode 100644 index 568c950e54..0000000000 --- a/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginDiscovery.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.plugin; - -import io.temporal.common.Experimental; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ServiceLoader; -import javax.annotation.Nonnull; - -/** - * Discovers plugins using Java's {@link ServiceLoader} mechanism. - * - *

To register a plugin for automatic discovery, create files at: - * - *

    - *
  • {@code META-INF/services/io.temporal.common.plugin.ClientPlugin} - *
  • {@code META-INF/services/io.temporal.common.plugin.WorkerPlugin} - *
- * - *

containing the fully qualified class names of plugin implementations, one per line. - * - *

Example file content: - * - *

- * com.mycompany.temporal.TracingPlugin
- * com.mycompany.temporal.MetricsPlugin
- * 
- * - *

Usage: - * - *

{@code
- * // Discover all client plugins
- * List clientPlugins = PluginDiscovery.discoverClientPlugins();
- *
- * // Discover all worker plugins
- * List workerPlugins = PluginDiscovery.discoverWorkerPlugins();
- *
- * // Discover all plugins (both types, deduplicated)
- * List allPlugins = PluginDiscovery.discoverAllPlugins();
- *
- * // Use with client options
- * WorkflowClientOptions options = WorkflowClientOptions.newBuilder()
- *     .setPlugins(allPlugins)
- *     .build();
- * }
- *
- * @see ServiceLoader
- * @see ClientPlugin
- * @see WorkerPlugin
- */
-@Experimental
-public final class PluginDiscovery {
-
-  private PluginDiscovery() {}
-
-  /**
-   * Discovers all available {@link ClientPlugin}s using the thread's context class loader.
-   *
-   * @return an unmodifiable list of discovered client plugins
-   */
-  @Nonnull
-  public static List discoverClientPlugins() {
-    return discoverClientPlugins(Thread.currentThread().getContextClassLoader());
-  }
-
-  /**
-   * Discovers all available {@link ClientPlugin}s using the specified class loader.
-   *
-   * @param classLoader the class loader to use for discovery
-   * @return an unmodifiable list of discovered client plugins
-   */
-  @Nonnull
-  public static List discoverClientPlugins(@Nonnull ClassLoader classLoader) {
-    ServiceLoader loader = ServiceLoader.load(ClientPlugin.class, classLoader);
-    List plugins = new ArrayList<>();
-    for (ClientPlugin plugin : loader) {
-      plugins.add(plugin);
-    }
-    return Collections.unmodifiableList(plugins);
-  }
-
-  /**
-   * Discovers all available {@link WorkerPlugin}s using the thread's context class loader.
-   *
-   * @return an unmodifiable list of discovered worker plugins
-   */
-  @Nonnull
-  public static List discoverWorkerPlugins() {
-    return discoverWorkerPlugins(Thread.currentThread().getContextClassLoader());
-  }
-
-  /**
-   * Discovers all available {@link WorkerPlugin}s using the specified class loader.
-   *
-   * @param classLoader the class loader to use for discovery
-   * @return an unmodifiable list of discovered worker plugins
-   */
-  @Nonnull
-  public static List discoverWorkerPlugins(@Nonnull ClassLoader classLoader) {
-    ServiceLoader loader = ServiceLoader.load(WorkerPlugin.class, classLoader);
-    List plugins = new ArrayList<>();
-    for (WorkerPlugin plugin : loader) {
-      plugins.add(plugin);
-    }
-    return Collections.unmodifiableList(plugins);
-  }
-
-  /**
-   * Discovers all available plugins (both {@link ClientPlugin} and {@link WorkerPlugin}) using the
-   * thread's context class loader. Plugins that implement both interfaces are only included once.
-   *
-   * @return an unmodifiable list of discovered plugins
-   */
-  @Nonnull
-  public static List discoverAllPlugins() {
-    return discoverAllPlugins(Thread.currentThread().getContextClassLoader());
-  }
-
-  /**
-   * Discovers all available plugins (both {@link ClientPlugin} and {@link WorkerPlugin}) using the
-   * specified class loader. Plugins that implement both interfaces are only included once.
-   *
-   * @param classLoader the class loader to use for discovery
-   * @return an unmodifiable list of discovered plugins
-   */
-  @Nonnull
-  public static List discoverAllPlugins(@Nonnull ClassLoader classLoader) {
-    List plugins = new ArrayList<>();
-
-    // Add all ClientPlugins
-    for (ClientPlugin plugin : discoverClientPlugins(classLoader)) {
-      plugins.add(plugin);
-    }
-
-    // Add WorkerPlugins that aren't already in the list (avoid duplicates for dual-interface
-    // plugins)
-    for (WorkerPlugin plugin : discoverWorkerPlugins(classLoader)) {
-      if (!plugins.contains(plugin)) {
-        plugins.add(plugin);
-      }
-    }
-
-    return Collections.unmodifiableList(plugins);
-  }
-}

From 9d4ba4af1c6129f919a14957b14e914a04d5fc63 Mon Sep 17 00:00:00 2001
From: Donald Pinckney 
Date: Wed, 14 Jan 2026 09:30:37 -0500
Subject: [PATCH 03/28] remove notes

---
 PR_NOTES.md | 138 ----------------------------------------------------
 1 file changed, 138 deletions(-)
 delete mode 100644 PR_NOTES.md

diff --git a/PR_NOTES.md b/PR_NOTES.md
deleted file mode 100644
index edadc56995..0000000000
--- a/PR_NOTES.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Plugin System for Java Temporal SDK
-
-## Overview
-
-This PR implements a plugin system for the Java Temporal SDK, modeled after the Python SDK's plugin architecture but adapted to Java idioms and the existing SDK design patterns.
-
-The plugin system provides a higher-level abstraction over the existing interceptor infrastructure, enabling users to:
-- Modify configuration during client/worker creation
-- Wrap execution lifecycles with setup/teardown logic
-- Auto-propagate plugins from client to worker
-- Bundle multiple customizations (interceptors, context propagators, etc.) into reusable units
-
-## Design Decisions
-
-### 1. No Base `Plugin` Interface
-
-**Decision:** `ClientPlugin` and `WorkerPlugin` each define their own `getName()` method independently, rather than sharing a base `Plugin` interface.
-
-**Rationale:** This matches the Python SDK's design. Python has separate `ClientPlugin` and `WorkerPlugin` with `name()` on each. I initially had a base `Plugin` interface but removed it to simplify.
-
-**Alternative considered:** I could add a shared `Plugin` interface with just `getName()`. This would allow `List` instead of `List` for storage. However, this adds an interface that serves no purpose other than type convenience, and Python doesn't have it.
-
-### 2. `ClientPluginCallback` Interface (Module Boundary)
-
-**Decision:** A `ClientPluginCallback` interface exists in `temporal-serviceclient`, which `ClientPlugin` (in `temporal-sdk`) extends.
-
-**Rationale:** This is required due to Java's module architecture:
-- `temporal-serviceclient` contains `WorkflowServiceStubs`
-- `temporal-sdk` depends on `temporal-serviceclient` (not vice versa)
-- `WorkflowServiceStubs.newServiceStubs(options, plugins)` needs to call plugin methods
-- Since serviceclient cannot import from sdk, I define a minimal callback interface in serviceclient
-
-This is the one structural difference from Python, which uses a single-package architecture where everything can import everything else.
-
-### 3. `PluginBase` Convenience Class
-
-**Decision:** I provide an abstract `PluginBase` class that implements both `ClientPlugin` and `WorkerPlugin`.
-
-**Rationale:** This is a common Java pattern (like `AbstractList` for `List`).
-
-
-### 4. `SimplePluginBuilder` with Private `SimplePlugin`
-
-**Decision:** I provide a builder for creating plugins declaratively, with the implementation class kept private.
-
-**Rationale:**
-- Builder pattern is more natural in Java than Python's constructor with many parameters
-- Private `SimplePlugin` is an implementation detail - users interact with the builder
-- Allows changing implementation without breaking API
-
-```java
-PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin")
-    .addWorkerInterceptors(new TracingInterceptor())
-    .customizeClient(b -> b.setIdentity("custom"))
-    .build();
-```
-
-### 5. No ServiceLoader Discovery
-
-**Decision:** I do not include ServiceLoader-based plugin discovery.
-
-**Rationale:**
-- Python doesn't have this - just uses explicit `plugins=[]`
-- ServiceLoader requires classes with no-arg constructors, which doesn't integrate with `SimplePluginBuilder`
-- "Magic" discovery is harder to debug than explicit configuration
-- Explicit plugin configuration is clearer and sufficient
-
-We could consider adding it in though if there is interest.
-
-
-## Files Changed
-
-### New Files (`temporal-sdk/src/main/java/io/temporal/common/plugin/`)
-- `ClientPlugin.java` - Client-side plugin interface
-- `WorkerPlugin.java` - Worker-side plugin interface
-- `PluginBase.java` - Convenience base class implementing both
-- `SimplePluginBuilder.java` - Builder for declarative plugin creation
-
-### Modified Files
-- `WorkflowServiceStubs.java` - Added `newServiceStubs(options, plugins)` and `ClientPluginCallback` interface
-- `WorkflowClientOptions.java` - Added `plugins` field with builder methods
-- `WorkflowClientInternalImpl.java` - Applies `ClientPlugin.configureClient()` during creation
-- `WorkerFactory.java` - Full plugin lifecycle (configuration, execution, shutdown)
-
-### Test Files (`temporal-sdk/src/test/java/io/temporal/common/plugin/`)
-- `PluginTest.java` - Core plugin interface tests
-- `SimplePluginBuilderTest.java` - Builder API tests
-- `WorkflowClientOptionsPluginTest.java` - Options integration tests
-
-## Example Usage
-
-### Custom Plugin
-```java
-public class TracingPlugin extends PluginBase {
-    private final Tracer tracer;
-
-    public TracingPlugin(Tracer tracer) {
-        super("my-org.tracing");
-        this.tracer = tracer;
-    }
-
-    @Override
-    public WorkflowClientOptions.Builder configureClient(
-            WorkflowClientOptions.Builder builder) {
-        return builder.setInterceptors(new TracingClientInterceptor(tracer));
-    }
-
-    @Override
-    public WorkerFactoryOptions.Builder configureWorkerFactory(
-            WorkerFactoryOptions.Builder builder) {
-        return builder.setWorkerInterceptors(new TracingWorkerInterceptor(tracer));
-    }
-}
-```
-
-### Using SimplePluginBuilder
-```java
-PluginBase metricsPlugin = SimplePluginBuilder.newBuilder("my-org.metrics")
-    .customizeServiceStubs(b -> b.setMetricsScope(myScope))
-    .addWorkerInterceptors(new MetricsInterceptor())
-    .build();
-```
-
-### Client/Worker with Plugins
-```java
-WorkflowClientOptions clientOptions = WorkflowClientOptions.newBuilder()
-    .setNamespace("default")
-    .addPlugin(new TracingPlugin(tracer))
-    .addPlugin(metricsPlugin)
-    .build();
-
-// Plugins that implement WorkerPlugin auto-propagate to workers
-WorkerFactory factory = WorkerFactory.newInstance(client);
-```
-
-## Open Questions
-
-1. **Mark as `@Experimental`?** - I've marked all public APIs as `@Experimental` to allow iteration. Is this appropriate?

From 4688e95228d1ee88043b349021743e00c8ea7d0d Mon Sep 17 00:00:00 2001
From: Donald Pinckney 
Date: Wed, 14 Jan 2026 15:55:46 -0500
Subject: [PATCH 04/28] Add initializeWorker in plugin interface.

---
 .../common/plugin/SimplePluginBuilder.java    | 37 ++++++++++++++++
 .../temporal/common/plugin/WorkerPlugin.java  | 25 +++++++++++
 .../io/temporal/worker/WorkerFactory.java     |  8 ++++
 .../io/temporal/common/plugin/PluginTest.java |  3 ++
 .../plugin/SimplePluginBuilderTest.java       | 42 +++++++++++++++++++
 5 files changed, 115 insertions(+)

diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java
index 4ae061ed83..78006d0a7f 100644
--- a/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java
+++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java
@@ -26,12 +26,14 @@
 import io.temporal.common.interceptors.WorkerInterceptor;
 import io.temporal.common.interceptors.WorkflowClientInterceptor;
 import io.temporal.serviceclient.WorkflowServiceStubsOptions;
+import io.temporal.worker.Worker;
 import io.temporal.worker.WorkerFactoryOptions;
 import io.temporal.worker.WorkerOptions;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import javax.annotation.Nonnull;
 
@@ -69,6 +71,7 @@ public final class SimplePluginBuilder {
   private final List> clientCustomizers = new ArrayList<>();
   private final List> factoryCustomizers = new ArrayList<>();
   private final List> workerCustomizers = new ArrayList<>();
+  private final List> workerInitializers = new ArrayList<>();
   private final List workerInterceptors = new ArrayList<>();
   private final List clientInterceptors = new ArrayList<>();
   private final List contextPropagators = new ArrayList<>();
@@ -139,6 +142,29 @@ public SimplePluginBuilder customizeWorker(@Nonnull ConsumerExample:
+   *
+   * 
{@code
+   * SimplePluginBuilder.newBuilder("my-plugin")
+   *     .initializeWorker((taskQueue, worker) -> {
+   *         worker.registerWorkflowImplementationTypes(MyWorkflow.class);
+   *         worker.registerActivitiesImplementations(new MyActivityImpl());
+   *     })
+   *     .build();
+   * }
+ * + * @param initializer a consumer that receives the task queue name and worker + * @return this builder for chaining + */ + public SimplePluginBuilder initializeWorker(@Nonnull BiConsumer initializer) { + workerInitializers.add(Objects.requireNonNull(initializer)); + return this; + } + /** * Adds worker interceptors. Interceptors are appended to any existing interceptors in the * configuration. @@ -188,6 +214,7 @@ public PluginBase build() { new ArrayList<>(clientCustomizers), new ArrayList<>(factoryCustomizers), new ArrayList<>(workerCustomizers), + new ArrayList<>(workerInitializers), new ArrayList<>(workerInterceptors), new ArrayList<>(clientInterceptors), new ArrayList<>(contextPropagators)); @@ -199,6 +226,7 @@ private static final class SimplePlugin extends PluginBase { private final List> clientCustomizers; private final List> factoryCustomizers; private final List> workerCustomizers; + private final List> workerInitializers; private final List workerInterceptors; private final List clientInterceptors; private final List contextPropagators; @@ -209,6 +237,7 @@ private static final class SimplePlugin extends PluginBase { List> clientCustomizers, List> factoryCustomizers, List> workerCustomizers, + List> workerInitializers, List workerInterceptors, List clientInterceptors, List contextPropagators) { @@ -217,6 +246,7 @@ private static final class SimplePlugin extends PluginBase { this.clientCustomizers = clientCustomizers; this.factoryCustomizers = factoryCustomizers; this.workerCustomizers = workerCustomizers; + this.workerInitializers = workerInitializers; this.workerInterceptors = workerInterceptors; this.clientInterceptors = clientInterceptors; this.contextPropagators = contextPropagators; @@ -292,5 +322,12 @@ public WorkerOptions.Builder configureWorker( } return builder; } + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { + for (BiConsumer initializer : workerInitializers) { + initializer.accept(taskQueue, worker); + } + } } } diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java index ec2a730037..7f02458fb6 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java @@ -21,6 +21,7 @@ package io.temporal.common.plugin; import io.temporal.common.Experimental; +import io.temporal.worker.Worker; import io.temporal.worker.WorkerFactory; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; @@ -103,6 +104,30 @@ default WorkerOptions.Builder configureWorker( return 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}. + * + *

Example: + * + *

{@code
+   * @Override
+   * public void initializeWorker(String taskQueue, Worker worker) {
+   *     worker.registerWorkflowImplementationTypes(MyWorkflow.class);
+   *     worker.registerActivitiesImplementations(new MyActivityImpl());
+   * }
+   * }
+ * + * @param taskQueue the task queue name for the worker + * @param worker the newly created worker + */ + default void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { + // Default: no-op + } + /** * Allows the plugin to wrap worker factory startup. Called during execution phase in reverse * order (first plugin wraps all others). 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 daf10b1b78..30efde5566 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -169,6 +169,14 @@ 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 (Object plugin : plugins) { + if (plugin instanceof WorkerPlugin) { + ((WorkerPlugin) plugin).initializeWorker(taskQueue, worker); + } + } + return worker; } else { log.warn( diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java index 7860b6c04e..0e526b62ff 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java @@ -97,6 +97,9 @@ public String getName() { final boolean[] called = {false}; plugin.runWorkerFactory(null, () -> called[0] = true); assertTrue("runWorkerFactory should call next", called[0]); + + // Test default initializeWorker is a no-op (doesn't throw) + plugin.initializeWorker("test-queue", null); } @Test diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java index bd49eabf94..73907c79a6 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java @@ -192,6 +192,48 @@ public void testInterceptorsAppendToExisting() { assertSame(newInterceptor, interceptors[1]); } + @Test + public void testInitializeWorker() { + AtomicBoolean initialized = new AtomicBoolean(false); + String[] capturedTaskQueue = {null}; + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .initializeWorker( + (taskQueue, worker) -> { + initialized.set(true); + capturedTaskQueue[0] = taskQueue; + }) + .build(); + + // Call initializeWorker with null worker (we're just testing the callback is invoked) + ((WorkerPlugin) plugin).initializeWorker("my-task-queue", null); + + assertTrue("Initializer should have been called", initialized.get()); + assertEquals("my-task-queue", capturedTaskQueue[0]); + } + + @Test + public void testMultipleWorkerInitializers() { + AtomicInteger callCount = new AtomicInteger(0); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) + .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) + .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) + .build(); + + ((WorkerPlugin) plugin).initializeWorker("test-queue", null); + + assertEquals("All initializers should be called", 3, callCount.get()); + } + + @Test(expected = NullPointerException.class) + public void testNullInitializeWorker() { + SimplePluginBuilder.newBuilder("test").initializeWorker(null); + } + @Test(expected = NullPointerException.class) public void testNullName() { SimplePluginBuilder.newBuilder(null); From 9007d0f248d936dea086f5ef4a52eac61c2ccfa7 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Wed, 14 Jan 2026 16:10:10 -0500 Subject: [PATCH 05/28] Re-do shutdownWorkerFactory design with chain --- .../temporal/common/plugin/WorkerPlugin.java | 25 +++++++++---- .../io/temporal/worker/WorkerFactory.java | 36 +++++++++++++------ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java index 7f02458fb6..57d38f1466 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java @@ -156,15 +156,28 @@ default void runWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable } /** - * Called when the worker factory is shutting down. Plugins are notified in forward (registration) - * order during shutdown. + * 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. * - *

This is called during both {@link WorkerFactory#shutdown()} and {@link - * WorkerFactory#shutdownNow()}. + *

Example: + * + *

{@code
+   * @Override
+   * public void shutdownWorkerFactory(WorkerFactory factory, Runnable next) {
+   *     logger.info("Shutting down workers...");
+   *     next.run();
+   *     logger.info("Workers shut down");
+   * }
+   * }
* * @param factory the worker factory being shut down + * @param next runnable that shuts down the next in chain (eventually shuts down actual workers) */ - default void onWorkerFactoryShutdown(@Nonnull WorkerFactory factory) { - // Default: no-op + default void shutdownWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) { + next.run(); } } 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 30efde5566..5b65c287b8 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -170,7 +170,8 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) { workflowClient.getOptions().getContextPropagators()); workers.put(taskQueue, worker); - // Go through the plugins to call plugin initializeWorker hooks (e.g. register workflows, activities, etc.) + // Go through the plugins to call plugin initializeWorker hooks (e.g. register workflows, + // activities, etc.) for (Object plugin : plugins) { if (plugin instanceof WorkerPlugin) { ((WorkerPlugin) plugin).initializeWorker(taskQueue, worker); @@ -339,20 +340,33 @@ public synchronized void shutdownNow() { private void shutdownInternal(boolean interruptUserTasks) { state = State.Shutdown; - // Notify plugins of shutdown (forward order) - for (Object plugin : plugins) { + // Build plugin shutdown chain (reverse order for proper nesting) + Runnable shutdownChain = () -> doShutdown(interruptUserTasks); + List reversed = new ArrayList<>(plugins); + Collections.reverse(reversed); + for (Object plugin : reversed) { if (plugin instanceof WorkerPlugin) { - try { - ((WorkerPlugin) plugin).onWorkerFactoryShutdown(this); - } catch (Exception e) { - log.warn( - "Plugin {} failed during shutdown notification", - ((WorkerPlugin) plugin).getName(), - e); - } + final Runnable next = shutdownChain; + final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; + 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( From 6a1ae399aefb90c13cb1219eb887bfad828302b3 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Wed, 14 Jan 2026 16:11:22 -0500 Subject: [PATCH 06/28] rename runWorkerFactory -> startWorkerFactory --- .../java/io/temporal/common/plugin/WorkerPlugin.java | 6 +++--- .../main/java/io/temporal/worker/WorkerFactory.java | 2 +- .../java/io/temporal/common/plugin/PluginTest.java | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java index 57d38f1466..0615f0a298 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java @@ -51,7 +51,7 @@ * } * * @Override - * public void runWorkerFactory(WorkerFactory factory, Runnable next) throws Exception { + * public void startWorkerFactory(WorkerFactory factory, Runnable next) throws Exception { * registry.recordWorkerStart(); * try { * next.run(); @@ -139,7 +139,7 @@ default void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) * *
{@code
    * @Override
-   * public void runWorkerFactory(WorkerFactory factory, Runnable next) throws Exception {
+   * public void startWorkerFactory(WorkerFactory factory, Runnable next) throws Exception {
    *     logger.info("Starting workers...");
    *     next.run();
    *     logger.info("Workers started");
@@ -150,7 +150,7 @@ default void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker)
    * @param next runnable that starts the next in chain (eventually starts actual workers)
    * @throws Exception if startup fails
    */
-  default void runWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next)
+  default void startWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next)
       throws Exception {
     next.run();
   }
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 5b65c287b8..ddbfe34d9c 100644
--- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java
+++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java
@@ -247,7 +247,7 @@ public synchronized void start() {
         startChain =
             () -> {
               try {
-                workerPlugin.runWorkerFactory(this, next);
+                workerPlugin.startWorkerFactory(this, next);
               } catch (RuntimeException e) {
                 throw e;
               } catch (Exception e) {
diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java
index 0e526b62ff..8d3174dea1 100644
--- a/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java
+++ b/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java
@@ -93,10 +93,10 @@ public String getName() {
     WorkerOptions.Builder workerBuilder = WorkerOptions.newBuilder();
     assertSame(workerBuilder, plugin.configureWorker("test-queue", workerBuilder));
 
-    // Test runWorkerFactory calls next
+    // Test startWorkerFactory calls next
     final boolean[] called = {false};
-    plugin.runWorkerFactory(null, () -> called[0] = true);
-    assertTrue("runWorkerFactory should call next", called[0]);
+    plugin.startWorkerFactory(null, () -> called[0] = true);
+    assertTrue("startWorkerFactory should call next", called[0]);
 
     // Test default initializeWorker is a no-op (doesn't throw)
     plugin.initializeWorker("test-queue", null);
@@ -150,7 +150,7 @@ public void testExecutionPhaseReverseOrder() throws Exception {
             () -> {
               order.add(workerPlugin.getName() + "-before");
               try {
-                workerPlugin.runWorkerFactory(null, next);
+                workerPlugin.startWorkerFactory(null, next);
               } catch (Exception e) {
                 throw new RuntimeException(e);
               }
@@ -192,7 +192,7 @@ public WorkflowClientOptions.Builder configureClient(WorkflowClientOptions.Build
   private PluginBase createExecutionTrackingPlugin(String name, List order) {
     return new PluginBase(name) {
       @Override
-      public void runWorkerFactory(io.temporal.worker.WorkerFactory factory, Runnable next) {
+      public void startWorkerFactory(io.temporal.worker.WorkerFactory factory, Runnable next) {
         next.run();
       }
     };

From 057dc14078d82d39d5d5c81dd3b9bb7629222f9e Mon Sep 17 00:00:00 2001
From: Donald Pinckney 
Date: Wed, 14 Jan 2026 16:30:02 -0500
Subject: [PATCH 07/28] Move plugins around

---
 .../ClientPlugin.java => client/Plugin.java}  |  8 ++---
 .../client/WorkflowClientInternalImpl.java    |  5 ++-
 .../client/WorkflowClientOptions.java         | 31 +++++++++----------
 .../common/{plugin => }/PluginBase.java       | 14 ++++-----
 .../{plugin => }/SimplePluginBuilder.java     | 15 +++++----
 .../WorkerPlugin.java => worker/Plugin.java}  | 15 ++++-----
 .../io/temporal/worker/WorkerFactory.java     | 25 +++++++--------
 .../WorkflowClientOptionsPluginTest.java      | 16 +++++-----
 .../common/{plugin => }/PluginTest.java       | 24 +++++++-------
 .../{plugin => }/SimplePluginBuilderTest.java | 28 +++++++++--------
 .../serviceclient/WorkflowServiceStubs.java   |  5 +--
 11 files changed, 92 insertions(+), 94 deletions(-)
 rename temporal-sdk/src/main/java/io/temporal/{common/plugin/ClientPlugin.java => client/Plugin.java} (96%)
 rename temporal-sdk/src/main/java/io/temporal/common/{plugin => }/PluginBase.java (88%)
 rename temporal-sdk/src/main/java/io/temporal/common/{plugin => }/SimplePluginBuilder.java (97%)
 rename temporal-sdk/src/main/java/io/temporal/{common/plugin/WorkerPlugin.java => worker/Plugin.java} (93%)
 rename temporal-sdk/src/test/java/io/temporal/{common/plugin => client}/WorkflowClientOptionsPluginTest.java (88%)
 rename temporal-sdk/src/test/java/io/temporal/common/{plugin => }/PluginTest.java (89%)
 rename temporal-sdk/src/test/java/io/temporal/common/{plugin => }/SimplePluginBuilderTest.java (88%)

diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/Plugin.java
similarity index 96%
rename from temporal-sdk/src/main/java/io/temporal/common/plugin/ClientPlugin.java
rename to temporal-sdk/src/main/java/io/temporal/client/Plugin.java
index c4d486c787..db410f232c 100644
--- a/temporal-sdk/src/main/java/io/temporal/common/plugin/ClientPlugin.java
+++ b/temporal-sdk/src/main/java/io/temporal/client/Plugin.java
@@ -18,10 +18,10 @@
  * limitations under the License.
  */
 
-package io.temporal.common.plugin;
+package io.temporal.client;
 
-import io.temporal.client.WorkflowClientOptions;
 import io.temporal.common.Experimental;
+import io.temporal.common.PluginBase;
 import io.temporal.serviceclient.WorkflowServiceStubs;
 import io.temporal.serviceclient.WorkflowServiceStubs.ClientPluginCallback;
 import io.temporal.serviceclient.WorkflowServiceStubsOptions;
@@ -65,11 +65,11 @@
  * }
  * }
* - * @see WorkerPlugin + * @see io.temporal.worker.Plugin * @see PluginBase */ @Experimental -public interface ClientPlugin extends ClientPluginCallback { +public interface Plugin extends ClientPluginCallback { /** * Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended 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 65eb49822c..f6893b1841 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -15,7 +15,6 @@ import io.temporal.common.WorkflowExecutionHistory; import io.temporal.common.interceptors.WorkflowClientCallsInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; -import io.temporal.common.plugin.ClientPlugin; import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.client.*; import io.temporal.internal.client.NexusStartWorkflowResponse; @@ -788,8 +787,8 @@ private static WorkflowClientOptions applyClientPluginConfiguration( WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options); for (Object plugin : plugins) { - if (plugin instanceof ClientPlugin) { - builder = ((ClientPlugin) plugin).configureClient(builder); + if (plugin instanceof Plugin) { + builder = ((Plugin) plugin).configureClient(builder); } } return builder.build(); 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 157a6c3837..19c14e3a0f 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java @@ -140,15 +140,14 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition * Sets the plugins to use with this client. Plugins can modify client and worker configuration, * intercept connection, and wrap execution lifecycle. * - *

Each plugin should implement {@link io.temporal.common.plugin.ClientPlugin} and/or {@link - * io.temporal.common.plugin.WorkerPlugin}. Plugins that implement both interfaces are - * automatically propagated to workers created from this client. + *

Each plugin should implement {@link io.temporal.client.Plugin} and/or {@link + * io.temporal.worker.Plugin}. Plugins that implement both interfaces are automatically + * propagated to workers created from this client. * - * @param plugins the list of plugins to use (each should implement ClientPlugin and/or - * WorkerPlugin) + * @param plugins the list of plugins to use (each should implement Plugin) * @return this builder for chaining - * @see io.temporal.common.plugin.ClientPlugin - * @see io.temporal.common.plugin.WorkerPlugin + * @see io.temporal.client.Plugin + * @see io.temporal.worker.Plugin */ @Experimental public Builder setPlugins(List plugins) { @@ -160,14 +159,14 @@ public Builder setPlugins(List plugins) { * Adds a plugin to use with this client. Plugins can modify client and worker configuration, * intercept connection, and wrap execution lifecycle. * - *

The plugin should implement {@link io.temporal.common.plugin.ClientPlugin} and/or {@link - * io.temporal.common.plugin.WorkerPlugin}. Plugins that implement both interfaces are - * automatically propagated to workers created from this client. + *

The plugin should implement {@link io.temporal.client.Plugin} and/or {@link + * io.temporal.worker.Plugin}. Plugins that implement both interfaces are automatically + * propagated to workers created from this client. * - * @param plugin the plugin to add (should implement ClientPlugin and/or WorkerPlugin) + * @param plugin the plugin to add (should implement Plugin) * @return this builder for chaining - * @see io.temporal.common.plugin.ClientPlugin - * @see io.temporal.common.plugin.WorkerPlugin + * @see io.temporal.client.Plugin + * @see io.temporal.worker.Plugin */ @Experimental public Builder addPlugin(Object plugin) { @@ -293,9 +292,9 @@ public QueryRejectCondition getQueryRejectCondition() { /** * Returns the list of plugins configured for this client. * - *

Each plugin implements {@link io.temporal.common.plugin.ClientPlugin} and/or {@link - * io.temporal.common.plugin.WorkerPlugin}. Plugins that implement both interfaces are - * automatically propagated to workers created from this client. + *

Each plugin implements {@link io.temporal.client.Plugin} and/or {@link + * io.temporal.worker.Plugin}. Plugins that implement both interfaces are automatically propagated + * to workers created from this client. * * @return an unmodifiable list of plugins, never null */ diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginBase.java b/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java similarity index 88% rename from temporal-sdk/src/main/java/io/temporal/common/plugin/PluginBase.java rename to temporal-sdk/src/main/java/io/temporal/common/PluginBase.java index 346dbc0884..e6ca55d5de 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/plugin/PluginBase.java +++ b/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java @@ -18,15 +18,15 @@ * limitations under the License. */ -package io.temporal.common.plugin; +package io.temporal.common; -import io.temporal.common.Experimental; +import io.temporal.client.Plugin; import java.util.Objects; import javax.annotation.Nonnull; /** - * Convenience base class for plugins that implement both {@link ClientPlugin} and {@link - * WorkerPlugin}. All methods have default no-op implementations. + * Convenience base class for plugins that implement both {@link io.temporal.client.Plugin} and + * {@link io.temporal.worker.Plugin}. All methods have default no-op implementations. * *

This is the recommended way to create plugins that need to customize both client and worker * behavior. Plugins that extend this class will automatically be propagated from the client to @@ -59,11 +59,11 @@ * } * } * - * @see ClientPlugin - * @see WorkerPlugin + * @see io.temporal.client.Plugin + * @see io.temporal.worker.Plugin */ @Experimental -public abstract class PluginBase implements ClientPlugin, WorkerPlugin { +public abstract class PluginBase implements Plugin, io.temporal.worker.Plugin { private final String name; diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java similarity index 97% rename from temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java rename to temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java index 78006d0a7f..b708d5aa04 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/plugin/SimplePluginBuilder.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java @@ -18,10 +18,9 @@ * limitations under the License. */ -package io.temporal.common.plugin; +package io.temporal.common; import io.temporal.client.WorkflowClientOptions; -import io.temporal.common.Experimental; import io.temporal.common.context.ContextPropagator; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -41,8 +40,8 @@ * Builder for creating simple plugins that only need to modify configuration. * *

This builder provides a declarative way to create plugins for common use cases without - * subclassing {@link PluginBase}. The resulting plugin implements both {@link ClientPlugin} and - * {@link WorkerPlugin}. + * subclassing {@link PluginBase}. The resulting plugin implements both {@link + * io.temporal.client.Plugin} and {@link io.temporal.worker.Plugin}. * *

Example: * @@ -59,8 +58,8 @@ * } * * @see PluginBase - * @see ClientPlugin - * @see WorkerPlugin + * @see io.temporal.client.Plugin + * @see io.temporal.worker.Plugin */ @Experimental public final class SimplePluginBuilder { @@ -204,8 +203,8 @@ public SimplePluginBuilder addContextPropagators(ContextPropagator... propagator /** * Builds the plugin with the configured settings. * - * @return a new plugin instance that implements both {@link ClientPlugin} and {@link - * WorkerPlugin} + * @return a new plugin instance that implements both {@link io.temporal.client.Plugin} and {@link + * io.temporal.worker.Plugin} */ public PluginBase build() { return new SimplePlugin( diff --git a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java similarity index 93% rename from temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java rename to temporal-sdk/src/main/java/io/temporal/worker/Plugin.java index 0615f0a298..6c1fa614e6 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/plugin/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java @@ -18,20 +18,17 @@ * limitations under the License. */ -package io.temporal.common.plugin; +package io.temporal.worker; import io.temporal.common.Experimental; -import io.temporal.worker.Worker; -import io.temporal.worker.WorkerFactory; -import io.temporal.worker.WorkerFactoryOptions; -import io.temporal.worker.WorkerOptions; +import io.temporal.common.PluginBase; import javax.annotation.Nonnull; /** * Plugin interface for customizing Temporal worker configuration and lifecycle. * - *

WorkerPlugins that also implement {@link ClientPlugin} are automatically propagated from the - * client to workers created from that client. + *

Plugins that also implement {@link io.temporal.client.Plugin} are automatically propagated + * from the client to workers created from that client. * *

Example implementation: * @@ -62,11 +59,11 @@ * } * } * - * @see ClientPlugin + * @see io.temporal.client.Plugin * @see PluginBase */ @Experimental -public interface WorkerPlugin { +public interface Plugin { /** * Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended 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 ddbfe34d9c..f5db68912e 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -8,7 +8,6 @@ import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.common.converter.DataConverter; -import io.temporal.common.plugin.WorkerPlugin; import io.temporal.internal.client.WorkflowClientInternal; import io.temporal.internal.sync.WorkflowThreadExecutor; import io.temporal.internal.task.VirtualThreadDelegate; @@ -173,8 +172,8 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) { // Go through the plugins to call plugin initializeWorker hooks (e.g. register workflows, // activities, etc.) for (Object plugin : plugins) { - if (plugin instanceof WorkerPlugin) { - ((WorkerPlugin) plugin).initializeWorker(taskQueue, worker); + if (plugin instanceof Plugin) { + ((Plugin) plugin).initializeWorker(taskQueue, worker); } } @@ -241,9 +240,9 @@ public synchronized void start() { List reversed = new ArrayList<>(plugins); Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof WorkerPlugin) { + if (plugin instanceof Plugin) { final Runnable next = startChain; - final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; + final Plugin workerPlugin = (Plugin) plugin; startChain = () -> { try { @@ -345,9 +344,9 @@ private void shutdownInternal(boolean interruptUserTasks) { List reversed = new ArrayList<>(plugins); Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof WorkerPlugin) { + if (plugin instanceof Plugin) { final Runnable next = shutdownChain; - final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; + final Plugin workerPlugin = (Plugin) plugin; shutdownChain = () -> { try { @@ -442,7 +441,7 @@ public String toString() { /** * Extracts worker plugins from the client plugins list. Only plugins that implement {@link - * WorkerPlugin} are included. + * Plugin} are included. */ private static List extractWorkerPlugins(List clientPlugins) { if (clientPlugins == null || clientPlugins.isEmpty()) { @@ -451,7 +450,7 @@ private static List extractWorkerPlugins(List clientPlugins) { List workerPlugins = new ArrayList<>(); for (Object plugin : clientPlugins) { - if (plugin instanceof WorkerPlugin) { + if (plugin instanceof Plugin) { workerPlugins.add(plugin); } } @@ -474,8 +473,8 @@ private static WorkerFactoryOptions applyPluginConfiguration( : WorkerFactoryOptions.newBuilder(options); for (Object plugin : plugins) { - if (plugin instanceof WorkerPlugin) { - builder = ((WorkerPlugin) plugin).configureWorkerFactory(builder); + if (plugin instanceof Plugin) { + builder = ((Plugin) plugin).configureWorkerFactory(builder); } } return builder.build(); @@ -495,8 +494,8 @@ private static WorkerOptions applyWorkerPluginConfiguration( options == null ? WorkerOptions.newBuilder() : WorkerOptions.newBuilder(options); for (Object plugin : plugins) { - if (plugin instanceof WorkerPlugin) { - builder = ((WorkerPlugin) plugin).configureWorker(taskQueue, builder); + if (plugin instanceof Plugin) { + builder = ((Plugin) plugin).configureWorker(taskQueue, builder); } } return builder.build(); diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java similarity index 88% rename from temporal-sdk/src/test/java/io/temporal/common/plugin/WorkflowClientOptionsPluginTest.java rename to temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java index 282762fc2b..22a4e2b247 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/plugin/WorkflowClientOptionsPluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java @@ -18,11 +18,11 @@ * limitations under the License. */ -package io.temporal.common.plugin; +package io.temporal.client; import static org.junit.Assert.*; -import io.temporal.client.WorkflowClientOptions; +import io.temporal.common.PluginBase; import java.util.Arrays; import java.util.List; import org.junit.Test; @@ -45,8 +45,8 @@ public void testSetPlugins() { List plugins = options.getPlugins(); assertEquals(2, plugins.size()); - assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); - assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); + assertEquals("plugin1", ((Plugin) plugins.get(0)).getName()); + assertEquals("plugin2", ((Plugin) plugins.get(1)).getName()); } @Test @@ -59,8 +59,8 @@ public void testAddPlugin() { List plugins = options.getPlugins(); assertEquals(2, plugins.size()); - assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); - assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); + assertEquals("plugin1", ((Plugin) plugins.get(0)).getName()); + assertEquals("plugin2", ((Plugin) plugins.get(1)).getName()); } @Test @@ -99,7 +99,7 @@ public void testToBuilder() { WorkflowClientOptions copy = original.toBuilder().build(); assertEquals(1, copy.getPlugins().size()); - assertEquals("plugin", ((ClientPlugin) copy.getPlugins().get(0)).getName()); + assertEquals("plugin", ((Plugin) copy.getPlugins().get(0)).getName()); } @Test @@ -110,7 +110,7 @@ public void testValidateAndBuildWithDefaults() { WorkflowClientOptions.newBuilder().addPlugin(plugin).validateAndBuildWithDefaults(); assertEquals(1, options.getPlugins().size()); - assertEquals("plugin", ((ClientPlugin) options.getPlugins().get(0)).getName()); + assertEquals("plugin", ((Plugin) options.getPlugins().get(0)).getName()); } @Test diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java similarity index 89% rename from temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java rename to temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index 8d3174dea1..716401c23d 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/plugin/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package io.temporal.common.plugin; +package io.temporal.common; import static org.junit.Assert.*; @@ -58,8 +58,8 @@ public void testPluginBaseNullName() { @Test public void testClientPluginDefaultMethods() throws Exception { - ClientPlugin plugin = - new ClientPlugin() { + io.temporal.client.Plugin plugin = + new io.temporal.client.Plugin() { @Override public String getName() { return "test"; @@ -77,8 +77,8 @@ public String getName() { @Test public void testWorkerPluginDefaultMethods() throws Exception { - WorkerPlugin plugin = - new WorkerPlugin() { + io.temporal.worker.Plugin plugin = + new io.temporal.worker.Plugin() { @Override public String getName() { return "test"; @@ -115,8 +115,8 @@ public void testConfigurationPhaseOrder() { // Simulate configuration phase (forward order) WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); for (Object plugin : plugins) { - if (plugin instanceof ClientPlugin) { - builder = ((ClientPlugin) plugin).configureClient(builder); + if (plugin instanceof io.temporal.client.Plugin) { + builder = ((io.temporal.client.Plugin) plugin).configureClient(builder); } } @@ -143,9 +143,9 @@ public void testExecutionPhaseReverseOrder() throws Exception { List reversed = new ArrayList<>(plugins); java.util.Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof WorkerPlugin) { + if (plugin instanceof io.temporal.worker.Plugin) { final Runnable next = chain; - final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; + final io.temporal.worker.Plugin workerPlugin = (io.temporal.worker.Plugin) plugin; chain = () -> { order.add(workerPlugin.getName() + "-before"); @@ -175,8 +175,10 @@ public void testPluginBaseImplementsBothInterfaces() { // empty implementation }; - assertTrue("PluginBase should implement ClientPlugin", plugin instanceof ClientPlugin); - assertTrue("PluginBase should implement WorkerPlugin", plugin instanceof WorkerPlugin); + assertTrue( + "PluginBase should implement client Plugin", plugin instanceof io.temporal.client.Plugin); + assertTrue( + "PluginBase should implement worker Plugin", plugin instanceof io.temporal.worker.Plugin); } private PluginBase createTrackingPlugin(String name, List order) { diff --git a/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java similarity index 88% rename from temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java rename to temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index 73907c79a6..cbda667fcd 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/plugin/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -18,7 +18,7 @@ * limitations under the License. */ -package io.temporal.common.plugin; +package io.temporal.common; import static org.junit.Assert.*; @@ -45,8 +45,10 @@ public void testSimplePluginName() { @Test public void testSimplePluginImplementsBothInterfaces() { PluginBase plugin = SimplePluginBuilder.newBuilder("test").build(); - assertTrue("Should implement ClientPlugin", plugin instanceof ClientPlugin); - assertTrue("Should implement WorkerPlugin", plugin instanceof WorkerPlugin); + assertTrue( + "Should implement io.temporal.client.Plugin", plugin instanceof io.temporal.client.Plugin); + assertTrue( + "Should implement io.temporal.worker.Plugin", plugin instanceof io.temporal.worker.Plugin); } @Test @@ -62,7 +64,7 @@ public void testCustomizeServiceStubs() { .build(); WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(); - ((ClientPlugin) plugin).configureServiceStubs(builder); + ((io.temporal.client.Plugin) plugin).configureServiceStubs(builder); assertTrue("Customizer should have been called", customized.get()); } @@ -81,7 +83,7 @@ public void testCustomizeClient() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((ClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.Plugin) plugin).configureClient(builder); assertTrue("Customizer should have been called", customized.get()); assertEquals("custom-identity", builder.build().getIdentity()); @@ -101,7 +103,7 @@ public void testCustomizeWorkerFactory() { .build(); WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder(); - ((WorkerPlugin) plugin).configureWorkerFactory(builder); + ((io.temporal.worker.Plugin) plugin).configureWorkerFactory(builder); assertTrue("Customizer should have been called", customized.get()); assertEquals(100, builder.build().getWorkflowCacheSize()); @@ -121,7 +123,7 @@ public void testCustomizeWorker() { .build(); WorkerOptions.Builder builder = WorkerOptions.newBuilder(); - ((WorkerPlugin) plugin).configureWorker("test-queue", builder); + ((io.temporal.worker.Plugin) plugin).configureWorker("test-queue", builder); assertTrue("Customizer should have been called", customized.get()); assertEquals(50, builder.build().getMaxConcurrentActivityExecutionSize()); @@ -139,7 +141,7 @@ public void testMultipleCustomizers() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((ClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.Plugin) plugin).configureClient(builder); assertEquals("All customizers should be called", 3, callCount.get()); } @@ -152,7 +154,7 @@ public void testAddWorkerInterceptors() { SimplePluginBuilder.newBuilder("test").addWorkerInterceptors(interceptor).build(); WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder(); - ((WorkerPlugin) plugin).configureWorkerFactory(builder); + ((io.temporal.worker.Plugin) plugin).configureWorkerFactory(builder); WorkerInterceptor[] interceptors = builder.build().getWorkerInterceptors(); assertEquals(1, interceptors.length); @@ -167,7 +169,7 @@ public void testAddClientInterceptors() { SimplePluginBuilder.newBuilder("test").addClientInterceptors(interceptor).build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((ClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.Plugin) plugin).configureClient(builder); WorkflowClientInterceptor[] interceptors = builder.build().getInterceptors(); assertEquals(1, interceptors.length); @@ -184,7 +186,7 @@ public void testInterceptorsAppendToExisting() { WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder().setWorkerInterceptors(existingInterceptor); - ((WorkerPlugin) plugin).configureWorkerFactory(builder); + ((io.temporal.worker.Plugin) plugin).configureWorkerFactory(builder); WorkerInterceptor[] interceptors = builder.build().getWorkerInterceptors(); assertEquals(2, interceptors.length); @@ -207,7 +209,7 @@ public void testInitializeWorker() { .build(); // Call initializeWorker with null worker (we're just testing the callback is invoked) - ((WorkerPlugin) plugin).initializeWorker("my-task-queue", null); + ((io.temporal.worker.Plugin) plugin).initializeWorker("my-task-queue", null); assertTrue("Initializer should have been called", initialized.get()); assertEquals("my-task-queue", capturedTaskQueue[0]); @@ -224,7 +226,7 @@ public void testMultipleWorkerInitializers() { .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) .build(); - ((WorkerPlugin) plugin).initializeWorker("test-queue", null); + ((io.temporal.worker.Plugin) plugin).initializeWorker("test-queue", null); assertEquals("All initializers should be called", 3, callCount.get()); } diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index acd18473aa..18f6665d06 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -142,7 +142,8 @@ static WorkflowServiceStubs newInstance( * the creation time and happens on the first request. * * @param options stub options to use - * @param plugins list of plugins to apply (plugins implementing ClientPlugin are processed) + * @param plugins list of plugins to apply (plugins implementing io.temporal.client.Plugin are + * processed) * @return the workflow service stubs */ static WorkflowServiceStubs newServiceStubs( @@ -185,7 +186,7 @@ static WorkflowServiceStubs newServiceStubs( /** * Callback interface for client plugins to participate in service stubs creation. This interface - * is implemented by {@code ClientPlugin} in the temporal-sdk module. + * is implemented by {@code io.temporal.client.Plugin} in the temporal-sdk module. */ interface ClientPluginCallback { /** From fb2f2e61c1d489f4c34acfc7943b9da5da27146e Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Wed, 14 Jan 2026 17:24:19 -0500 Subject: [PATCH 08/28] add startWorker and shutdownWorker --- .../temporal/common/SimplePluginBuilder.java | 82 +++++++++++++ .../main/java/io/temporal/worker/Plugin.java | 65 +++++++++- .../io/temporal/worker/WorkerFactory.java | 84 ++++++++++++- .../java/io/temporal/common/PluginTest.java | 115 ++++++++++++++++++ .../common/SimplePluginBuilderTest.java | 88 ++++++++++++++ 5 files changed, 427 insertions(+), 7 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java index b708d5aa04..775352419f 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java @@ -71,6 +71,8 @@ public final class SimplePluginBuilder { private final List> factoryCustomizers = new ArrayList<>(); private final List> workerCustomizers = new ArrayList<>(); private final List> workerInitializers = new ArrayList<>(); + private final List> workerStartCallbacks = new ArrayList<>(); + private final List> workerShutdownCallbacks = new ArrayList<>(); private final List workerInterceptors = new ArrayList<>(); private final List clientInterceptors = new ArrayList<>(); private final List contextPropagators = new ArrayList<>(); @@ -164,6 +166,59 @@ public SimplePluginBuilder initializeWorker(@Nonnull BiConsumer 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. + * + *

Example: + * + *

{@code
+   * SimplePluginBuilder.newBuilder("my-plugin")
+   *     .onWorkerStart((taskQueue, worker) -> {
+   *         logger.info("Worker started for task queue: {}", taskQueue);
+   *         perWorkerResources.put(taskQueue, new ResourcePool());
+   *     })
+   *     .build();
+   * }
+ * + * @param callback a consumer that receives the task queue name and worker when the worker starts + * @return this builder for chaining + */ + public SimplePluginBuilder 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}. + * + *

Example: + * + *

{@code
+   * SimplePluginBuilder.newBuilder("my-plugin")
+   *     .onWorkerShutdown((taskQueue, worker) -> {
+   *         logger.info("Worker shutting down for task queue: {}", taskQueue);
+   *         ResourcePool pool = perWorkerResources.remove(taskQueue);
+   *         if (pool != null) {
+   *             pool.close();
+   *         }
+   *     })
+   *     .build();
+   * }
+ * + * @param callback a consumer that receives the task queue name and worker when the worker shuts + * down + * @return this builder for chaining + */ + public SimplePluginBuilder onWorkerShutdown(@Nonnull BiConsumer callback) { + workerShutdownCallbacks.add(Objects.requireNonNull(callback)); + return this; + } + /** * Adds worker interceptors. Interceptors are appended to any existing interceptors in the * configuration. @@ -214,6 +269,8 @@ public PluginBase build() { new ArrayList<>(factoryCustomizers), new ArrayList<>(workerCustomizers), new ArrayList<>(workerInitializers), + new ArrayList<>(workerStartCallbacks), + new ArrayList<>(workerShutdownCallbacks), new ArrayList<>(workerInterceptors), new ArrayList<>(clientInterceptors), new ArrayList<>(contextPropagators)); @@ -226,6 +283,8 @@ private static final class SimplePlugin extends PluginBase { private final List> factoryCustomizers; private final List> workerCustomizers; private final List> workerInitializers; + private final List> workerStartCallbacks; + private final List> workerShutdownCallbacks; private final List workerInterceptors; private final List clientInterceptors; private final List contextPropagators; @@ -237,6 +296,8 @@ private static final class SimplePlugin extends PluginBase { List> factoryCustomizers, List> workerCustomizers, List> workerInitializers, + List> workerStartCallbacks, + List> workerShutdownCallbacks, List workerInterceptors, List clientInterceptors, List contextPropagators) { @@ -246,6 +307,8 @@ private static final class SimplePlugin extends PluginBase { this.factoryCustomizers = factoryCustomizers; this.workerCustomizers = workerCustomizers; this.workerInitializers = workerInitializers; + this.workerStartCallbacks = workerStartCallbacks; + this.workerShutdownCallbacks = workerShutdownCallbacks; this.workerInterceptors = workerInterceptors; this.clientInterceptors = clientInterceptors; this.contextPropagators = contextPropagators; @@ -328,5 +391,24 @@ public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) initializer.accept(taskQueue, worker); } } + + @Override + public void startWorker( + @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) + throws Exception { + next.run(); + for (BiConsumer callback : workerStartCallbacks) { + callback.accept(taskQueue, worker); + } + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) { + for (BiConsumer callback : workerShutdownCallbacks) { + callback.accept(taskQueue, worker); + } + next.run(); + } } } diff --git a/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java b/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java index 6c1fa614e6..3c0188fea6 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java @@ -106,7 +106,8 @@ default WorkerOptions.Builder configureWorker( * 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}. + * created in {@link WorkerFactory#newWorker}. This is the appropriate place for registrations + * because it is called before the worker starts polling. * *

Example: * @@ -125,6 +126,68 @@ default void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) // Default: no-op } + /** + * 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. + * + *

Example: + * + *

{@code
+   * @Override
+   * public void startWorker(String taskQueue, Worker worker, Runnable next) throws Exception {
+   *     logger.info("Starting worker for task queue: {}", taskQueue);
+   *     perWorkerResources.put(taskQueue, new ResourcePool());
+   *     next.run();
+   * }
+   * }
+ * + * @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 + */ + default void startWorker( + @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) throws Exception { + next.run(); + } + + /** + * 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}. + * + *

Example: + * + *

{@code
+   * @Override
+   * public void shutdownWorker(String taskQueue, Worker worker, Runnable next) {
+   *     logger.info("Shutting down worker for task queue: {}", taskQueue);
+   *     next.run();
+   *     ResourcePool pool = perWorkerResources.remove(taskQueue);
+   *     if (pool != null) {
+   *         pool.close();
+   *     }
+   * }
+   * }
+ * + * @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) + */ + default void shutdownWorker( + @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) { + next.run(); + } + /** * Allows the plugin to wrap worker factory startup. Called during execution phase in reverse * order (first plugin wraps all others). 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 f5db68912e..cf1843f98e 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -263,8 +263,39 @@ public synchronized void start() { /** Internal method that actually starts the workers. Called from the plugin chain. */ private void doStart() { - for (Worker worker : workers.values()) { - worker.start(); + // 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; + List reversed = new ArrayList<>(plugins); + Collections.reverse(reversed); + for (Object plugin : reversed) { + if (plugin instanceof Plugin) { + final Runnable next = startChain; + final Plugin workerPlugin = (Plugin) plugin; + 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; @@ -368,10 +399,51 @@ private void shutdownInternal(boolean interruptUserTasks) { 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); + + List reversed = new ArrayList<>(plugins); + Collections.reverse(reversed); + for (Object plugin : reversed) { + if (plugin instanceof Plugin) { + final Runnable next = shutdownChain; + final Plugin workerPlugin = (Plugin) plugin; + 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(); diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index 716401c23d..8050d13635 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -98,6 +98,16 @@ public String getName() { plugin.startWorkerFactory(null, () -> called[0] = true); assertTrue("startWorkerFactory should call next", called[0]); + // Test startWorker calls next + called[0] = false; + plugin.startWorker("test-queue", null, () -> called[0] = true); + assertTrue("startWorker should call next", called[0]); + + // Test shutdownWorker calls next + called[0] = false; + plugin.shutdownWorker("test-queue", null, () -> called[0] = true); + assertTrue("shutdownWorker should call next", called[0]); + // Test default initializeWorker is a no-op (doesn't throw) plugin.initializeWorker("test-queue", null); } @@ -169,6 +179,96 @@ public void testExecutionPhaseReverseOrder() throws Exception { order); } + @Test + public void testStartWorkerReverseOrder() throws Exception { + List order = new ArrayList<>(); + + PluginBase pluginA = createWorkerLifecycleTrackingPlugin("A", order); + PluginBase pluginB = createWorkerLifecycleTrackingPlugin("B", order); + PluginBase pluginC = createWorkerLifecycleTrackingPlugin("C", order); + + List plugins = Arrays.asList(pluginA, pluginB, pluginC); + + // Build chain in reverse (like WorkerFactory does) + Runnable chain = () -> order.add("worker-start"); + + List reversed = new ArrayList<>(plugins); + java.util.Collections.reverse(reversed); + for (Object plugin : reversed) { + if (plugin instanceof io.temporal.worker.Plugin) { + final Runnable next = chain; + final io.temporal.worker.Plugin workerPlugin = (io.temporal.worker.Plugin) plugin; + chain = + () -> { + order.add(workerPlugin.getName() + "-startWorker-before"); + try { + workerPlugin.startWorker("test-queue", null, next); + } catch (Exception e) { + throw new RuntimeException(e); + } + order.add(workerPlugin.getName() + "-startWorker-after"); + }; + } + } + + chain.run(); + + // First plugin should wrap all others + assertEquals( + Arrays.asList( + "A-startWorker-before", + "B-startWorker-before", + "C-startWorker-before", + "worker-start", + "C-startWorker-after", + "B-startWorker-after", + "A-startWorker-after"), + order); + } + + @Test + public void testShutdownWorkerReverseOrder() { + List order = new ArrayList<>(); + + PluginBase pluginA = createWorkerLifecycleTrackingPlugin("A", order); + PluginBase pluginB = createWorkerLifecycleTrackingPlugin("B", order); + PluginBase pluginC = createWorkerLifecycleTrackingPlugin("C", order); + + List plugins = Arrays.asList(pluginA, pluginB, pluginC); + + // Build chain in reverse (like WorkerFactory does) + Runnable chain = () -> order.add("worker-shutdown"); + + List reversed = new ArrayList<>(plugins); + java.util.Collections.reverse(reversed); + for (Object plugin : reversed) { + if (plugin instanceof io.temporal.worker.Plugin) { + final Runnable next = chain; + final io.temporal.worker.Plugin workerPlugin = (io.temporal.worker.Plugin) plugin; + chain = + () -> { + order.add(workerPlugin.getName() + "-shutdownWorker-before"); + workerPlugin.shutdownWorker("test-queue", null, next); + order.add(workerPlugin.getName() + "-shutdownWorker-after"); + }; + } + } + + chain.run(); + + // First plugin should wrap all others + assertEquals( + Arrays.asList( + "A-shutdownWorker-before", + "B-shutdownWorker-before", + "C-shutdownWorker-before", + "worker-shutdown", + "C-shutdownWorker-after", + "B-shutdownWorker-after", + "A-shutdownWorker-after"), + order); + } + @Test public void testPluginBaseImplementsBothInterfaces() { PluginBase plugin = new PluginBase("dual-plugin") { @@ -199,4 +299,19 @@ public void startWorkerFactory(io.temporal.worker.WorkerFactory factory, Runnabl } }; } + + private PluginBase createWorkerLifecycleTrackingPlugin(String name, List order) { + return new PluginBase(name) { + @Override + public void startWorker(String taskQueue, io.temporal.worker.Worker worker, Runnable next) { + next.run(); + } + + @Override + public void shutdownWorker( + String taskQueue, io.temporal.worker.Worker worker, Runnable next) { + next.run(); + } + }; + } } diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index cbda667fcd..cbb76d6d6b 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -236,6 +236,94 @@ public void testNullInitializeWorker() { SimplePluginBuilder.newBuilder("test").initializeWorker(null); } + @Test + public void testOnWorkerStart() throws Exception { + AtomicBoolean started = new AtomicBoolean(false); + String[] capturedTaskQueue = {null}; + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .onWorkerStart( + (taskQueue, worker) -> { + started.set(true); + capturedTaskQueue[0] = taskQueue; + }) + .build(); + + AtomicBoolean nextCalled = new AtomicBoolean(false); + ((io.temporal.worker.Plugin) plugin) + .startWorker("my-task-queue", null, () -> nextCalled.set(true)); + + assertTrue("next should be called", nextCalled.get()); + assertTrue("Callback should have been called", started.get()); + assertEquals("my-task-queue", capturedTaskQueue[0]); + } + + @Test + public void testOnWorkerShutdown() { + AtomicBoolean shutdown = new AtomicBoolean(false); + String[] capturedTaskQueue = {null}; + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .onWorkerShutdown( + (taskQueue, worker) -> { + shutdown.set(true); + capturedTaskQueue[0] = taskQueue; + }) + .build(); + + AtomicBoolean nextCalled = new AtomicBoolean(false); + ((io.temporal.worker.Plugin) plugin) + .shutdownWorker("my-task-queue", null, () -> nextCalled.set(true)); + + assertTrue("next should be called", nextCalled.get()); + assertTrue("Callback should have been called", shutdown.get()); + assertEquals("my-task-queue", capturedTaskQueue[0]); + } + + @Test + public void testMultipleOnWorkerStartCallbacks() throws Exception { + AtomicInteger callCount = new AtomicInteger(0); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .onWorkerStart((taskQueue, worker) -> callCount.incrementAndGet()) + .onWorkerStart((taskQueue, worker) -> callCount.incrementAndGet()) + .onWorkerStart((taskQueue, worker) -> callCount.incrementAndGet()) + .build(); + + ((io.temporal.worker.Plugin) plugin).startWorker("test-queue", null, () -> {}); + + assertEquals("All callbacks should be called", 3, callCount.get()); + } + + @Test + public void testMultipleOnWorkerShutdownCallbacks() { + AtomicInteger callCount = new AtomicInteger(0); + + PluginBase plugin = + SimplePluginBuilder.newBuilder("test") + .onWorkerShutdown((taskQueue, worker) -> callCount.incrementAndGet()) + .onWorkerShutdown((taskQueue, worker) -> callCount.incrementAndGet()) + .onWorkerShutdown((taskQueue, worker) -> callCount.incrementAndGet()) + .build(); + + ((io.temporal.worker.Plugin) plugin).shutdownWorker("test-queue", null, () -> {}); + + assertEquals("All callbacks should be called", 3, callCount.get()); + } + + @Test(expected = NullPointerException.class) + public void testNullOnWorkerStart() { + SimplePluginBuilder.newBuilder("test").onWorkerStart(null); + } + + @Test(expected = NullPointerException.class) + public void testNullOnWorkerShutdown() { + SimplePluginBuilder.newBuilder("test").onWorkerShutdown(null); + } + @Test(expected = NullPointerException.class) public void testNullName() { SimplePluginBuilder.newBuilder(null); From 55482cbebe2d409c25f9d359c28dc8eb24e8c980 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 08:12:10 -0500 Subject: [PATCH 09/28] rename again --- .../client/{Plugin.java => ClientPlugin.java} | 4 +-- .../client/WorkflowClientInternalImpl.java | 4 +-- .../client/WorkflowClientOptions.java | 22 ++++++------ .../java/io/temporal/common/PluginBase.java | 12 +++---- .../temporal/common/SimplePluginBuilder.java | 10 +++--- .../io/temporal/worker/WorkerFactory.java | 30 ++++++++-------- .../worker/{Plugin.java => WorkerPlugin.java} | 8 ++--- .../WorkflowClientOptionsPluginTest.java | 12 +++---- .../java/io/temporal/common/PluginTest.java | 33 ++++++++++-------- .../common/SimplePluginBuilderTest.java | 34 ++++++++++--------- .../serviceclient/WorkflowServiceStubs.java | 6 ++-- 11 files changed, 91 insertions(+), 84 deletions(-) rename temporal-sdk/src/main/java/io/temporal/client/{Plugin.java => ClientPlugin.java} (97%) rename temporal-sdk/src/main/java/io/temporal/worker/{Plugin.java => WorkerPlugin.java} (97%) diff --git a/temporal-sdk/src/main/java/io/temporal/client/Plugin.java b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java similarity index 97% rename from temporal-sdk/src/main/java/io/temporal/client/Plugin.java rename to temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java index db410f232c..717039145a 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/Plugin.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java @@ -65,11 +65,11 @@ * } * } * - * @see io.temporal.worker.Plugin + * @see io.temporal.worker.WorkerPlugin * @see PluginBase */ @Experimental -public interface Plugin extends ClientPluginCallback { +public interface ClientPlugin extends ClientPluginCallback { /** * Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended 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 f6893b1841..63bca861da 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -787,8 +787,8 @@ private static WorkflowClientOptions applyClientPluginConfiguration( WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options); for (Object plugin : plugins) { - if (plugin instanceof Plugin) { - builder = ((Plugin) plugin).configureClient(builder); + if (plugin instanceof ClientPlugin) { + builder = ((ClientPlugin) plugin).configureClient(builder); } } return builder.build(); 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 19c14e3a0f..abeb06bb99 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java @@ -140,14 +140,14 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition * Sets the plugins to use with this client. Plugins can modify client and worker configuration, * intercept connection, and wrap execution lifecycle. * - *

Each plugin should implement {@link io.temporal.client.Plugin} and/or {@link - * io.temporal.worker.Plugin}. Plugins that implement both interfaces are automatically + *

Each plugin should implement {@link io.temporal.client.ClientPlugin} and/or {@link + * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically * propagated to workers created from this client. * * @param plugins the list of plugins to use (each should implement Plugin) * @return this builder for chaining - * @see io.temporal.client.Plugin - * @see io.temporal.worker.Plugin + * @see io.temporal.client.ClientPlugin + * @see io.temporal.worker.WorkerPlugin */ @Experimental public Builder setPlugins(List plugins) { @@ -159,14 +159,14 @@ public Builder setPlugins(List plugins) { * Adds a plugin to use with this client. Plugins can modify client and worker configuration, * intercept connection, and wrap execution lifecycle. * - *

The plugin should implement {@link io.temporal.client.Plugin} and/or {@link - * io.temporal.worker.Plugin}. Plugins that implement both interfaces are automatically + *

The plugin should implement {@link io.temporal.client.ClientPlugin} and/or {@link + * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically * propagated to workers created from this client. * * @param plugin the plugin to add (should implement Plugin) * @return this builder for chaining - * @see io.temporal.client.Plugin - * @see io.temporal.worker.Plugin + * @see io.temporal.client.ClientPlugin + * @see io.temporal.worker.WorkerPlugin */ @Experimental public Builder addPlugin(Object plugin) { @@ -292,9 +292,9 @@ public QueryRejectCondition getQueryRejectCondition() { /** * Returns the list of plugins configured for this client. * - *

Each plugin implements {@link io.temporal.client.Plugin} and/or {@link - * io.temporal.worker.Plugin}. Plugins that implement both interfaces are automatically propagated - * to workers created from this client. + *

Each plugin implements {@link io.temporal.client.ClientPlugin} and/or {@link + * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically + * propagated to workers created from this client. * * @return an unmodifiable list of plugins, never null */ diff --git a/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java b/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java index e6ca55d5de..650af78da6 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java +++ b/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java @@ -20,13 +20,13 @@ package io.temporal.common; -import io.temporal.client.Plugin; +import io.temporal.client.ClientPlugin; import java.util.Objects; import javax.annotation.Nonnull; /** - * Convenience base class for plugins that implement both {@link io.temporal.client.Plugin} and - * {@link io.temporal.worker.Plugin}. All methods have default no-op implementations. + * Convenience base class for plugins that implement both {@link io.temporal.client.ClientPlugin} + * and {@link io.temporal.worker.WorkerPlugin}. All methods have default no-op implementations. * *

This is the recommended way to create plugins that need to customize both client and worker * behavior. Plugins that extend this class will automatically be propagated from the client to @@ -59,11 +59,11 @@ * } * } * - * @see io.temporal.client.Plugin - * @see io.temporal.worker.Plugin + * @see io.temporal.client.ClientPlugin + * @see io.temporal.worker.WorkerPlugin */ @Experimental -public abstract class PluginBase implements Plugin, io.temporal.worker.Plugin { +public abstract class PluginBase implements ClientPlugin, io.temporal.worker.WorkerPlugin { private final String name; diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java index 775352419f..bb7019d9a2 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java @@ -41,7 +41,7 @@ * *

This builder provides a declarative way to create plugins for common use cases without * subclassing {@link PluginBase}. The resulting plugin implements both {@link - * io.temporal.client.Plugin} and {@link io.temporal.worker.Plugin}. + * io.temporal.client.ClientPlugin} and {@link io.temporal.worker.WorkerPlugin}. * *

Example: * @@ -58,8 +58,8 @@ * } * * @see PluginBase - * @see io.temporal.client.Plugin - * @see io.temporal.worker.Plugin + * @see io.temporal.client.ClientPlugin + * @see io.temporal.worker.WorkerPlugin */ @Experimental public final class SimplePluginBuilder { @@ -258,8 +258,8 @@ public SimplePluginBuilder addContextPropagators(ContextPropagator... propagator /** * Builds the plugin with the configured settings. * - * @return a new plugin instance that implements both {@link io.temporal.client.Plugin} and {@link - * io.temporal.worker.Plugin} + * @return a new plugin instance that implements both {@link io.temporal.client.ClientPlugin} and + * {@link io.temporal.worker.WorkerPlugin} */ public PluginBase build() { return new SimplePlugin( 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 cf1843f98e..439bf36e63 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -172,8 +172,8 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) { // Go through the plugins to call plugin initializeWorker hooks (e.g. register workflows, // activities, etc.) for (Object plugin : plugins) { - if (plugin instanceof Plugin) { - ((Plugin) plugin).initializeWorker(taskQueue, worker); + if (plugin instanceof WorkerPlugin) { + ((WorkerPlugin) plugin).initializeWorker(taskQueue, worker); } } @@ -240,9 +240,9 @@ public synchronized void start() { List reversed = new ArrayList<>(plugins); Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof Plugin) { + if (plugin instanceof WorkerPlugin) { final Runnable next = startChain; - final Plugin workerPlugin = (Plugin) plugin; + final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; startChain = () -> { try { @@ -273,9 +273,9 @@ private void doStart() { List reversed = new ArrayList<>(plugins); Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof Plugin) { + if (plugin instanceof WorkerPlugin) { final Runnable next = startChain; - final Plugin workerPlugin = (Plugin) plugin; + final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; startChain = () -> { try { @@ -375,9 +375,9 @@ private void shutdownInternal(boolean interruptUserTasks) { List reversed = new ArrayList<>(plugins); Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof Plugin) { + if (plugin instanceof WorkerPlugin) { final Runnable next = shutdownChain; - final Plugin workerPlugin = (Plugin) plugin; + final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; shutdownChain = () -> { try { @@ -416,9 +416,9 @@ private void doShutdown(boolean interruptUserTasks) { List reversed = new ArrayList<>(plugins); Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof Plugin) { + if (plugin instanceof WorkerPlugin) { final Runnable next = shutdownChain; - final Plugin workerPlugin = (Plugin) plugin; + final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; shutdownChain = () -> { try { @@ -522,7 +522,7 @@ private static List extractWorkerPlugins(List clientPlugins) { List workerPlugins = new ArrayList<>(); for (Object plugin : clientPlugins) { - if (plugin instanceof Plugin) { + if (plugin instanceof WorkerPlugin) { workerPlugins.add(plugin); } } @@ -545,8 +545,8 @@ private static WorkerFactoryOptions applyPluginConfiguration( : WorkerFactoryOptions.newBuilder(options); for (Object plugin : plugins) { - if (plugin instanceof Plugin) { - builder = ((Plugin) plugin).configureWorkerFactory(builder); + if (plugin instanceof WorkerPlugin) { + builder = ((WorkerPlugin) plugin).configureWorkerFactory(builder); } } return builder.build(); @@ -566,8 +566,8 @@ private static WorkerOptions applyWorkerPluginConfiguration( options == null ? WorkerOptions.newBuilder() : WorkerOptions.newBuilder(options); for (Object plugin : plugins) { - if (plugin instanceof Plugin) { - builder = ((Plugin) plugin).configureWorker(taskQueue, builder); + if (plugin instanceof WorkerPlugin) { + builder = ((WorkerPlugin) plugin).configureWorker(taskQueue, builder); } } return builder.build(); diff --git a/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java similarity index 97% rename from temporal-sdk/src/main/java/io/temporal/worker/Plugin.java rename to temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java index 3c0188fea6..89fd56b28c 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/Plugin.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java @@ -27,8 +27,8 @@ /** * Plugin interface for customizing Temporal worker configuration and lifecycle. * - *

Plugins that also implement {@link io.temporal.client.Plugin} are automatically propagated - * from the client to workers created from that client. + *

Plugins that also implement {@link io.temporal.client.ClientPlugin} are automatically + * propagated from the client to workers created from that client. * *

Example implementation: * @@ -59,11 +59,11 @@ * } * } * - * @see io.temporal.client.Plugin + * @see io.temporal.client.ClientPlugin * @see PluginBase */ @Experimental -public interface Plugin { +public interface WorkerPlugin { /** * Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended diff --git a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java index 22a4e2b247..70ea5a521c 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java @@ -45,8 +45,8 @@ public void testSetPlugins() { List plugins = options.getPlugins(); assertEquals(2, plugins.size()); - assertEquals("plugin1", ((Plugin) plugins.get(0)).getName()); - assertEquals("plugin2", ((Plugin) plugins.get(1)).getName()); + assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); + assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); } @Test @@ -59,8 +59,8 @@ public void testAddPlugin() { List plugins = options.getPlugins(); assertEquals(2, plugins.size()); - assertEquals("plugin1", ((Plugin) plugins.get(0)).getName()); - assertEquals("plugin2", ((Plugin) plugins.get(1)).getName()); + assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); + assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); } @Test @@ -99,7 +99,7 @@ public void testToBuilder() { WorkflowClientOptions copy = original.toBuilder().build(); assertEquals(1, copy.getPlugins().size()); - assertEquals("plugin", ((Plugin) copy.getPlugins().get(0)).getName()); + assertEquals("plugin", ((ClientPlugin) copy.getPlugins().get(0)).getName()); } @Test @@ -110,7 +110,7 @@ public void testValidateAndBuildWithDefaults() { WorkflowClientOptions.newBuilder().addPlugin(plugin).validateAndBuildWithDefaults(); assertEquals(1, options.getPlugins().size()); - assertEquals("plugin", ((Plugin) options.getPlugins().get(0)).getName()); + assertEquals("plugin", ((ClientPlugin) options.getPlugins().get(0)).getName()); } @Test diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index 8050d13635..f2706e8781 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -58,8 +58,8 @@ public void testPluginBaseNullName() { @Test public void testClientPluginDefaultMethods() throws Exception { - io.temporal.client.Plugin plugin = - new io.temporal.client.Plugin() { + io.temporal.client.ClientPlugin plugin = + new io.temporal.client.ClientPlugin() { @Override public String getName() { return "test"; @@ -77,8 +77,8 @@ public String getName() { @Test public void testWorkerPluginDefaultMethods() throws Exception { - io.temporal.worker.Plugin plugin = - new io.temporal.worker.Plugin() { + io.temporal.worker.WorkerPlugin plugin = + new io.temporal.worker.WorkerPlugin() { @Override public String getName() { return "test"; @@ -125,8 +125,8 @@ public void testConfigurationPhaseOrder() { // Simulate configuration phase (forward order) WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); for (Object plugin : plugins) { - if (plugin instanceof io.temporal.client.Plugin) { - builder = ((io.temporal.client.Plugin) plugin).configureClient(builder); + if (plugin instanceof io.temporal.client.ClientPlugin) { + builder = ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); } } @@ -153,9 +153,10 @@ public void testExecutionPhaseReverseOrder() throws Exception { List reversed = new ArrayList<>(plugins); java.util.Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof io.temporal.worker.Plugin) { + if (plugin instanceof io.temporal.worker.WorkerPlugin) { final Runnable next = chain; - final io.temporal.worker.Plugin workerPlugin = (io.temporal.worker.Plugin) plugin; + final io.temporal.worker.WorkerPlugin workerPlugin = + (io.temporal.worker.WorkerPlugin) plugin; chain = () -> { order.add(workerPlugin.getName() + "-before"); @@ -195,9 +196,10 @@ public void testStartWorkerReverseOrder() throws Exception { List reversed = new ArrayList<>(plugins); java.util.Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof io.temporal.worker.Plugin) { + if (plugin instanceof io.temporal.worker.WorkerPlugin) { final Runnable next = chain; - final io.temporal.worker.Plugin workerPlugin = (io.temporal.worker.Plugin) plugin; + final io.temporal.worker.WorkerPlugin workerPlugin = + (io.temporal.worker.WorkerPlugin) plugin; chain = () -> { order.add(workerPlugin.getName() + "-startWorker-before"); @@ -242,9 +244,10 @@ public void testShutdownWorkerReverseOrder() { List reversed = new ArrayList<>(plugins); java.util.Collections.reverse(reversed); for (Object plugin : reversed) { - if (plugin instanceof io.temporal.worker.Plugin) { + if (plugin instanceof io.temporal.worker.WorkerPlugin) { final Runnable next = chain; - final io.temporal.worker.Plugin workerPlugin = (io.temporal.worker.Plugin) plugin; + final io.temporal.worker.WorkerPlugin workerPlugin = + (io.temporal.worker.WorkerPlugin) plugin; chain = () -> { order.add(workerPlugin.getName() + "-shutdownWorker-before"); @@ -276,9 +279,11 @@ public void testPluginBaseImplementsBothInterfaces() { }; assertTrue( - "PluginBase should implement client Plugin", plugin instanceof io.temporal.client.Plugin); + "PluginBase should implement client Plugin", + plugin instanceof io.temporal.client.ClientPlugin); assertTrue( - "PluginBase should implement worker Plugin", plugin instanceof io.temporal.worker.Plugin); + "PluginBase should implement worker Plugin", + plugin instanceof io.temporal.worker.WorkerPlugin); } private PluginBase createTrackingPlugin(String name, List order) { diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index cbb76d6d6b..6258f16568 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -46,9 +46,11 @@ public void testSimplePluginName() { public void testSimplePluginImplementsBothInterfaces() { PluginBase plugin = SimplePluginBuilder.newBuilder("test").build(); assertTrue( - "Should implement io.temporal.client.Plugin", plugin instanceof io.temporal.client.Plugin); + "Should implement io.temporal.client.ClientPlugin", + plugin instanceof io.temporal.client.ClientPlugin); assertTrue( - "Should implement io.temporal.worker.Plugin", plugin instanceof io.temporal.worker.Plugin); + "Should implement io.temporal.worker.WorkerPlugin", + plugin instanceof io.temporal.worker.WorkerPlugin); } @Test @@ -64,7 +66,7 @@ public void testCustomizeServiceStubs() { .build(); WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(); - ((io.temporal.client.Plugin) plugin).configureServiceStubs(builder); + ((io.temporal.client.ClientPlugin) plugin).configureServiceStubs(builder); assertTrue("Customizer should have been called", customized.get()); } @@ -83,7 +85,7 @@ public void testCustomizeClient() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.Plugin) plugin).configureClient(builder); + ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); assertTrue("Customizer should have been called", customized.get()); assertEquals("custom-identity", builder.build().getIdentity()); @@ -103,7 +105,7 @@ public void testCustomizeWorkerFactory() { .build(); WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder(); - ((io.temporal.worker.Plugin) plugin).configureWorkerFactory(builder); + ((io.temporal.worker.WorkerPlugin) plugin).configureWorkerFactory(builder); assertTrue("Customizer should have been called", customized.get()); assertEquals(100, builder.build().getWorkflowCacheSize()); @@ -123,7 +125,7 @@ public void testCustomizeWorker() { .build(); WorkerOptions.Builder builder = WorkerOptions.newBuilder(); - ((io.temporal.worker.Plugin) plugin).configureWorker("test-queue", builder); + ((io.temporal.worker.WorkerPlugin) plugin).configureWorker("test-queue", builder); assertTrue("Customizer should have been called", customized.get()); assertEquals(50, builder.build().getMaxConcurrentActivityExecutionSize()); @@ -141,7 +143,7 @@ public void testMultipleCustomizers() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.Plugin) plugin).configureClient(builder); + ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); assertEquals("All customizers should be called", 3, callCount.get()); } @@ -154,7 +156,7 @@ public void testAddWorkerInterceptors() { SimplePluginBuilder.newBuilder("test").addWorkerInterceptors(interceptor).build(); WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder(); - ((io.temporal.worker.Plugin) plugin).configureWorkerFactory(builder); + ((io.temporal.worker.WorkerPlugin) plugin).configureWorkerFactory(builder); WorkerInterceptor[] interceptors = builder.build().getWorkerInterceptors(); assertEquals(1, interceptors.length); @@ -169,7 +171,7 @@ public void testAddClientInterceptors() { SimplePluginBuilder.newBuilder("test").addClientInterceptors(interceptor).build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.Plugin) plugin).configureClient(builder); + ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); WorkflowClientInterceptor[] interceptors = builder.build().getInterceptors(); assertEquals(1, interceptors.length); @@ -186,7 +188,7 @@ public void testInterceptorsAppendToExisting() { WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder().setWorkerInterceptors(existingInterceptor); - ((io.temporal.worker.Plugin) plugin).configureWorkerFactory(builder); + ((io.temporal.worker.WorkerPlugin) plugin).configureWorkerFactory(builder); WorkerInterceptor[] interceptors = builder.build().getWorkerInterceptors(); assertEquals(2, interceptors.length); @@ -209,7 +211,7 @@ public void testInitializeWorker() { .build(); // Call initializeWorker with null worker (we're just testing the callback is invoked) - ((io.temporal.worker.Plugin) plugin).initializeWorker("my-task-queue", null); + ((io.temporal.worker.WorkerPlugin) plugin).initializeWorker("my-task-queue", null); assertTrue("Initializer should have been called", initialized.get()); assertEquals("my-task-queue", capturedTaskQueue[0]); @@ -226,7 +228,7 @@ public void testMultipleWorkerInitializers() { .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) .build(); - ((io.temporal.worker.Plugin) plugin).initializeWorker("test-queue", null); + ((io.temporal.worker.WorkerPlugin) plugin).initializeWorker("test-queue", null); assertEquals("All initializers should be called", 3, callCount.get()); } @@ -251,7 +253,7 @@ public void testOnWorkerStart() throws Exception { .build(); AtomicBoolean nextCalled = new AtomicBoolean(false); - ((io.temporal.worker.Plugin) plugin) + ((io.temporal.worker.WorkerPlugin) plugin) .startWorker("my-task-queue", null, () -> nextCalled.set(true)); assertTrue("next should be called", nextCalled.get()); @@ -274,7 +276,7 @@ public void testOnWorkerShutdown() { .build(); AtomicBoolean nextCalled = new AtomicBoolean(false); - ((io.temporal.worker.Plugin) plugin) + ((io.temporal.worker.WorkerPlugin) plugin) .shutdownWorker("my-task-queue", null, () -> nextCalled.set(true)); assertTrue("next should be called", nextCalled.get()); @@ -293,7 +295,7 @@ public void testMultipleOnWorkerStartCallbacks() throws Exception { .onWorkerStart((taskQueue, worker) -> callCount.incrementAndGet()) .build(); - ((io.temporal.worker.Plugin) plugin).startWorker("test-queue", null, () -> {}); + ((io.temporal.worker.WorkerPlugin) plugin).startWorker("test-queue", null, () -> {}); assertEquals("All callbacks should be called", 3, callCount.get()); } @@ -309,7 +311,7 @@ public void testMultipleOnWorkerShutdownCallbacks() { .onWorkerShutdown((taskQueue, worker) -> callCount.incrementAndGet()) .build(); - ((io.temporal.worker.Plugin) plugin).shutdownWorker("test-queue", null, () -> {}); + ((io.temporal.worker.WorkerPlugin) plugin).shutdownWorker("test-queue", null, () -> {}); assertEquals("All callbacks should be called", 3, callCount.get()); } diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index 18f6665d06..94874aa8dd 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -142,8 +142,8 @@ static WorkflowServiceStubs newInstance( * the creation time and happens on the first request. * * @param options stub options to use - * @param plugins list of plugins to apply (plugins implementing io.temporal.client.Plugin are - * processed) + * @param plugins list of plugins to apply (plugins implementing io.temporal.client.ClientPlugin + * are processed) * @return the workflow service stubs */ static WorkflowServiceStubs newServiceStubs( @@ -186,7 +186,7 @@ static WorkflowServiceStubs newServiceStubs( /** * Callback interface for client plugins to participate in service stubs creation. This interface - * is implemented by {@code io.temporal.client.Plugin} in the temporal-sdk module. + * is implemented by {@code io.temporal.client.ClientPlugin} in the temporal-sdk module. */ interface ClientPluginCallback { /** From 195c241538948c43a28f7d857e5ae995cad1f340 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 10:27:28 -0500 Subject: [PATCH 10/28] Improved SimplePlugin builder design --- .../java/io/temporal/client/ClientPlugin.java | 6 +- .../java/io/temporal/common/PluginBase.java | 91 ---- .../java/io/temporal/common/SimplePlugin.java | 471 ++++++++++++++++++ .../temporal/common/SimplePluginBuilder.java | 414 --------------- .../java/io/temporal/worker/WorkerPlugin.java | 6 +- .../WorkflowClientOptionsPluginTest.java | 22 +- .../java/io/temporal/common/PluginTest.java | 64 ++- .../common/SimplePluginBuilderTest.java | 70 +-- 8 files changed, 551 insertions(+), 593 deletions(-) delete mode 100644 temporal-sdk/src/main/java/io/temporal/common/PluginBase.java create mode 100644 temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java delete mode 100644 temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java index 717039145a..4992abd4cf 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java @@ -21,7 +21,7 @@ package io.temporal.client; import io.temporal.common.Experimental; -import io.temporal.common.PluginBase; +import io.temporal.common.SimplePlugin; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubs.ClientPluginCallback; import io.temporal.serviceclient.WorkflowServiceStubsOptions; @@ -41,7 +41,7 @@ *

Example implementation: * *

{@code
- * public class LoggingPlugin extends PluginBase {
+ * public class LoggingPlugin extends SimplePlugin {
  *     public LoggingPlugin() {
  *         super("my-org.logging");
  *     }
@@ -66,7 +66,7 @@
  * }
* * @see io.temporal.worker.WorkerPlugin - * @see PluginBase + * @see SimplePlugin */ @Experimental public interface ClientPlugin extends ClientPluginCallback { diff --git a/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java b/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java deleted file mode 100644 index 650af78da6..0000000000 --- a/temporal-sdk/src/main/java/io/temporal/common/PluginBase.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Convenience base class for plugins that implement both {@link io.temporal.client.ClientPlugin} - * and {@link io.temporal.worker.WorkerPlugin}. All methods have default no-op implementations. - * - *

This is the recommended way to create plugins that need to customize both client and worker - * behavior. Plugins that extend this class will automatically be propagated from the client to - * workers. - * - *

Example: - * - *

{@code
- * public class TracingPlugin extends PluginBase {
- *     private final Tracer tracer;
- *
- *     public TracingPlugin(Tracer tracer) {
- *         super("io.temporal.tracing");
- *         this.tracer = tracer;
- *     }
- *
- *     @Override
- *     public WorkflowClientOptions.Builder configureClient(
- *             WorkflowClientOptions.Builder builder) {
- *         // Add tracing interceptor to client
- *         return builder.setInterceptors(new TracingClientInterceptor(tracer));
- *     }
- *
- *     @Override
- *     public WorkerFactoryOptions.Builder configureWorkerFactory(
- *             WorkerFactoryOptions.Builder builder) {
- *         // Add tracing interceptor to workers
- *         return builder.setWorkerInterceptors(new TracingWorkerInterceptor(tracer));
- *     }
- * }
- * }
- * - * @see io.temporal.client.ClientPlugin - * @see io.temporal.worker.WorkerPlugin - */ -@Experimental -public abstract class PluginBase implements ClientPlugin, io.temporal.worker.WorkerPlugin { - - private final String name; - - /** - * Creates a new plugin with the specified name. - * - * @param name a unique name for this plugin, used for logging and duplicate detection. - * Recommended format: "organization.plugin-name" (e.g., "io.temporal.tracing") - * @throws NullPointerException if name is null - */ - protected PluginBase(@Nonnull String name) { - this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); - } - - @Override - @Nonnull - public String getName() { - return name; - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{name='" + name + "'}"; - } -} 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 0000000000..6c43b7889d --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -0,0 +1,471 @@ +/* + * 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.WorkflowServiceStubsOptions; +import io.temporal.worker.Worker; +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 javax.annotation.Nonnull; + +/** + * A plugin that implements both {@link ClientPlugin} and {@link WorkerPlugin}. This class can be + * used in two ways: + * + *
    + *
  1. Builder pattern: Use {@link #newBuilder(String)} to declaratively configure a plugin + * with callbacks + *
  2. Subclassing: Extend this class and override specific methods for custom behavior + *
+ * + *

Builder Pattern Example

+ * + *
{@code
+ * SimplePlugin myPlugin = SimplePlugin.newBuilder("my-plugin")
+ *     .addWorkerInterceptors(new TracingInterceptor())
+ *     .addClientInterceptors(new LoggingInterceptor())
+ *     .customizeClient(b -> b.setIdentity("custom-identity"))
+ *     .build();
+ * }
+ * + *

Subclassing Example

+ * + *
{@code
+ * public class TracingPlugin extends SimplePlugin {
+ *     private final Tracer tracer;
+ *
+ *     public TracingPlugin(Tracer tracer) {
+ *         super("io.temporal.tracing");
+ *         this.tracer = tracer;
+ *     }
+ *
+ *     @Override
+ *     public WorkflowClientOptions.Builder configureClient(
+ *             WorkflowClientOptions.Builder builder) {
+ *         return builder.setInterceptors(new TracingClientInterceptor(tracer));
+ *     }
+ * }
+ * }
+ * + *

Hybrid Example (Builder + Override)

+ * + *
{@code
+ * public class HybridPlugin extends SimplePlugin {
+ *     public HybridPlugin() {
+ *         super(SimplePlugin.newBuilder("hybrid")
+ *             .addClientInterceptors(new LoggingInterceptor()));
+ *     }
+ *
+ *     @Override
+ *     public void initializeWorker(String taskQueue, Worker worker) {
+ *         worker.registerWorkflowImplementationTypes(MyWorkflow.class);
+ *     }
+ * }
+ * }
+ * + * @see ClientPlugin + * @see WorkerPlugin + */ +@Experimental +public class SimplePlugin implements ClientPlugin, WorkerPlugin { + + private final String name; + private final List> stubsCustomizers; + private final List> clientCustomizers; + private final List> factoryCustomizers; + private final List> workerCustomizers; + private final List> workerInitializers; + private final List> workerStartCallbacks; + private final List> workerShutdownCallbacks; + private final List workerInterceptors; + private final List clientInterceptors; + private final List contextPropagators; + + /** + * Creates a new plugin with the specified name. Use this constructor when subclassing to override + * specific methods. + * + * @param name a unique name for this plugin, used for logging and duplicate detection. + * Recommended format: "organization.plugin-name" (e.g., "io.temporal.tracing") + * @throws NullPointerException if name is null + */ + protected SimplePlugin(@Nonnull String name) { + this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); + this.stubsCustomizers = Collections.emptyList(); + this.clientCustomizers = Collections.emptyList(); + this.factoryCustomizers = Collections.emptyList(); + this.workerCustomizers = Collections.emptyList(); + this.workerInitializers = Collections.emptyList(); + this.workerStartCallbacks = Collections.emptyList(); + this.workerShutdownCallbacks = Collections.emptyList(); + this.workerInterceptors = Collections.emptyList(); + this.clientInterceptors = Collections.emptyList(); + this.contextPropagators = Collections.emptyList(); + } + + /** + * Creates a new plugin from a builder. Use this constructor when subclassing to combine builder + * configuration with method overrides. + * + * @param builder the builder with configuration + * @throws NullPointerException if builder is null + */ + protected SimplePlugin(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "Builder cannot be null"); + this.name = builder.name; + this.stubsCustomizers = new ArrayList<>(builder.stubsCustomizers); + this.clientCustomizers = new ArrayList<>(builder.clientCustomizers); + this.factoryCustomizers = new ArrayList<>(builder.factoryCustomizers); + this.workerCustomizers = new ArrayList<>(builder.workerCustomizers); + this.workerInitializers = new ArrayList<>(builder.workerInitializers); + this.workerStartCallbacks = new ArrayList<>(builder.workerStartCallbacks); + this.workerShutdownCallbacks = new ArrayList<>(builder.workerShutdownCallbacks); + this.workerInterceptors = new ArrayList<>(builder.workerInterceptors); + this.clientInterceptors = new ArrayList<>(builder.clientInterceptors); + this.contextPropagators = new ArrayList<>(builder.contextPropagators); + } + + /** + * Creates a new builder with the specified plugin name. + * + * @param name a unique name for the plugin, used for logging and duplicate detection. Recommended + * format: "organization.plugin-name" (e.g., "my-org.tracing") + * @return a new builder instance + */ + public static Builder newBuilder(@Nonnull String name) { + return new Builder(name); + } + + @Override + @Nonnull + public String getName() { + return name; + } + + @Override + @Nonnull + public WorkflowServiceStubsOptions.Builder configureServiceStubs( + @Nonnull WorkflowServiceStubsOptions.Builder builder) { + for (Consumer customizer : stubsCustomizers) { + customizer.accept(builder); + } + return builder; + } + + @Override + @Nonnull + public WorkflowClientOptions.Builder configureClient( + @Nonnull WorkflowClientOptions.Builder builder) { + // Apply customizers + for (Consumer customizer : clientCustomizers) { + customizer.accept(builder); + } + + // Add client interceptors + if (!clientInterceptors.isEmpty()) { + WorkflowClientInterceptor[] existing = builder.build().getInterceptors(); + List combined = + new ArrayList<>(existing != null ? Arrays.asList(existing) : new ArrayList<>()); + combined.addAll(clientInterceptors); + builder.setInterceptors(combined.toArray(new WorkflowClientInterceptor[0])); + } + + // Add context propagators + if (!contextPropagators.isEmpty()) { + List existing = builder.build().getContextPropagators(); + List combined = + new ArrayList<>(existing != null ? existing : new ArrayList<>()); + combined.addAll(contextPropagators); + builder.setContextPropagators(combined); + } + + return builder; + } + + @Override + @Nonnull + public WorkerFactoryOptions.Builder configureWorkerFactory( + @Nonnull WorkerFactoryOptions.Builder builder) { + // Apply customizers + for (Consumer customizer : factoryCustomizers) { + customizer.accept(builder); + } + + // Add worker interceptors + if (!workerInterceptors.isEmpty()) { + WorkerInterceptor[] existing = builder.build().getWorkerInterceptors(); + List combined = + new ArrayList<>(existing != null ? Arrays.asList(existing) : new ArrayList<>()); + combined.addAll(workerInterceptors); + builder.setWorkerInterceptors(combined.toArray(new WorkerInterceptor[0])); + } + + return builder; + } + + @Override + @Nonnull + public WorkerOptions.Builder configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { + for (Consumer customizer : workerCustomizers) { + customizer.accept(builder); + } + return builder; + } + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { + for (BiConsumer initializer : workerInitializers) { + initializer.accept(taskQueue, worker); + } + } + + @Override + public void startWorker(@Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) + throws Exception { + next.run(); + for (BiConsumer callback : workerStartCallbacks) { + callback.accept(taskQueue, worker); + } + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) { + for (BiConsumer callback : workerShutdownCallbacks) { + callback.accept(taskQueue, worker); + } + next.run(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{name='" + name + "'}"; + } + + /** Builder for creating {@link SimplePlugin} instances with declarative configuration. */ + public static final class Builder { + + private final String name; + private final List> stubsCustomizers = + new ArrayList<>(); + private final List> clientCustomizers = + new ArrayList<>(); + private final List> factoryCustomizers = + new ArrayList<>(); + private final List> workerCustomizers = new ArrayList<>(); + private final List> workerInitializers = new ArrayList<>(); + private final List> workerStartCallbacks = new ArrayList<>(); + private final List> workerShutdownCallbacks = new ArrayList<>(); + private final List workerInterceptors = new ArrayList<>(); + private final List clientInterceptors = new ArrayList<>(); + private final List contextPropagators = new ArrayList<>(); + + private Builder(@Nonnull String name) { + this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); + } + + /** + * Adds a customizer for {@link WorkflowServiceStubsOptions}. Multiple customizers are applied + * in the order they are added. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public Builder customizeServiceStubs( + @Nonnull Consumer customizer) { + stubsCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds a customizer for {@link WorkflowClientOptions}. Multiple customizers are applied in the + * order they are added. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public Builder customizeClient(@Nonnull Consumer customizer) { + clientCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds a customizer for {@link WorkerFactoryOptions}. Multiple customizers are applied in the + * order they are added. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public Builder customizeWorkerFactory( + @Nonnull Consumer customizer) { + factoryCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds a customizer for {@link WorkerOptions}. Multiple customizers are applied in the order + * they are added. The customizer is applied to all workers created by the factory. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public Builder customizeWorker(@Nonnull Consumer customizer) { + workerCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds an initializer that is called after a worker is created. This can be used to register + * workflows, activities, and Nexus services on the worker. + * + *

Example: + * + *

{@code
+     * SimplePlugin.newBuilder("my-plugin")
+     *     .initializeWorker((taskQueue, worker) -> {
+     *         worker.registerWorkflowImplementationTypes(MyWorkflow.class);
+     *         worker.registerActivitiesImplementations(new MyActivityImpl());
+     *     })
+     *     .build();
+     * }
+ * + * @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. + * + *

Example: + * + *

{@code
+     * SimplePlugin.newBuilder("my-plugin")
+     *     .onWorkerStart((taskQueue, worker) -> {
+     *         logger.info("Worker started for task queue: {}", taskQueue);
+     *         perWorkerResources.put(taskQueue, new ResourcePool());
+     *     })
+     *     .build();
+     * }
+ * + * @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}. + * + *

Example: + * + *

{@code
+     * SimplePlugin.newBuilder("my-plugin")
+     *     .onWorkerShutdown((taskQueue, worker) -> {
+     *         logger.info("Worker shutting down for task queue: {}", taskQueue);
+     *         ResourcePool pool = perWorkerResources.remove(taskQueue);
+     *         if (pool != null) {
+     *             pool.close();
+     *         }
+     *     })
+     *     .build();
+     * }
+ * + * @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 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/common/SimplePluginBuilder.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java deleted file mode 100644 index bb7019d9a2..0000000000 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePluginBuilder.java +++ /dev/null @@ -1,414 +0,0 @@ -/* - * 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.WorkflowClientOptions; -import io.temporal.common.context.ContextPropagator; -import io.temporal.common.interceptors.WorkerInterceptor; -import io.temporal.common.interceptors.WorkflowClientInterceptor; -import io.temporal.serviceclient.WorkflowServiceStubsOptions; -import io.temporal.worker.Worker; -import io.temporal.worker.WorkerFactoryOptions; -import io.temporal.worker.WorkerOptions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import javax.annotation.Nonnull; - -/** - * Builder for creating simple plugins that only need to modify configuration. - * - *

This builder provides a declarative way to create plugins for common use cases without - * subclassing {@link PluginBase}. The resulting plugin implements both {@link - * io.temporal.client.ClientPlugin} and {@link io.temporal.worker.WorkerPlugin}. - * - *

Example: - * - *

{@code
- * PluginBase myPlugin = SimplePluginBuilder.newBuilder("my-plugin")
- *     .addWorkerInterceptors(new TracingInterceptor())
- *     .addClientInterceptors(new LoggingInterceptor())
- *     .customizeClient(b -> b.setIdentity("custom-identity"))
- *     .build();
- *
- * WorkflowClientOptions options = WorkflowClientOptions.newBuilder()
- *     .addPlugin(myPlugin)
- *     .build();
- * }
- * - * @see PluginBase - * @see io.temporal.client.ClientPlugin - * @see io.temporal.worker.WorkerPlugin - */ -@Experimental -public final class SimplePluginBuilder { - - private final String name; - private final List> stubsCustomizers = - new ArrayList<>(); - private final List> clientCustomizers = new ArrayList<>(); - private final List> factoryCustomizers = new ArrayList<>(); - private final List> workerCustomizers = new ArrayList<>(); - private final List> workerInitializers = new ArrayList<>(); - private final List> workerStartCallbacks = new ArrayList<>(); - private final List> workerShutdownCallbacks = new ArrayList<>(); - private final List workerInterceptors = new ArrayList<>(); - private final List clientInterceptors = new ArrayList<>(); - private final List contextPropagators = new ArrayList<>(); - - private SimplePluginBuilder(@Nonnull String name) { - this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); - } - - /** - * Creates a new builder with the specified plugin name. - * - * @param name a unique name for the plugin, used for logging and duplicate detection. Recommended - * format: "organization.plugin-name" (e.g., "my-org.tracing") - * @return a new builder instance - */ - public static SimplePluginBuilder newBuilder(@Nonnull String name) { - return new SimplePluginBuilder(name); - } - - /** - * Adds a customizer for {@link WorkflowServiceStubsOptions}. Multiple customizers are applied in - * the order they are added. - * - * @param customizer a consumer that modifies the options builder - * @return this builder for chaining - */ - public SimplePluginBuilder customizeServiceStubs( - @Nonnull Consumer customizer) { - stubsCustomizers.add(Objects.requireNonNull(customizer)); - return this; - } - - /** - * Adds a customizer for {@link WorkflowClientOptions}. Multiple customizers are applied in the - * order they are added. - * - * @param customizer a consumer that modifies the options builder - * @return this builder for chaining - */ - public SimplePluginBuilder customizeClient( - @Nonnull Consumer customizer) { - clientCustomizers.add(Objects.requireNonNull(customizer)); - return this; - } - - /** - * Adds a customizer for {@link WorkerFactoryOptions}. Multiple customizers are applied in the - * order they are added. - * - * @param customizer a consumer that modifies the options builder - * @return this builder for chaining - */ - public SimplePluginBuilder customizeWorkerFactory( - @Nonnull Consumer customizer) { - factoryCustomizers.add(Objects.requireNonNull(customizer)); - return this; - } - - /** - * Adds a customizer for {@link WorkerOptions}. Multiple customizers are applied in the order they - * are added. The customizer is applied to all workers created by the factory. - * - * @param customizer a consumer that modifies the options builder - * @return this builder for chaining - */ - public SimplePluginBuilder customizeWorker(@Nonnull Consumer customizer) { - workerCustomizers.add(Objects.requireNonNull(customizer)); - return this; - } - - /** - * Adds an initializer that is called after a worker is created. This can be used to register - * workflows, activities, and Nexus services on the worker. - * - *

Example: - * - *

{@code
-   * SimplePluginBuilder.newBuilder("my-plugin")
-   *     .initializeWorker((taskQueue, worker) -> {
-   *         worker.registerWorkflowImplementationTypes(MyWorkflow.class);
-   *         worker.registerActivitiesImplementations(new MyActivityImpl());
-   *     })
-   *     .build();
-   * }
- * - * @param initializer a consumer that receives the task queue name and worker - * @return this builder for chaining - */ - public SimplePluginBuilder 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. - * - *

Example: - * - *

{@code
-   * SimplePluginBuilder.newBuilder("my-plugin")
-   *     .onWorkerStart((taskQueue, worker) -> {
-   *         logger.info("Worker started for task queue: {}", taskQueue);
-   *         perWorkerResources.put(taskQueue, new ResourcePool());
-   *     })
-   *     .build();
-   * }
- * - * @param callback a consumer that receives the task queue name and worker when the worker starts - * @return this builder for chaining - */ - public SimplePluginBuilder 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}. - * - *

Example: - * - *

{@code
-   * SimplePluginBuilder.newBuilder("my-plugin")
-   *     .onWorkerShutdown((taskQueue, worker) -> {
-   *         logger.info("Worker shutting down for task queue: {}", taskQueue);
-   *         ResourcePool pool = perWorkerResources.remove(taskQueue);
-   *         if (pool != null) {
-   *             pool.close();
-   *         }
-   *     })
-   *     .build();
-   * }
- * - * @param callback a consumer that receives the task queue name and worker when the worker shuts - * down - * @return this builder for chaining - */ - public SimplePluginBuilder onWorkerShutdown(@Nonnull BiConsumer callback) { - workerShutdownCallbacks.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 SimplePluginBuilder 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 SimplePluginBuilder 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 SimplePluginBuilder addContextPropagators(ContextPropagator... propagators) { - contextPropagators.addAll(Arrays.asList(propagators)); - return this; - } - - /** - * Builds the plugin with the configured settings. - * - * @return a new plugin instance that implements both {@link io.temporal.client.ClientPlugin} and - * {@link io.temporal.worker.WorkerPlugin} - */ - public PluginBase build() { - return new SimplePlugin( - name, - new ArrayList<>(stubsCustomizers), - new ArrayList<>(clientCustomizers), - new ArrayList<>(factoryCustomizers), - new ArrayList<>(workerCustomizers), - new ArrayList<>(workerInitializers), - new ArrayList<>(workerStartCallbacks), - new ArrayList<>(workerShutdownCallbacks), - new ArrayList<>(workerInterceptors), - new ArrayList<>(clientInterceptors), - new ArrayList<>(contextPropagators)); - } - - /** Internal implementation of the simple plugin. */ - private static final class SimplePlugin extends PluginBase { - private final List> stubsCustomizers; - private final List> clientCustomizers; - private final List> factoryCustomizers; - private final List> workerCustomizers; - private final List> workerInitializers; - private final List> workerStartCallbacks; - private final List> workerShutdownCallbacks; - private final List workerInterceptors; - private final List clientInterceptors; - private final List contextPropagators; - - SimplePlugin( - String name, - List> stubsCustomizers, - List> clientCustomizers, - List> factoryCustomizers, - List> workerCustomizers, - List> workerInitializers, - List> workerStartCallbacks, - List> workerShutdownCallbacks, - List workerInterceptors, - List clientInterceptors, - List contextPropagators) { - super(name); - this.stubsCustomizers = stubsCustomizers; - this.clientCustomizers = clientCustomizers; - this.factoryCustomizers = factoryCustomizers; - this.workerCustomizers = workerCustomizers; - this.workerInitializers = workerInitializers; - this.workerStartCallbacks = workerStartCallbacks; - this.workerShutdownCallbacks = workerShutdownCallbacks; - this.workerInterceptors = workerInterceptors; - this.clientInterceptors = clientInterceptors; - this.contextPropagators = contextPropagators; - } - - @Override - @Nonnull - public WorkflowServiceStubsOptions.Builder configureServiceStubs( - @Nonnull WorkflowServiceStubsOptions.Builder builder) { - for (Consumer customizer : stubsCustomizers) { - customizer.accept(builder); - } - return builder; - } - - @Override - @Nonnull - public WorkflowClientOptions.Builder configureClient( - @Nonnull WorkflowClientOptions.Builder builder) { - // Apply customizers - for (Consumer customizer : clientCustomizers) { - customizer.accept(builder); - } - - // Add client interceptors - if (!clientInterceptors.isEmpty()) { - WorkflowClientInterceptor[] existing = builder.build().getInterceptors(); - List combined = - new ArrayList<>(existing != null ? Arrays.asList(existing) : new ArrayList<>()); - combined.addAll(clientInterceptors); - builder.setInterceptors(combined.toArray(new WorkflowClientInterceptor[0])); - } - - // Add context propagators - if (!contextPropagators.isEmpty()) { - List existing = builder.build().getContextPropagators(); - List combined = - new ArrayList<>(existing != null ? existing : new ArrayList<>()); - combined.addAll(contextPropagators); - builder.setContextPropagators(combined); - } - - return builder; - } - - @Override - @Nonnull - public WorkerFactoryOptions.Builder configureWorkerFactory( - @Nonnull WorkerFactoryOptions.Builder builder) { - // Apply customizers - for (Consumer customizer : factoryCustomizers) { - customizer.accept(builder); - } - - // Add worker interceptors - if (!workerInterceptors.isEmpty()) { - WorkerInterceptor[] existing = builder.build().getWorkerInterceptors(); - List combined = - new ArrayList<>(existing != null ? Arrays.asList(existing) : new ArrayList<>()); - combined.addAll(workerInterceptors); - builder.setWorkerInterceptors(combined.toArray(new WorkerInterceptor[0])); - } - - return builder; - } - - @Override - @Nonnull - public WorkerOptions.Builder configureWorker( - @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { - for (Consumer customizer : workerCustomizers) { - customizer.accept(builder); - } - return builder; - } - - @Override - public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { - for (BiConsumer initializer : workerInitializers) { - initializer.accept(taskQueue, worker); - } - } - - @Override - public void startWorker( - @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) - throws Exception { - next.run(); - for (BiConsumer callback : workerStartCallbacks) { - callback.accept(taskQueue, worker); - } - } - - @Override - public void shutdownWorker( - @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) { - for (BiConsumer callback : workerShutdownCallbacks) { - callback.accept(taskQueue, worker); - } - next.run(); - } - } -} diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java index 89fd56b28c..212bd8fbd0 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java @@ -21,7 +21,7 @@ package io.temporal.worker; import io.temporal.common.Experimental; -import io.temporal.common.PluginBase; +import io.temporal.common.SimplePlugin; import javax.annotation.Nonnull; /** @@ -33,7 +33,7 @@ *

Example implementation: * *

{@code
- * public class MetricsPlugin extends PluginBase {
+ * public class MetricsPlugin extends SimplePlugin {
  *     private final MetricsRegistry registry;
  *
  *     public MetricsPlugin(MetricsRegistry registry) {
@@ -60,7 +60,7 @@
  * }
* * @see io.temporal.client.ClientPlugin - * @see PluginBase + * @see SimplePlugin */ @Experimental public interface WorkerPlugin { diff --git a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java index 70ea5a521c..942cef2c41 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java @@ -22,7 +22,7 @@ import static org.junit.Assert.*; -import io.temporal.common.PluginBase; +import io.temporal.common.SimplePlugin; import java.util.Arrays; import java.util.List; import org.junit.Test; @@ -37,8 +37,8 @@ public void testDefaultPluginsEmpty() { @Test public void testSetPlugins() { - PluginBase plugin1 = new TestPlugin("plugin1"); - PluginBase plugin2 = new TestPlugin("plugin2"); + SimplePlugin plugin1 = new TestPlugin("plugin1"); + SimplePlugin plugin2 = new TestPlugin("plugin2"); WorkflowClientOptions options = WorkflowClientOptions.newBuilder().setPlugins(Arrays.asList(plugin1, plugin2)).build(); @@ -51,8 +51,8 @@ public void testSetPlugins() { @Test public void testAddPlugin() { - PluginBase plugin1 = new TestPlugin("plugin1"); - PluginBase plugin2 = new TestPlugin("plugin2"); + SimplePlugin plugin1 = new TestPlugin("plugin1"); + SimplePlugin plugin2 = new TestPlugin("plugin2"); WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin1).addPlugin(plugin2).build(); @@ -66,7 +66,7 @@ public void testAddPlugin() { @Test @SuppressWarnings("unchecked") public void testPluginsAreImmutable() { - PluginBase plugin = new TestPlugin("plugin"); + SimplePlugin plugin = new TestPlugin("plugin"); WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); @@ -92,7 +92,7 @@ public void testAddPluginNull() { @Test public void testToBuilder() { - PluginBase plugin = new TestPlugin("plugin"); + SimplePlugin plugin = new TestPlugin("plugin"); WorkflowClientOptions original = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); @@ -104,7 +104,7 @@ public void testToBuilder() { @Test public void testValidateAndBuildWithDefaults() { - PluginBase plugin = new TestPlugin("plugin"); + SimplePlugin plugin = new TestPlugin("plugin"); WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin).validateAndBuildWithDefaults(); @@ -115,7 +115,7 @@ public void testValidateAndBuildWithDefaults() { @Test public void testEqualsWithPlugins() { - PluginBase plugin = new TestPlugin("plugin"); + SimplePlugin plugin = new TestPlugin("plugin"); WorkflowClientOptions options1 = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); @@ -127,7 +127,7 @@ public void testEqualsWithPlugins() { @Test public void testToStringWithPlugins() { - PluginBase plugin = new TestPlugin("my-plugin"); + SimplePlugin plugin = new TestPlugin("my-plugin"); WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); @@ -135,7 +135,7 @@ public void testToStringWithPlugins() { assertTrue("toString should contain plugins", str.contains("plugins")); } - private static class TestPlugin extends PluginBase { + private static class TestPlugin extends SimplePlugin { TestPlugin(String name) { super(name); } diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index f2706e8781..47bcc91cad 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -34,26 +34,20 @@ public class PluginTest { @Test - public void testPluginBaseName() { - PluginBase plugin = new PluginBase("test-plugin") { - // empty implementation - }; + public void testSimplePluginName() { + SimplePlugin plugin = new SimplePlugin("test-plugin") {}; assertEquals("test-plugin", plugin.getName()); } @Test - public void testPluginBaseToString() { - PluginBase plugin = new PluginBase("my-plugin") { - // empty implementation - }; + public void testSimplePluginToString() { + SimplePlugin plugin = new SimplePlugin("my-plugin") {}; assertTrue(plugin.toString().contains("my-plugin")); } @Test(expected = NullPointerException.class) - public void testPluginBaseNullName() { - new PluginBase(null) { - // empty implementation - }; + public void testSimplePluginNullName() { + new SimplePlugin((String) null) {}; } @Test @@ -116,9 +110,9 @@ public String getName() { public void testConfigurationPhaseOrder() { List order = new ArrayList<>(); - PluginBase pluginA = createTrackingPlugin("A", order); - PluginBase pluginB = createTrackingPlugin("B", order); - PluginBase pluginC = createTrackingPlugin("C", order); + SimplePlugin pluginA = createTrackingPlugin("A", order); + SimplePlugin pluginB = createTrackingPlugin("B", order); + SimplePlugin pluginC = createTrackingPlugin("C", order); List plugins = Arrays.asList(pluginA, pluginB, pluginC); @@ -138,9 +132,9 @@ public void testConfigurationPhaseOrder() { public void testExecutionPhaseReverseOrder() throws Exception { List order = new ArrayList<>(); - PluginBase pluginA = createExecutionTrackingPlugin("A", order); - PluginBase pluginB = createExecutionTrackingPlugin("B", order); - PluginBase pluginC = createExecutionTrackingPlugin("C", order); + SimplePlugin pluginA = createExecutionTrackingPlugin("A", order); + SimplePlugin pluginB = createExecutionTrackingPlugin("B", order); + SimplePlugin pluginC = createExecutionTrackingPlugin("C", order); List plugins = Arrays.asList(pluginA, pluginB, pluginC); @@ -184,9 +178,9 @@ public void testExecutionPhaseReverseOrder() throws Exception { public void testStartWorkerReverseOrder() throws Exception { List order = new ArrayList<>(); - PluginBase pluginA = createWorkerLifecycleTrackingPlugin("A", order); - PluginBase pluginB = createWorkerLifecycleTrackingPlugin("B", order); - PluginBase pluginC = createWorkerLifecycleTrackingPlugin("C", order); + SimplePlugin pluginA = createWorkerLifecycleTrackingPlugin("A", order); + SimplePlugin pluginB = createWorkerLifecycleTrackingPlugin("B", order); + SimplePlugin pluginC = createWorkerLifecycleTrackingPlugin("C", order); List plugins = Arrays.asList(pluginA, pluginB, pluginC); @@ -232,9 +226,9 @@ public void testStartWorkerReverseOrder() throws Exception { public void testShutdownWorkerReverseOrder() { List order = new ArrayList<>(); - PluginBase pluginA = createWorkerLifecycleTrackingPlugin("A", order); - PluginBase pluginB = createWorkerLifecycleTrackingPlugin("B", order); - PluginBase pluginC = createWorkerLifecycleTrackingPlugin("C", order); + SimplePlugin pluginA = createWorkerLifecycleTrackingPlugin("A", order); + SimplePlugin pluginB = createWorkerLifecycleTrackingPlugin("B", order); + SimplePlugin pluginC = createWorkerLifecycleTrackingPlugin("C", order); List plugins = Arrays.asList(pluginA, pluginB, pluginC); @@ -273,21 +267,19 @@ public void testShutdownWorkerReverseOrder() { } @Test - public void testPluginBaseImplementsBothInterfaces() { - PluginBase plugin = new PluginBase("dual-plugin") { - // empty implementation - }; + public void testSimplePluginImplementsBothInterfaces() { + SimplePlugin plugin = new SimplePlugin("dual-plugin") {}; assertTrue( - "PluginBase should implement client Plugin", + "SimplePlugin should implement ClientPlugin", plugin instanceof io.temporal.client.ClientPlugin); assertTrue( - "PluginBase should implement worker Plugin", + "SimplePlugin should implement WorkerPlugin", plugin instanceof io.temporal.worker.WorkerPlugin); } - private PluginBase createTrackingPlugin(String name, List order) { - return new PluginBase(name) { + private SimplePlugin createTrackingPlugin(String name, List order) { + return new SimplePlugin(name) { @Override public WorkflowClientOptions.Builder configureClient(WorkflowClientOptions.Builder builder) { order.add(name + "-config"); @@ -296,8 +288,8 @@ public WorkflowClientOptions.Builder configureClient(WorkflowClientOptions.Build }; } - private PluginBase createExecutionTrackingPlugin(String name, List order) { - return new PluginBase(name) { + private SimplePlugin createExecutionTrackingPlugin(String name, List order) { + return new SimplePlugin(name) { @Override public void startWorkerFactory(io.temporal.worker.WorkerFactory factory, Runnable next) { next.run(); @@ -305,8 +297,8 @@ public void startWorkerFactory(io.temporal.worker.WorkerFactory factory, Runnabl }; } - private PluginBase createWorkerLifecycleTrackingPlugin(String name, List order) { - return new PluginBase(name) { + private SimplePlugin createWorkerLifecycleTrackingPlugin(String name, List order) { + return new SimplePlugin(name) { @Override public void startWorker(String taskQueue, io.temporal.worker.Worker worker, Runnable next) { next.run(); diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index 6258f16568..217db4d0d8 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -38,13 +38,13 @@ public class SimplePluginBuilderTest { @Test public void testSimplePluginName() { - PluginBase plugin = SimplePluginBuilder.newBuilder("test-plugin").build(); + SimplePlugin plugin = SimplePlugin.newBuilder("test-plugin").build(); assertEquals("test-plugin", plugin.getName()); } @Test public void testSimplePluginImplementsBothInterfaces() { - PluginBase plugin = SimplePluginBuilder.newBuilder("test").build(); + SimplePlugin plugin = SimplePlugin.newBuilder("test").build(); assertTrue( "Should implement io.temporal.client.ClientPlugin", plugin instanceof io.temporal.client.ClientPlugin); @@ -57,8 +57,8 @@ public void testSimplePluginImplementsBothInterfaces() { public void testCustomizeServiceStubs() { AtomicBoolean customized = new AtomicBoolean(false); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .customizeServiceStubs( builder -> { customized.set(true); @@ -75,8 +75,8 @@ public void testCustomizeServiceStubs() { public void testCustomizeClient() { AtomicBoolean customized = new AtomicBoolean(false); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .customizeClient( builder -> { customized.set(true); @@ -95,8 +95,8 @@ public void testCustomizeClient() { public void testCustomizeWorkerFactory() { AtomicBoolean customized = new AtomicBoolean(false); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .customizeWorkerFactory( builder -> { customized.set(true); @@ -115,8 +115,8 @@ public void testCustomizeWorkerFactory() { public void testCustomizeWorker() { AtomicBoolean customized = new AtomicBoolean(false); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .customizeWorker( builder -> { customized.set(true); @@ -135,8 +135,8 @@ public void testCustomizeWorker() { public void testMultipleCustomizers() { AtomicInteger callCount = new AtomicInteger(0); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .customizeClient(builder -> callCount.incrementAndGet()) .customizeClient(builder -> callCount.incrementAndGet()) .customizeClient(builder -> callCount.incrementAndGet()) @@ -152,8 +152,8 @@ public void testMultipleCustomizers() { public void testAddWorkerInterceptors() { WorkerInterceptor interceptor = new WorkerInterceptorBase() {}; - PluginBase plugin = - SimplePluginBuilder.newBuilder("test").addWorkerInterceptors(interceptor).build(); + SimplePlugin plugin = + SimplePlugin.newBuilder("test").addWorkerInterceptors(interceptor).build(); WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder(); ((io.temporal.worker.WorkerPlugin) plugin).configureWorkerFactory(builder); @@ -167,8 +167,8 @@ public void testAddWorkerInterceptors() { public void testAddClientInterceptors() { WorkflowClientInterceptor interceptor = new WorkflowClientInterceptorBase() {}; - PluginBase plugin = - SimplePluginBuilder.newBuilder("test").addClientInterceptors(interceptor).build(); + SimplePlugin plugin = + SimplePlugin.newBuilder("test").addClientInterceptors(interceptor).build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); @@ -183,8 +183,8 @@ public void testInterceptorsAppendToExisting() { WorkerInterceptor existingInterceptor = new WorkerInterceptorBase() {}; WorkerInterceptor newInterceptor = new WorkerInterceptorBase() {}; - PluginBase plugin = - SimplePluginBuilder.newBuilder("test").addWorkerInterceptors(newInterceptor).build(); + SimplePlugin plugin = + SimplePlugin.newBuilder("test").addWorkerInterceptors(newInterceptor).build(); WorkerFactoryOptions.Builder builder = WorkerFactoryOptions.newBuilder().setWorkerInterceptors(existingInterceptor); @@ -201,8 +201,8 @@ public void testInitializeWorker() { AtomicBoolean initialized = new AtomicBoolean(false); String[] capturedTaskQueue = {null}; - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .initializeWorker( (taskQueue, worker) -> { initialized.set(true); @@ -221,8 +221,8 @@ public void testInitializeWorker() { public void testMultipleWorkerInitializers() { AtomicInteger callCount = new AtomicInteger(0); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) .initializeWorker((taskQueue, worker) -> callCount.incrementAndGet()) @@ -235,7 +235,7 @@ public void testMultipleWorkerInitializers() { @Test(expected = NullPointerException.class) public void testNullInitializeWorker() { - SimplePluginBuilder.newBuilder("test").initializeWorker(null); + SimplePlugin.newBuilder("test").initializeWorker(null); } @Test @@ -243,8 +243,8 @@ public void testOnWorkerStart() throws Exception { AtomicBoolean started = new AtomicBoolean(false); String[] capturedTaskQueue = {null}; - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .onWorkerStart( (taskQueue, worker) -> { started.set(true); @@ -266,8 +266,8 @@ public void testOnWorkerShutdown() { AtomicBoolean shutdown = new AtomicBoolean(false); String[] capturedTaskQueue = {null}; - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .onWorkerShutdown( (taskQueue, worker) -> { shutdown.set(true); @@ -288,8 +288,8 @@ public void testOnWorkerShutdown() { public void testMultipleOnWorkerStartCallbacks() throws Exception { AtomicInteger callCount = new AtomicInteger(0); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .onWorkerStart((taskQueue, worker) -> callCount.incrementAndGet()) .onWorkerStart((taskQueue, worker) -> callCount.incrementAndGet()) .onWorkerStart((taskQueue, worker) -> callCount.incrementAndGet()) @@ -304,8 +304,8 @@ public void testMultipleOnWorkerStartCallbacks() throws Exception { public void testMultipleOnWorkerShutdownCallbacks() { AtomicInteger callCount = new AtomicInteger(0); - PluginBase plugin = - SimplePluginBuilder.newBuilder("test") + SimplePlugin plugin = + SimplePlugin.newBuilder("test") .onWorkerShutdown((taskQueue, worker) -> callCount.incrementAndGet()) .onWorkerShutdown((taskQueue, worker) -> callCount.incrementAndGet()) .onWorkerShutdown((taskQueue, worker) -> callCount.incrementAndGet()) @@ -318,21 +318,21 @@ public void testMultipleOnWorkerShutdownCallbacks() { @Test(expected = NullPointerException.class) public void testNullOnWorkerStart() { - SimplePluginBuilder.newBuilder("test").onWorkerStart(null); + SimplePlugin.newBuilder("test").onWorkerStart(null); } @Test(expected = NullPointerException.class) public void testNullOnWorkerShutdown() { - SimplePluginBuilder.newBuilder("test").onWorkerShutdown(null); + SimplePlugin.newBuilder("test").onWorkerShutdown(null); } @Test(expected = NullPointerException.class) public void testNullName() { - SimplePluginBuilder.newBuilder(null); + SimplePlugin.newBuilder(null); } @Test(expected = NullPointerException.class) public void testNullCustomizer() { - SimplePluginBuilder.newBuilder("test").customizeClient(null); + SimplePlugin.newBuilder("test").customizeClient(null); } } From 26a2c0a2972e797ab28ae58e8f44bd95c8740ba0 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 11:35:00 -0500 Subject: [PATCH 11/28] Remove default implementations, and add in startWorkerFactory and shutdownWorkerFactory to SimplePlugin. --- .../java/io/temporal/client/ClientPlugin.java | 17 ++-- .../java/io/temporal/common/SimplePlugin.java | 78 +++++++++++++++++++ .../java/io/temporal/worker/WorkerPlugin.java | 37 +++------ .../java/io/temporal/common/PluginTest.java | 36 +++------ .../common/SimplePluginBuilderTest.java | 73 +++++++++++++++++ 5 files changed, 179 insertions(+), 62 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java index 4992abd4cf..e7d4c42948 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java @@ -89,10 +89,8 @@ public interface ClientPlugin extends ClientPluginCallback { */ @Override @Nonnull - default WorkflowServiceStubsOptions.Builder configureServiceStubs( - @Nonnull WorkflowServiceStubsOptions.Builder builder) { - return builder; - } + WorkflowServiceStubsOptions.Builder configureServiceStubs( + @Nonnull WorkflowServiceStubsOptions.Builder builder); /** * Allows the plugin to modify workflow client options before the client is created. Called during @@ -102,10 +100,7 @@ default WorkflowServiceStubsOptions.Builder configureServiceStubs( * @return the modified builder */ @Nonnull - default WorkflowClientOptions.Builder configureClient( - @Nonnull WorkflowClientOptions.Builder builder) { - return builder; - } + WorkflowClientOptions.Builder configureClient(@Nonnull WorkflowClientOptions.Builder builder); /** * Allows the plugin to wrap service client connection. Called during connection phase in reverse @@ -132,10 +127,8 @@ default WorkflowClientOptions.Builder configureClient( */ @Override @Nonnull - default WorkflowServiceStubs connectServiceClient( + WorkflowServiceStubs connectServiceClient( @Nonnull WorkflowServiceStubsOptions options, @Nonnull ClientPluginCallback.ServiceStubsSupplier next) - throws Exception { - return next.get(); - } + throws Exception; } diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index 6c43b7889d..7a09cee382 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -25,8 +25,10 @@ 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; @@ -108,6 +110,8 @@ public class SimplePlugin implements ClientPlugin, WorkerPlugin { private final List> workerInitializers; private final List> workerStartCallbacks; private final List> workerShutdownCallbacks; + private final List> workerFactoryStartCallbacks; + private final List> workerFactoryShutdownCallbacks; private final List workerInterceptors; private final List clientInterceptors; private final List contextPropagators; @@ -129,6 +133,8 @@ protected SimplePlugin(@Nonnull String name) { this.workerInitializers = Collections.emptyList(); this.workerStartCallbacks = Collections.emptyList(); this.workerShutdownCallbacks = Collections.emptyList(); + this.workerFactoryStartCallbacks = Collections.emptyList(); + this.workerFactoryShutdownCallbacks = Collections.emptyList(); this.workerInterceptors = Collections.emptyList(); this.clientInterceptors = Collections.emptyList(); this.contextPropagators = Collections.emptyList(); @@ -151,6 +157,8 @@ protected SimplePlugin(@Nonnull Builder builder) { this.workerInitializers = new ArrayList<>(builder.workerInitializers); this.workerStartCallbacks = new ArrayList<>(builder.workerStartCallbacks); this.workerShutdownCallbacks = new ArrayList<>(builder.workerShutdownCallbacks); + this.workerFactoryStartCallbacks = new ArrayList<>(builder.workerFactoryStartCallbacks); + this.workerFactoryShutdownCallbacks = new ArrayList<>(builder.workerFactoryShutdownCallbacks); this.workerInterceptors = new ArrayList<>(builder.workerInterceptors); this.clientInterceptors = new ArrayList<>(builder.clientInterceptors); this.contextPropagators = new ArrayList<>(builder.contextPropagators); @@ -269,6 +277,28 @@ public void shutdownWorker( next.run(); } + @Override + public WorkflowServiceStubs connectServiceClient( + WorkflowServiceStubsOptions options, ServiceStubsSupplier next) throws Exception { + return next.get(); + } + + @Override + public void startWorkerFactory(WorkerFactory factory, Runnable next) throws Exception { + next.run(); + for (Consumer callback : workerFactoryStartCallbacks) { + callback.accept(factory); + } + } + + @Override + public void shutdownWorkerFactory(WorkerFactory factory, Runnable next) throws Exception { + for (Consumer callback : workerFactoryShutdownCallbacks) { + callback.accept(factory); + } + next.run(); + } + @Override public String toString() { return getClass().getSimpleName() + "{name='" + name + "'}"; @@ -288,6 +318,8 @@ public static final class Builder { private final List> workerInitializers = new ArrayList<>(); private final List> workerStartCallbacks = new ArrayList<>(); private final List> workerShutdownCallbacks = new ArrayList<>(); + private final List> workerFactoryStartCallbacks = new ArrayList<>(); + private final List> workerFactoryShutdownCallbacks = new ArrayList<>(); private final List workerInterceptors = new ArrayList<>(); private final List clientInterceptors = new ArrayList<>(); private final List contextPropagators = new ArrayList<>(); @@ -423,6 +455,52 @@ public Builder onWorkerShutdown(@Nonnull BiConsumer 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. + * + *

Example: + * + *

{@code
+     * SimplePlugin.newBuilder("my-plugin")
+     *     .onWorkerFactoryStart(factory -> {
+     *         logger.info("Worker factory started");
+     *         globalResources.initialize();
+     *     })
+     *     .build();
+     * }
+ * + * @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. + * + *

Example: + * + *

{@code
+     * SimplePlugin.newBuilder("my-plugin")
+     *     .onWorkerFactoryShutdown(factory -> {
+     *         logger.info("Worker factory shutting down");
+     *         globalResources.cleanup();
+     *     })
+     *     .build();
+     * }
+ * + * @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. diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java index 212bd8fbd0..cb800ae669 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java @@ -82,10 +82,8 @@ public interface WorkerPlugin { * @return the modified builder */ @Nonnull - default WorkerFactoryOptions.Builder configureWorkerFactory( - @Nonnull WorkerFactoryOptions.Builder builder) { - return builder; - } + WorkerFactoryOptions.Builder configureWorkerFactory( + @Nonnull WorkerFactoryOptions.Builder builder); /** * Allows the plugin to modify worker options before a worker is created. Called during @@ -96,10 +94,8 @@ default WorkerFactoryOptions.Builder configureWorkerFactory( * @return the modified builder */ @Nonnull - default WorkerOptions.Builder configureWorker( - @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { - return builder; - } + WorkerOptions.Builder configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder); /** * Called after a worker is created, allowing plugins to register workflows, activities, Nexus @@ -122,9 +118,7 @@ default WorkerOptions.Builder configureWorker( * @param taskQueue the task queue name for the worker * @param worker the newly created worker */ - default void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { - // Default: no-op - } + void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker); /** * Allows the plugin to wrap individual worker startup. Called during execution phase in reverse @@ -151,10 +145,8 @@ default void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) * @param next runnable that starts the next in chain (eventually starts the actual worker) * @throws Exception if startup fails */ - default void startWorker( - @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) throws Exception { - next.run(); - } + 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 @@ -183,10 +175,7 @@ default void startWorker( * @param next runnable that shuts down the next in chain (eventually shuts down the actual * worker) */ - default void shutdownWorker( - @Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Runnable next) { - next.run(); - } + 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 @@ -210,10 +199,7 @@ default void shutdownWorker( * @param next runnable that starts the next in chain (eventually starts actual workers) * @throws Exception if startup fails */ - default void startWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) - throws Exception { - next.run(); - } + void startWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) throws Exception; /** * Allows the plugin to wrap worker factory shutdown. Called during shutdown phase in reverse @@ -237,7 +223,6 @@ default void startWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnabl * @param factory the worker factory being shut down * @param next runnable that shuts down the next in chain (eventually shuts down actual workers) */ - default void shutdownWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) { - next.run(); - } + void shutdownWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) + throws Exception; } diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index 47bcc91cad..b2ef3d2cbb 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -51,39 +51,22 @@ public void testSimplePluginNullName() { } @Test - public void testClientPluginDefaultMethods() throws Exception { - io.temporal.client.ClientPlugin plugin = - new io.temporal.client.ClientPlugin() { - @Override - public String getName() { - return "test"; - } - }; + public void testSimplePluginDefaultBehavior() throws Exception { + SimplePlugin plugin = new SimplePlugin("test") {}; - // Test default configureServiceStubs returns same builder + // Test configureServiceStubs returns same builder (no customizers) WorkflowServiceStubsOptions.Builder stubsBuilder = WorkflowServiceStubsOptions.newBuilder(); assertSame(stubsBuilder, plugin.configureServiceStubs(stubsBuilder)); - // Test default configureClient returns same builder + // Test configureClient returns same builder (no customizers) WorkflowClientOptions.Builder clientBuilder = WorkflowClientOptions.newBuilder(); assertSame(clientBuilder, plugin.configureClient(clientBuilder)); - } - @Test - public void testWorkerPluginDefaultMethods() throws Exception { - io.temporal.worker.WorkerPlugin plugin = - new io.temporal.worker.WorkerPlugin() { - @Override - public String getName() { - return "test"; - } - }; - - // Test default configureWorkerFactory returns same builder + // Test configureWorkerFactory returns same builder (no customizers) WorkerFactoryOptions.Builder factoryBuilder = WorkerFactoryOptions.newBuilder(); assertSame(factoryBuilder, plugin.configureWorkerFactory(factoryBuilder)); - // Test default configureWorker returns same builder + // Test configureWorker returns same builder (no customizers) WorkerOptions.Builder workerBuilder = WorkerOptions.newBuilder(); assertSame(workerBuilder, plugin.configureWorker("test-queue", workerBuilder)); @@ -102,7 +85,12 @@ public String getName() { plugin.shutdownWorker("test-queue", null, () -> called[0] = true); assertTrue("shutdownWorker should call next", called[0]); - // Test default initializeWorker is a no-op (doesn't throw) + // Test shutdownWorkerFactory calls next + called[0] = false; + plugin.shutdownWorkerFactory(null, () -> called[0] = true); + assertTrue("shutdownWorkerFactory should call next", called[0]); + + // Test initializeWorker is a no-op (doesn't throw) plugin.initializeWorker("test-queue", null); } diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index 217db4d0d8..b7e7c78b9d 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -316,6 +316,69 @@ public void testMultipleOnWorkerShutdownCallbacks() { assertEquals("All callbacks should be called", 3, callCount.get()); } + @Test + public void testOnWorkerFactoryStart() throws Exception { + AtomicBoolean started = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test").onWorkerFactoryStart(factory -> started.set(true)).build(); + + AtomicBoolean nextCalled = new AtomicBoolean(false); + ((io.temporal.worker.WorkerPlugin) plugin).startWorkerFactory(null, () -> nextCalled.set(true)); + + assertTrue("next should be called", nextCalled.get()); + assertTrue("Callback should have been called", started.get()); + } + + @Test + public void testOnWorkerFactoryShutdown() throws Exception { + AtomicBoolean shutdown = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .onWorkerFactoryShutdown(factory -> shutdown.set(true)) + .build(); + + AtomicBoolean nextCalled = new AtomicBoolean(false); + ((io.temporal.worker.WorkerPlugin) plugin) + .shutdownWorkerFactory(null, () -> nextCalled.set(true)); + + assertTrue("next should be called", nextCalled.get()); + assertTrue("Callback should have been called", shutdown.get()); + } + + @Test + public void testMultipleOnWorkerFactoryStartCallbacks() throws Exception { + AtomicInteger callCount = new AtomicInteger(0); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .onWorkerFactoryStart(factory -> callCount.incrementAndGet()) + .onWorkerFactoryStart(factory -> callCount.incrementAndGet()) + .onWorkerFactoryStart(factory -> callCount.incrementAndGet()) + .build(); + + ((io.temporal.worker.WorkerPlugin) plugin).startWorkerFactory(null, () -> {}); + + assertEquals("All callbacks should be called", 3, callCount.get()); + } + + @Test + public void testMultipleOnWorkerFactoryShutdownCallbacks() throws Exception { + AtomicInteger callCount = new AtomicInteger(0); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .onWorkerFactoryShutdown(factory -> callCount.incrementAndGet()) + .onWorkerFactoryShutdown(factory -> callCount.incrementAndGet()) + .onWorkerFactoryShutdown(factory -> callCount.incrementAndGet()) + .build(); + + ((io.temporal.worker.WorkerPlugin) plugin).shutdownWorkerFactory(null, () -> {}); + + assertEquals("All callbacks should be called", 3, callCount.get()); + } + @Test(expected = NullPointerException.class) public void testNullOnWorkerStart() { SimplePlugin.newBuilder("test").onWorkerStart(null); @@ -326,6 +389,16 @@ public void testNullOnWorkerShutdown() { SimplePlugin.newBuilder("test").onWorkerShutdown(null); } + @Test(expected = NullPointerException.class) + public void testNullOnWorkerFactoryStart() { + SimplePlugin.newBuilder("test").onWorkerFactoryStart(null); + } + + @Test(expected = NullPointerException.class) + public void testNullOnWorkerFactoryShutdown() { + SimplePlugin.newBuilder("test").onWorkerFactoryShutdown(null); + } + @Test(expected = NullPointerException.class) public void testNullName() { SimplePlugin.newBuilder(null); From 9a666e05983db4b9c114082e5f1db11f4fe799d7 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 11:52:55 -0500 Subject: [PATCH 12/28] Don't return the builders --- .../java/io/temporal/client/ClientPlugin.java | 14 +++------- .../client/WorkflowClientInternalImpl.java | 2 +- .../java/io/temporal/common/SimplePlugin.java | 27 +++++-------------- .../io/temporal/worker/WorkerFactory.java | 4 +-- .../java/io/temporal/worker/WorkerPlugin.java | 15 +++-------- .../java/io/temporal/common/PluginTest.java | 21 +++++++-------- .../serviceclient/WorkflowServiceStubs.java | 7 ++--- 7 files changed, 29 insertions(+), 61 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java index e7d4c42948..5f9fcdc1c1 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java @@ -47,10 +47,9 @@ * } * * @Override - * public WorkflowClientOptions.Builder configureClient( - * WorkflowClientOptions.Builder builder) { + * public void configureClient(WorkflowClientOptions.Builder builder) { * // Add custom interceptor - * return builder.setInterceptors(new LoggingInterceptor()); + * builder.setInterceptors(new LoggingInterceptor()); * } * * @Override @@ -85,22 +84,17 @@ public interface ClientPlugin extends ClientPluginCallback { * during configuration phase in forward (registration) order. * * @param builder the options builder to modify - * @return the modified builder (may return same instance or new builder) */ @Override - @Nonnull - WorkflowServiceStubsOptions.Builder configureServiceStubs( - @Nonnull WorkflowServiceStubsOptions.Builder builder); + 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 - * @return the modified builder */ - @Nonnull - WorkflowClientOptions.Builder configureClient(@Nonnull WorkflowClientOptions.Builder builder); + void configureClient(@Nonnull WorkflowClientOptions.Builder builder); /** * Allows the plugin to wrap service client connection. Called during connection phase in reverse 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 63bca861da..0afbeedf4d 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -788,7 +788,7 @@ private static WorkflowClientOptions applyClientPluginConfiguration( WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options); for (Object plugin : plugins) { if (plugin instanceof ClientPlugin) { - builder = ((ClientPlugin) plugin).configureClient(builder); + ((ClientPlugin) plugin).configureClient(builder); } } return builder.build(); diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index 7a09cee382..b1d7d08e56 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -73,9 +73,8 @@ * } * * @Override - * public WorkflowClientOptions.Builder configureClient( - * WorkflowClientOptions.Builder builder) { - * return builder.setInterceptors(new TracingClientInterceptor(tracer)); + * public void configureClient(WorkflowClientOptions.Builder builder) { + * builder.setInterceptors(new TracingClientInterceptor(tracer)); * } * } * } @@ -182,19 +181,14 @@ public String getName() { } @Override - @Nonnull - public WorkflowServiceStubsOptions.Builder configureServiceStubs( - @Nonnull WorkflowServiceStubsOptions.Builder builder) { + public void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder) { for (Consumer customizer : stubsCustomizers) { customizer.accept(builder); } - return builder; } @Override - @Nonnull - public WorkflowClientOptions.Builder configureClient( - @Nonnull WorkflowClientOptions.Builder builder) { + public void configureClient(@Nonnull WorkflowClientOptions.Builder builder) { // Apply customizers for (Consumer customizer : clientCustomizers) { customizer.accept(builder); @@ -217,14 +211,10 @@ public WorkflowClientOptions.Builder configureClient( combined.addAll(contextPropagators); builder.setContextPropagators(combined); } - - return builder; } @Override - @Nonnull - public WorkerFactoryOptions.Builder configureWorkerFactory( - @Nonnull WorkerFactoryOptions.Builder builder) { + public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { // Apply customizers for (Consumer customizer : factoryCustomizers) { customizer.accept(builder); @@ -238,18 +228,13 @@ public WorkerFactoryOptions.Builder configureWorkerFactory( combined.addAll(workerInterceptors); builder.setWorkerInterceptors(combined.toArray(new WorkerInterceptor[0])); } - - return builder; } @Override - @Nonnull - public WorkerOptions.Builder configureWorker( - @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { + public void configureWorker(@Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { for (Consumer customizer : workerCustomizers) { customizer.accept(builder); } - return builder; } @Override 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 439bf36e63..19a0949481 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -546,7 +546,7 @@ private static WorkerFactoryOptions applyPluginConfiguration( for (Object plugin : plugins) { if (plugin instanceof WorkerPlugin) { - builder = ((WorkerPlugin) plugin).configureWorkerFactory(builder); + ((WorkerPlugin) plugin).configureWorkerFactory(builder); } } return builder.build(); @@ -567,7 +567,7 @@ private static WorkerOptions applyWorkerPluginConfiguration( for (Object plugin : plugins) { if (plugin instanceof WorkerPlugin) { - builder = ((WorkerPlugin) plugin).configureWorker(taskQueue, builder); + ((WorkerPlugin) plugin).configureWorker(taskQueue, builder); } } return builder.build(); diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java index cb800ae669..5446645f04 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java @@ -42,9 +42,8 @@ * } * * @Override - * public WorkerFactoryOptions.Builder configureWorkerFactory( - * WorkerFactoryOptions.Builder builder) { - * return builder.setWorkerInterceptors(new MetricsWorkerInterceptor(registry)); + * public void configureWorkerFactory(WorkerFactoryOptions.Builder builder) { + * builder.setWorkerInterceptors(new MetricsWorkerInterceptor(registry)); * } * * @Override @@ -79,11 +78,8 @@ public interface WorkerPlugin { * configuration phase in forward (registration) order. * * @param builder the options builder to modify - * @return the modified builder */ - @Nonnull - WorkerFactoryOptions.Builder configureWorkerFactory( - @Nonnull WorkerFactoryOptions.Builder builder); + void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder); /** * Allows the plugin to modify worker options before a worker is created. Called during @@ -91,11 +87,8 @@ WorkerFactoryOptions.Builder configureWorkerFactory( * * @param taskQueue the task queue name for the worker being created * @param builder the options builder to modify - * @return the modified builder */ - @Nonnull - WorkerOptions.Builder configureWorker( - @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder); + void configureWorker(@Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder); /** * Called after a worker is created, allowing plugins to register workflows, activities, Nexus diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index b2ef3d2cbb..ec41acbb1e 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -54,21 +54,21 @@ public void testSimplePluginNullName() { public void testSimplePluginDefaultBehavior() throws Exception { SimplePlugin plugin = new SimplePlugin("test") {}; - // Test configureServiceStubs returns same builder (no customizers) + // Test configureServiceStubs doesn't throw (no customizers) WorkflowServiceStubsOptions.Builder stubsBuilder = WorkflowServiceStubsOptions.newBuilder(); - assertSame(stubsBuilder, plugin.configureServiceStubs(stubsBuilder)); + plugin.configureServiceStubs(stubsBuilder); - // Test configureClient returns same builder (no customizers) + // Test configureClient doesn't throw (no customizers) WorkflowClientOptions.Builder clientBuilder = WorkflowClientOptions.newBuilder(); - assertSame(clientBuilder, plugin.configureClient(clientBuilder)); + plugin.configureClient(clientBuilder); - // Test configureWorkerFactory returns same builder (no customizers) + // Test configureWorkerFactory doesn't throw (no customizers) WorkerFactoryOptions.Builder factoryBuilder = WorkerFactoryOptions.newBuilder(); - assertSame(factoryBuilder, plugin.configureWorkerFactory(factoryBuilder)); + plugin.configureWorkerFactory(factoryBuilder); - // Test configureWorker returns same builder (no customizers) + // Test configureWorker doesn't throw (no customizers) WorkerOptions.Builder workerBuilder = WorkerOptions.newBuilder(); - assertSame(workerBuilder, plugin.configureWorker("test-queue", workerBuilder)); + plugin.configureWorker("test-queue", workerBuilder); // Test startWorkerFactory calls next final boolean[] called = {false}; @@ -108,7 +108,7 @@ public void testConfigurationPhaseOrder() { WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); for (Object plugin : plugins) { if (plugin instanceof io.temporal.client.ClientPlugin) { - builder = ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); } } @@ -269,9 +269,8 @@ public void testSimplePluginImplementsBothInterfaces() { private SimplePlugin createTrackingPlugin(String name, List order) { return new SimplePlugin(name) { @Override - public WorkflowClientOptions.Builder configureClient(WorkflowClientOptions.Builder builder) { + public void configureClient(WorkflowClientOptions.Builder builder) { order.add(name + "-config"); - return builder; } }; } diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index 94874aa8dd..2262d57ef5 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -154,7 +154,7 @@ static WorkflowServiceStubs newServiceStubs( WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(options); for (Object plugin : plugins) { if (plugin instanceof ClientPluginCallback) { - builder = ((ClientPluginCallback) plugin).configureServiceStubs(builder); + ((ClientPluginCallback) plugin).configureServiceStubs(builder); } } WorkflowServiceStubsOptions finalOptions = builder.validateAndBuildWithDefaults(); @@ -193,11 +193,8 @@ interface ClientPluginCallback { * Allows the plugin to modify service stubs options before the service stubs are created. * * @param builder the options builder to modify - * @return the modified builder */ - @Nonnull - WorkflowServiceStubsOptions.Builder configureServiceStubs( - @Nonnull WorkflowServiceStubsOptions.Builder builder); + void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder); /** * Allows the plugin to wrap service client connection. From 218c79b7d647584c5f5befa6837032061e200c23 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 12:02:26 -0500 Subject: [PATCH 13/28] Remove ServiceStubsSupplier and checked exception --- .../java/io/temporal/client/ClientPlugin.java | 10 +++---- .../java/io/temporal/common/SimplePlugin.java | 3 ++- .../serviceclient/WorkflowServiceStubs.java | 27 +++++-------------- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java index 5f9fcdc1c1..3f2b6f259a 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java @@ -25,6 +25,7 @@ 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; /** @@ -55,7 +56,7 @@ * @Override * public WorkflowServiceStubs connectServiceClient( * WorkflowServiceStubsOptions options, - * ServiceStubsSupplier next) throws Exception { + * Supplier<WorkflowServiceStubs> next) { * logger.info("Connecting to Temporal at {}", options.getTarget()); * WorkflowServiceStubs stubs = next.get(); * logger.info("Connected successfully"); @@ -106,7 +107,7 @@ public interface ClientPlugin extends ClientPluginCallback { * @Override * public WorkflowServiceStubs connectServiceClient( * WorkflowServiceStubsOptions options, - * ClientPluginCallback.ServiceStubsSupplier next) throws Exception { + * Supplier next) { * logger.info("Connecting to Temporal..."); * WorkflowServiceStubs stubs = next.get(); * logger.info("Connected successfully"); @@ -117,12 +118,9 @@ public interface ClientPlugin extends ClientPluginCallback { * @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) - * @throws Exception if connection fails */ @Override @Nonnull WorkflowServiceStubs connectServiceClient( - @Nonnull WorkflowServiceStubsOptions options, - @Nonnull ClientPluginCallback.ServiceStubsSupplier next) - throws Exception; + @Nonnull WorkflowServiceStubsOptions options, @Nonnull Supplier next); } diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index b1d7d08e56..1be916f01d 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -39,6 +39,7 @@ import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Supplier; import javax.annotation.Nonnull; /** @@ -264,7 +265,7 @@ public void shutdownWorker( @Override public WorkflowServiceStubs connectServiceClient( - WorkflowServiceStubsOptions options, ServiceStubsSupplier next) throws Exception { + WorkflowServiceStubsOptions options, Supplier next) { return next.get(); } diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index 2262d57ef5..168f52a191 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -160,7 +161,7 @@ static WorkflowServiceStubs newServiceStubs( WorkflowServiceStubsOptions finalOptions = builder.validateAndBuildWithDefaults(); // Build connection chain (reverse order for proper nesting) - ClientPluginCallback.ServiceStubsSupplier connectionChain = + Supplier connectionChain = () -> WorkflowThreadMarker.protectFromWorkflowThread( new WorkflowServiceStubsImpl(null, finalOptions), WorkflowServiceStubs.class); @@ -169,19 +170,13 @@ static WorkflowServiceStubs newServiceStubs( Collections.reverse(reversed); for (Object plugin : reversed) { if (plugin instanceof ClientPluginCallback) { - final ClientPluginCallback.ServiceStubsSupplier next = connectionChain; + final Supplier next = connectionChain; final ClientPluginCallback callback = (ClientPluginCallback) plugin; connectionChain = () -> callback.connectServiceClient(finalOptions, next); } } - try { - return connectionChain.get(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Failed to create service stubs with plugins", e); - } + return connectionChain.get(); } /** @@ -200,20 +195,12 @@ interface ClientPluginCallback { * Allows the plugin to wrap service client connection. * * @param options the final options being used for connection - * @param next supplier that creates the service stubs - * @return the service stubs - * @throws Exception if connection fails + * @param next supplier that creates the service stubs (calls next plugin or actual connection) + * @return the service stubs (possibly wrapped or decorated) */ @Nonnull WorkflowServiceStubs connectServiceClient( - @Nonnull WorkflowServiceStubsOptions options, @Nonnull ServiceStubsSupplier next) - throws Exception; - - /** Functional interface for the connection chain. */ - @FunctionalInterface - interface ServiceStubsSupplier { - WorkflowServiceStubs get() throws Exception; - } + @Nonnull WorkflowServiceStubsOptions options, @Nonnull Supplier next); } WorkflowServiceStubsOptions getOptions(); From 3550c552854bc99dcfb7cd2950532d374ca32c57 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 12:09:51 -0500 Subject: [PATCH 14/28] Cleanup applyClientPluginConfiguration --- .../client/WorkflowClientInternalImpl.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 0afbeedf4d..4038508745 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -65,9 +65,10 @@ public static WorkflowClient newInstance( WorkflowClientInternalImpl( WorkflowServiceStubs workflowServiceStubs, WorkflowClientOptions options) { - // Apply plugin configuration phase (forward order) - options = applyClientPluginConfiguration(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; @@ -778,19 +779,15 @@ public NexusStartWorkflowResponse startNexus( * Applies client plugin configuration phase. Plugins are called in forward (registration) order * to modify the client options. */ - private static WorkflowClientOptions applyClientPluginConfiguration( - WorkflowClientOptions options) { - List plugins = options.getPlugins(); + private static void applyClientPluginConfiguration( + WorkflowClientOptions.Builder builder, List plugins) { if (plugins == null || plugins.isEmpty()) { - return options; + return; } - - WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options); for (Object plugin : plugins) { if (plugin instanceof ClientPlugin) { ((ClientPlugin) plugin).configureClient(builder); } } - return builder.build(); } } From 6aefa1a1fba2b2e76d486675092a0c0b358a742d Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 12:19:31 -0500 Subject: [PATCH 15/28] array instead of list --- .../client/WorkflowClientInternalImpl.java | 4 +- .../client/WorkflowClientOptions.java | 53 ++++---------- .../io/temporal/worker/WorkerFactory.java | 8 +- .../WorkflowClientOptionsPluginTest.java | 73 ++++--------------- .../serviceclient/WorkflowServiceStubs.java | 12 +-- 5 files changed, 40 insertions(+), 110 deletions(-) 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 4038508745..17a0679f8c 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -780,8 +780,8 @@ public NexusStartWorkflowResponse startNexus( * to modify the client options. */ private static void applyClientPluginConfiguration( - WorkflowClientOptions.Builder builder, List plugins) { - if (plugins == null || plugins.isEmpty()) { + WorkflowClientOptions.Builder builder, Object[] plugins) { + if (plugins == null || plugins.length == 0) { return; } for (Object plugin : plugins) { 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 abeb06bb99..c51e4e348a 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java @@ -7,7 +7,6 @@ import io.temporal.common.converter.GlobalDataConverter; import io.temporal.common.interceptors.WorkflowClientInterceptor; import java.lang.management.ManagementFactory; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -49,7 +48,7 @@ public static final class Builder { private String binaryChecksum; private List contextPropagators; private QueryRejectCondition queryRejectCondition; - private List plugins; + private Object[] plugins; private Builder() {} @@ -64,7 +63,7 @@ private Builder(WorkflowClientOptions options) { binaryChecksum = options.binaryChecksum; contextPropagators = options.contextPropagators; queryRejectCondition = options.queryRejectCondition; - plugins = options.plugins != null ? new ArrayList<>(options.plugins) : null; + plugins = options.plugins; } public Builder setNamespace(String namespace) { @@ -144,36 +143,14 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically * propagated to workers created from this client. * - * @param plugins the list of plugins to use (each should implement Plugin) + * @param plugins the plugins to use (each should implement ClientPlugin and/or WorkerPlugin) * @return this builder for chaining * @see io.temporal.client.ClientPlugin * @see io.temporal.worker.WorkerPlugin */ @Experimental - public Builder setPlugins(List plugins) { - this.plugins = plugins != null ? new ArrayList<>(plugins) : null; - return this; - } - - /** - * Adds a plugin to use with this client. Plugins can modify client and worker configuration, - * intercept connection, and wrap execution lifecycle. - * - *

The plugin should implement {@link io.temporal.client.ClientPlugin} and/or {@link - * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically - * propagated to workers created from this client. - * - * @param plugin the plugin to add (should implement Plugin) - * @return this builder for chaining - * @see io.temporal.client.ClientPlugin - * @see io.temporal.worker.WorkerPlugin - */ - @Experimental - public Builder addPlugin(Object plugin) { - if (this.plugins == null) { - this.plugins = new ArrayList<>(); - } - this.plugins.add(Objects.requireNonNull(plugin, "Plugin cannot be null")); + public Builder setPlugins(Object... plugins) { + this.plugins = Objects.requireNonNull(plugins); return this; } @@ -210,7 +187,7 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private static final List EMPTY_CONTEXT_PROPAGATORS = Collections.emptyList(); - private static final List EMPTY_PLUGINS = Collections.emptyList(); + private static final Object[] EMPTY_PLUGINS = new Object[0]; private final String namespace; @@ -226,7 +203,7 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private final QueryRejectCondition queryRejectCondition; - private final List plugins; + private final Object[] plugins; private WorkflowClientOptions( String namespace, @@ -236,7 +213,7 @@ private WorkflowClientOptions( String binaryChecksum, List contextPropagators, QueryRejectCondition queryRejectCondition, - List plugins) { + Object[] plugins) { this.namespace = namespace; this.dataConverter = dataConverter; this.interceptors = interceptors; @@ -290,17 +267,17 @@ public QueryRejectCondition getQueryRejectCondition() { } /** - * Returns the list of plugins configured for this client. + * Returns the plugins configured for this client. * *

Each plugin implements {@link io.temporal.client.ClientPlugin} and/or {@link * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically * propagated to workers created from this client. * - * @return an unmodifiable list of plugins, never null + * @return the array of plugins, never null */ @Experimental - public List getPlugins() { - return plugins != null ? Collections.unmodifiableList(plugins) : Collections.emptyList(); + public Object[] getPlugins() { + return plugins; } @Override @@ -324,7 +301,7 @@ public String toString() { + ", queryRejectCondition=" + queryRejectCondition + ", plugins=" - + plugins + + Arrays.toString(plugins) + '}'; } @@ -340,7 +317,7 @@ public boolean equals(Object o) { && com.google.common.base.Objects.equal(binaryChecksum, that.binaryChecksum) && com.google.common.base.Objects.equal(contextPropagators, that.contextPropagators) && queryRejectCondition == that.queryRejectCondition - && com.google.common.base.Objects.equal(plugins, that.plugins); + && Arrays.equals(plugins, that.plugins); } @Override @@ -353,6 +330,6 @@ public int hashCode() { binaryChecksum, contextPropagators, queryRejectCondition, - plugins); + Arrays.hashCode(plugins)); } } 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 19a0949481..35e76cd86b 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -512,11 +512,11 @@ public String toString() { } /** - * Extracts worker plugins from the client plugins list. Only plugins that implement {@link - * Plugin} are included. + * Extracts worker plugins from the client plugins array. Only plugins that implement {@link + * WorkerPlugin} are included. */ - private static List extractWorkerPlugins(List clientPlugins) { - if (clientPlugins == null || clientPlugins.isEmpty()) { + private static List extractWorkerPlugins(Object[] clientPlugins) { + if (clientPlugins == null || clientPlugins.length == 0) { return Collections.emptyList(); } diff --git a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java index 942cef2c41..221637249e 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java @@ -23,8 +23,6 @@ import static org.junit.Assert.*; import io.temporal.common.SimplePlugin; -import java.util.Arrays; -import java.util.List; import org.junit.Test; public class WorkflowClientOptionsPluginTest { @@ -32,7 +30,7 @@ public class WorkflowClientOptionsPluginTest { @Test public void testDefaultPluginsEmpty() { WorkflowClientOptions options = WorkflowClientOptions.newBuilder().build(); - assertTrue("Default plugins should be empty", options.getPlugins().isEmpty()); + assertEquals("Default plugins should be empty", 0, options.getPlugins().length); } @Test @@ -41,65 +39,24 @@ public void testSetPlugins() { SimplePlugin plugin2 = new TestPlugin("plugin2"); WorkflowClientOptions options = - WorkflowClientOptions.newBuilder().setPlugins(Arrays.asList(plugin1, plugin2)).build(); + WorkflowClientOptions.newBuilder().setPlugins(plugin1, plugin2).build(); - List plugins = options.getPlugins(); - assertEquals(2, plugins.size()); - assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); - assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); - } - - @Test - public void testAddPlugin() { - SimplePlugin plugin1 = new TestPlugin("plugin1"); - SimplePlugin plugin2 = new TestPlugin("plugin2"); - - WorkflowClientOptions options = - WorkflowClientOptions.newBuilder().addPlugin(plugin1).addPlugin(plugin2).build(); - - List plugins = options.getPlugins(); - assertEquals(2, plugins.size()); - assertEquals("plugin1", ((ClientPlugin) plugins.get(0)).getName()); - assertEquals("plugin2", ((ClientPlugin) plugins.get(1)).getName()); - } - - @Test - @SuppressWarnings("unchecked") - public void testPluginsAreImmutable() { - SimplePlugin plugin = new TestPlugin("plugin"); - - WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); - - List plugins = (List) options.getPlugins(); - try { - plugins.add(new TestPlugin("another")); - fail("Should not be able to modify plugins list"); - } catch (UnsupportedOperationException e) { - // expected - } - } - - @Test - public void testSetPluginsNull() { - WorkflowClientOptions options = WorkflowClientOptions.newBuilder().setPlugins(null).build(); - assertTrue("Null plugins should result in empty list", options.getPlugins().isEmpty()); - } - - @Test(expected = NullPointerException.class) - public void testAddPluginNull() { - WorkflowClientOptions.newBuilder().addPlugin(null); + Object[] plugins = options.getPlugins(); + assertEquals(2, plugins.length); + assertEquals("plugin1", ((ClientPlugin) plugins[0]).getName()); + assertEquals("plugin2", ((ClientPlugin) plugins[1]).getName()); } @Test public void testToBuilder() { SimplePlugin plugin = new TestPlugin("plugin"); - WorkflowClientOptions original = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + WorkflowClientOptions original = WorkflowClientOptions.newBuilder().setPlugins(plugin).build(); WorkflowClientOptions copy = original.toBuilder().build(); - assertEquals(1, copy.getPlugins().size()); - assertEquals("plugin", ((ClientPlugin) copy.getPlugins().get(0)).getName()); + assertEquals(1, copy.getPlugins().length); + assertEquals("plugin", ((ClientPlugin) copy.getPlugins()[0]).getName()); } @Test @@ -107,19 +64,19 @@ public void testValidateAndBuildWithDefaults() { SimplePlugin plugin = new TestPlugin("plugin"); WorkflowClientOptions options = - WorkflowClientOptions.newBuilder().addPlugin(plugin).validateAndBuildWithDefaults(); + WorkflowClientOptions.newBuilder().setPlugins(plugin).validateAndBuildWithDefaults(); - assertEquals(1, options.getPlugins().size()); - assertEquals("plugin", ((ClientPlugin) options.getPlugins().get(0)).getName()); + assertEquals(1, options.getPlugins().length); + assertEquals("plugin", ((ClientPlugin) options.getPlugins()[0]).getName()); } @Test public void testEqualsWithPlugins() { SimplePlugin plugin = new TestPlugin("plugin"); - WorkflowClientOptions options1 = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + WorkflowClientOptions options1 = WorkflowClientOptions.newBuilder().setPlugins(plugin).build(); - WorkflowClientOptions options2 = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + WorkflowClientOptions options2 = WorkflowClientOptions.newBuilder().setPlugins(plugin).build(); assertEquals(options1, options2); assertEquals(options1.hashCode(), options2.hashCode()); @@ -129,7 +86,7 @@ public void testEqualsWithPlugins() { public void testToStringWithPlugins() { SimplePlugin plugin = new TestPlugin("my-plugin"); - WorkflowClientOptions options = WorkflowClientOptions.newBuilder().addPlugin(plugin).build(); + WorkflowClientOptions options = WorkflowClientOptions.newBuilder().setPlugins(plugin).build(); String str = options.toString(); assertTrue("toString should contain plugins", str.contains("plugins")); diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index 168f52a191..f945a21b57 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -6,9 +6,6 @@ import io.temporal.internal.WorkflowThreadMarker; import io.temporal.internal.testservice.InProcessGRPCServer; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -143,12 +140,12 @@ static WorkflowServiceStubs newInstance( * the creation time and happens on the first request. * * @param options stub options to use - * @param plugins list of plugins to apply (plugins implementing io.temporal.client.ClientPlugin + * @param plugins array of plugins to apply (plugins implementing io.temporal.client.ClientPlugin * are processed) * @return the workflow service stubs */ static WorkflowServiceStubs newServiceStubs( - @Nonnull WorkflowServiceStubsOptions options, @Nonnull List plugins) { + @Nonnull WorkflowServiceStubsOptions options, @Nonnull Object[] plugins) { enforceNonWorkflowThread(); // Apply plugin configuration phase (forward order) @@ -166,9 +163,8 @@ static WorkflowServiceStubs newServiceStubs( WorkflowThreadMarker.protectFromWorkflowThread( new WorkflowServiceStubsImpl(null, finalOptions), WorkflowServiceStubs.class); - List reversed = new ArrayList<>(plugins); - Collections.reverse(reversed); - for (Object plugin : reversed) { + for (int i = plugins.length - 1; i >= 0; i--) { + Object plugin = plugins[i]; if (plugin instanceof ClientPluginCallback) { final Supplier next = connectionChain; final ClientPluginCallback callback = (ClientPluginCallback) plugin; From 861712e7a3d07c22bac2520522dead8b1af189a9 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Thu, 15 Jan 2026 14:29:35 -0500 Subject: [PATCH 16/28] Require ClientPlugin in WorkflowClientOptions --- .../client/WorkflowClientInternalImpl.java | 8 +- .../client/WorkflowClientOptions.java | 28 ++- .../io/temporal/worker/WorkerFactory.java | 175 ++++++++---------- .../temporal/worker/WorkerFactoryOptions.java | 38 ++++ .../WorkflowClientOptionsPluginTest.java | 10 +- .../serviceclient/WorkflowServiceStubs.java | 20 +- 6 files changed, 143 insertions(+), 136 deletions(-) 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 17a0679f8c..6f475520f0 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -780,14 +780,12 @@ public NexusStartWorkflowResponse startNexus( * to modify the client options. */ private static void applyClientPluginConfiguration( - WorkflowClientOptions.Builder builder, Object[] plugins) { + WorkflowClientOptions.Builder builder, ClientPlugin[] plugins) { if (plugins == null || plugins.length == 0) { return; } - for (Object plugin : plugins) { - if (plugin instanceof ClientPlugin) { - ((ClientPlugin) plugin).configureClient(builder); - } + 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 c51e4e348a..6e4a7a0673 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java @@ -48,7 +48,7 @@ public static final class Builder { private String binaryChecksum; private List contextPropagators; private QueryRejectCondition queryRejectCondition; - private Object[] plugins; + private ClientPlugin[] plugins; private Builder() {} @@ -136,20 +136,19 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition } /** - * Sets the plugins to use with this client. Plugins can modify client and worker configuration, - * intercept connection, and wrap execution lifecycle. + * Sets the plugins to use with this client. Plugins can modify client configuration, intercept + * connection, and wrap execution lifecycle. * - *

Each plugin should implement {@link io.temporal.client.ClientPlugin} and/or {@link - * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically + *

Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically * propagated to workers created from this client. * - * @param plugins the plugins to use (each should implement ClientPlugin and/or WorkerPlugin) + * @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(Object... plugins) { + public Builder setPlugins(ClientPlugin... plugins) { this.plugins = Objects.requireNonNull(plugins); return this; } @@ -187,7 +186,7 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private static final List EMPTY_CONTEXT_PROPAGATORS = Collections.emptyList(); - private static final Object[] EMPTY_PLUGINS = new Object[0]; + private static final ClientPlugin[] EMPTY_PLUGINS = new ClientPlugin[0]; private final String namespace; @@ -203,7 +202,7 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private final QueryRejectCondition queryRejectCondition; - private final Object[] plugins; + private final ClientPlugin[] plugins; private WorkflowClientOptions( String namespace, @@ -213,7 +212,7 @@ private WorkflowClientOptions( String binaryChecksum, List contextPropagators, QueryRejectCondition queryRejectCondition, - Object[] plugins) { + ClientPlugin[] plugins) { this.namespace = namespace; this.dataConverter = dataConverter; this.interceptors = interceptors; @@ -267,16 +266,15 @@ public QueryRejectCondition getQueryRejectCondition() { } /** - * Returns the plugins configured for this client. + * Returns the client plugins configured for this client. * - *

Each plugin implements {@link io.temporal.client.ClientPlugin} and/or {@link - * io.temporal.worker.WorkerPlugin}. Plugins that implement both interfaces are automatically + *

Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically * propagated to workers created from this client. * - * @return the array of plugins, never null + * @return the array of client plugins, never null */ @Experimental - public Object[] getPlugins() { + public ClientPlugin[] getPlugins() { return plugins; } 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 35e76cd86b..c9ae8b82a7 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -50,7 +50,7 @@ public final class WorkerFactory { private final @Nonnull WorkflowExecutorCache cache; /** Plugins propagated from the client and applied to this factory. */ - private final List plugins; + private final List plugins; private State state = State.Initial; @@ -171,10 +171,8 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) { // Go through the plugins to call plugin initializeWorker hooks (e.g. register workflows, // activities, etc.) - for (Object plugin : plugins) { - if (plugin instanceof WorkerPlugin) { - ((WorkerPlugin) plugin).initializeWorker(taskQueue, worker); - } + for (WorkerPlugin plugin : plugins) { + plugin.initializeWorker(taskQueue, worker); } return worker; @@ -237,24 +235,20 @@ public synchronized void start() { // Build plugin execution chain (reverse order for proper nesting) Runnable startChain = this::doStart; - List reversed = new ArrayList<>(plugins); - Collections.reverse(reversed); - for (Object plugin : reversed) { - if (plugin instanceof WorkerPlugin) { - final Runnable next = startChain; - final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; - startChain = - () -> { - try { - workerPlugin.startWorkerFactory(this, next); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException( - "Plugin " + workerPlugin.getName() + " failed during startup", e); - } - }; - } + 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 @@ -270,28 +264,24 @@ private void doStart() { // Build plugin chain for this worker (reverse order for proper nesting) Runnable startChain = worker::start; - List reversed = new ArrayList<>(plugins); - Collections.reverse(reversed); - for (Object plugin : reversed) { - if (plugin instanceof WorkerPlugin) { - final Runnable next = startChain; - final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; - 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); - } - }; - } + 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 @@ -372,23 +362,19 @@ private void shutdownInternal(boolean interruptUserTasks) { // Build plugin shutdown chain (reverse order for proper nesting) Runnable shutdownChain = () -> doShutdown(interruptUserTasks); - List reversed = new ArrayList<>(plugins); - Collections.reverse(reversed); - for (Object plugin : reversed) { - if (plugin instanceof WorkerPlugin) { - final Runnable next = shutdownChain; - final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; - 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(); - } - }; - } + 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 @@ -413,27 +399,23 @@ private void doShutdown(boolean interruptUserTasks) { Runnable shutdownChain = () -> futureHolder[0] = worker.shutdown(shutdownManager, interruptUserTasks); - List reversed = new ArrayList<>(plugins); - Collections.reverse(reversed); - for (Object plugin : reversed) { - if (plugin instanceof WorkerPlugin) { - final Runnable next = shutdownChain; - final WorkerPlugin workerPlugin = (WorkerPlugin) plugin; - 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(); - } - }; - } + 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 @@ -512,18 +494,19 @@ public String toString() { } /** - * Extracts worker plugins from the client plugins array. Only plugins that implement {@link + * Extracts worker plugins from the client plugins array. Only plugins that also implement {@link * WorkerPlugin} are included. */ - private static List extractWorkerPlugins(Object[] clientPlugins) { + private static List extractWorkerPlugins( + io.temporal.client.ClientPlugin[] clientPlugins) { if (clientPlugins == null || clientPlugins.length == 0) { return Collections.emptyList(); } - List workerPlugins = new ArrayList<>(); - for (Object plugin : clientPlugins) { + List workerPlugins = new ArrayList<>(); + for (io.temporal.client.ClientPlugin plugin : clientPlugins) { if (plugin instanceof WorkerPlugin) { - workerPlugins.add(plugin); + workerPlugins.add((WorkerPlugin) plugin); } } return Collections.unmodifiableList(workerPlugins); @@ -534,7 +517,7 @@ private static List extractWorkerPlugins(Object[] clientPlugins) { * (registration) order. */ private static WorkerFactoryOptions applyPluginConfiguration( - WorkerFactoryOptions options, List plugins) { + WorkerFactoryOptions options, List plugins) { if (plugins == null || plugins.isEmpty()) { return options; } @@ -544,10 +527,8 @@ private static WorkerFactoryOptions applyPluginConfiguration( ? WorkerFactoryOptions.newBuilder() : WorkerFactoryOptions.newBuilder(options); - for (Object plugin : plugins) { - if (plugin instanceof WorkerPlugin) { - ((WorkerPlugin) plugin).configureWorkerFactory(builder); - } + for (WorkerPlugin plugin : plugins) { + plugin.configureWorkerFactory(builder); } return builder.build(); } @@ -557,7 +538,7 @@ private static WorkerFactoryOptions applyPluginConfiguration( * order. */ private static WorkerOptions applyWorkerPluginConfiguration( - String taskQueue, WorkerOptions options, List plugins) { + String taskQueue, WorkerOptions options, List plugins) { if (plugins == null || plugins.isEmpty()) { return options; } @@ -565,10 +546,8 @@ private static WorkerOptions applyWorkerPluginConfiguration( WorkerOptions.Builder builder = options == null ? WorkerOptions.newBuilder() : WorkerOptions.newBuilder(options); - for (Object plugin : plugins) { - if (plugin instanceof WorkerPlugin) { - ((WorkerPlugin) plugin).configureWorker(taskQueue, builder); - } + for (WorkerPlugin plugin : plugins) { + plugin.configureWorker(taskQueue, builder); } return builder.build(); } 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 c50da81cd5..8fda76d20c 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/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java index 221637249e..af971355fb 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java @@ -41,10 +41,10 @@ public void testSetPlugins() { WorkflowClientOptions options = WorkflowClientOptions.newBuilder().setPlugins(plugin1, plugin2).build(); - Object[] plugins = options.getPlugins(); + ClientPlugin[] plugins = options.getPlugins(); assertEquals(2, plugins.length); - assertEquals("plugin1", ((ClientPlugin) plugins[0]).getName()); - assertEquals("plugin2", ((ClientPlugin) plugins[1]).getName()); + assertEquals("plugin1", plugins[0].getName()); + assertEquals("plugin2", plugins[1].getName()); } @Test @@ -56,7 +56,7 @@ public void testToBuilder() { WorkflowClientOptions copy = original.toBuilder().build(); assertEquals(1, copy.getPlugins().length); - assertEquals("plugin", ((ClientPlugin) copy.getPlugins()[0]).getName()); + assertEquals("plugin", copy.getPlugins()[0].getName()); } @Test @@ -67,7 +67,7 @@ public void testValidateAndBuildWithDefaults() { WorkflowClientOptions.newBuilder().setPlugins(plugin).validateAndBuildWithDefaults(); assertEquals(1, options.getPlugins().length); - assertEquals("plugin", ((ClientPlugin) options.getPlugins()[0]).getName()); + assertEquals("plugin", options.getPlugins()[0].getName()); } @Test diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index f945a21b57..7ae3077c7f 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -140,20 +140,17 @@ static WorkflowServiceStubs newInstance( * the creation time and happens on the first request. * * @param options stub options to use - * @param plugins array of plugins to apply (plugins implementing io.temporal.client.ClientPlugin - * are processed) + * @param plugins array of client plugins to apply * @return the workflow service stubs */ static WorkflowServiceStubs newServiceStubs( - @Nonnull WorkflowServiceStubsOptions options, @Nonnull Object[] plugins) { + @Nonnull WorkflowServiceStubsOptions options, @Nonnull ClientPluginCallback[] plugins) { enforceNonWorkflowThread(); // Apply plugin configuration phase (forward order) WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(options); - for (Object plugin : plugins) { - if (plugin instanceof ClientPluginCallback) { - ((ClientPluginCallback) plugin).configureServiceStubs(builder); - } + for (ClientPluginCallback plugin : plugins) { + plugin.configureServiceStubs(builder); } WorkflowServiceStubsOptions finalOptions = builder.validateAndBuildWithDefaults(); @@ -164,12 +161,9 @@ static WorkflowServiceStubs newServiceStubs( new WorkflowServiceStubsImpl(null, finalOptions), WorkflowServiceStubs.class); for (int i = plugins.length - 1; i >= 0; i--) { - Object plugin = plugins[i]; - if (plugin instanceof ClientPluginCallback) { - final Supplier next = connectionChain; - final ClientPluginCallback callback = (ClientPluginCallback) plugin; - connectionChain = () -> callback.connectServiceClient(finalOptions, next); - } + final Supplier next = connectionChain; + final ClientPluginCallback callback = plugins[i]; + connectionChain = () -> callback.connectServiceClient(finalOptions, next); } return connectionChain.get(); From a5a1ceba5cde92c5e694e2227744147ccb49ae2e Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 09:52:00 -0500 Subject: [PATCH 17/28] Seaparate out WorkflowClientPlugin and WorkflowServiceStubsPlugin --- .../java/io/temporal/client/ClientPlugin.java | 126 -------- .../client/WorkflowClientInternalImpl.java | 56 +++- .../client/WorkflowClientOptions.java | 26 +- .../temporal/client/WorkflowClientPlugin.java | 80 +++++ .../java/io/temporal/common/SimplePlugin.java | 13 +- .../io/temporal/worker/WorkerFactory.java | 55 +++- .../temporal/worker/WorkerFactoryOptions.java | 4 +- .../WorkflowClientOptionsPluginTest.java | 2 +- .../common/PluginPropagationTest.java | 280 ++++++++++++++++++ .../java/io/temporal/common/PluginTest.java | 15 +- .../common/SimplePluginBuilderTest.java | 19 +- .../serviceclient/WorkflowServiceStubs.java | 111 +++---- .../WorkflowServiceStubsOptions.java | 51 +++- .../WorkflowServiceStubsPlugin.java | 63 ++++ 14 files changed, 645 insertions(+), 256 deletions(-) delete mode 100644 temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java create mode 100644 temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java create mode 100644 temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java create mode 100644 temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java diff --git a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java deleted file mode 100644 index 3f2b6f259a..0000000000 --- a/temporal-sdk/src/main/java/io/temporal/client/ClientPlugin.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 - *
- * - *

Example implementation: - * - *

{@code
- * public class LoggingPlugin extends SimplePlugin {
- *     public LoggingPlugin() {
- *         super("my-org.logging");
- *     }
- *
- *     @Override
- *     public void configureClient(WorkflowClientOptions.Builder builder) {
- *         // Add custom interceptor
- *         builder.setInterceptors(new LoggingInterceptor());
- *     }
- *
- *     @Override
- *     public WorkflowServiceStubs connectServiceClient(
- *             WorkflowServiceStubsOptions options,
- *             Supplier<WorkflowServiceStubs> next) {
- *         logger.info("Connecting to Temporal at {}", options.getTarget());
- *         WorkflowServiceStubs stubs = next.get();
- *         logger.info("Connected successfully");
- *         return stubs;
- *     }
- * }
- * }
- * - * @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). - * - *

Example: - * - *

{@code
-   * @Override
-   * public WorkflowServiceStubs connectServiceClient(
-   *         WorkflowServiceStubsOptions options,
-   *         Supplier next) {
-   *     logger.info("Connecting to Temporal...");
-   *     WorkflowServiceStubs stubs = next.get();
-   *     logger.info("Connected successfully");
-   *     return stubs;
-   * }
-   * }
- * - * @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 6f475520f0..1d63552493 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -24,6 +24,7 @@ import io.temporal.internal.sync.StubMarker; import io.temporal.serviceclient.MetricsTag; import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.worker.WorkerFactory; import io.temporal.workflow.*; import java.lang.annotation.Annotation; @@ -65,9 +66,17 @@ public static WorkflowClient newInstance( WorkflowClientInternalImpl( WorkflowServiceStubs workflowServiceStubs, WorkflowClientOptions options) { + // Extract WorkflowClientPlugins from service stubs plugins (propagation) + WorkflowClientPlugin[] propagatedPlugins = + extractClientPlugins(workflowServiceStubs.getOptions().getPlugins()); + + // Merge propagated plugins with client-specified plugins + WorkflowClientPlugin[] mergedPlugins = mergePlugins(propagatedPlugins, options.getPlugins()); + // Apply plugin configuration phase (forward order), then validate WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options); - applyClientPluginConfiguration(builder, options.getPlugins()); + builder.setPlugins(mergedPlugins); + applyClientPluginConfiguration(builder, mergedPlugins); options = builder.validateAndBuildWithDefaults(); workflowServiceStubs = new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace()); @@ -776,16 +785,53 @@ public NexusStartWorkflowResponse startNexus( } /** - * Applies client plugin configuration phase. Plugins are called in forward (registration) order - * to modify the client options. + * Applies workflow 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) { + WorkflowClientOptions.Builder builder, WorkflowClientPlugin[] plugins) { if (plugins == null || plugins.length == 0) { return; } - for (ClientPlugin plugin : plugins) { + for (WorkflowClientPlugin plugin : plugins) { plugin.configureClient(builder); } } + + /** + * Extracts WorkflowClientPlugins from service stubs plugins. Only plugins that also implement + * {@link WorkflowClientPlugin} are included. This enables plugin propagation from service stubs + * to workflow client. + */ + private static WorkflowClientPlugin[] extractClientPlugins( + WorkflowServiceStubsPlugin[] stubsPlugins) { + if (stubsPlugins == null || stubsPlugins.length == 0) { + return new WorkflowClientPlugin[0]; + } + List clientPlugins = new ArrayList<>(); + for (WorkflowServiceStubsPlugin plugin : stubsPlugins) { + if (plugin instanceof WorkflowClientPlugin) { + clientPlugins.add((WorkflowClientPlugin) plugin); + } + } + return clientPlugins.toArray(new WorkflowClientPlugin[0]); + } + + /** + * Merges propagated plugins with explicitly specified plugins. Propagated plugins come first + * (from service stubs), followed by client-specific plugins. + */ + private static WorkflowClientPlugin[] mergePlugins( + WorkflowClientPlugin[] propagated, WorkflowClientPlugin[] explicit) { + if (propagated == null || propagated.length == 0) { + return explicit; + } + if (explicit == null || explicit.length == 0) { + return propagated; + } + WorkflowClientPlugin[] merged = new WorkflowClientPlugin[propagated.length + explicit.length]; + System.arraycopy(propagated, 0, merged, 0, propagated.length); + System.arraycopy(explicit, 0, merged, propagated.length, explicit.length); + return merged; + } } 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 6e4a7a0673..83b8af4e7d 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java @@ -48,7 +48,7 @@ public static final class Builder { private String binaryChecksum; private List contextPropagators; private QueryRejectCondition queryRejectCondition; - private ClientPlugin[] plugins; + private WorkflowClientPlugin[] plugins; private Builder() {} @@ -136,19 +136,19 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition } /** - * Sets the plugins to use with this client. Plugins can modify client configuration, intercept - * connection, and wrap execution lifecycle. + * Sets the workflow client plugins to use with this client. Plugins can modify client + * configuration. * *

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 + * @param plugins the workflow client plugins to use * @return this builder for chaining - * @see io.temporal.client.ClientPlugin + * @see WorkflowClientPlugin * @see io.temporal.worker.WorkerPlugin */ @Experimental - public Builder setPlugins(ClientPlugin... plugins) { + public Builder setPlugins(WorkflowClientPlugin... plugins) { this.plugins = Objects.requireNonNull(plugins); return this; } @@ -162,7 +162,7 @@ public WorkflowClientOptions build() { binaryChecksum, contextPropagators, queryRejectCondition, - plugins); + plugins == null ? EMPTY_PLUGINS : plugins); } public WorkflowClientOptions validateAndBuildWithDefaults() { @@ -186,7 +186,7 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private static final List EMPTY_CONTEXT_PROPAGATORS = Collections.emptyList(); - private static final ClientPlugin[] EMPTY_PLUGINS = new ClientPlugin[0]; + private static final WorkflowClientPlugin[] EMPTY_PLUGINS = new WorkflowClientPlugin[0]; private final String namespace; @@ -202,7 +202,7 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { private final QueryRejectCondition queryRejectCondition; - private final ClientPlugin[] plugins; + private final WorkflowClientPlugin[] plugins; private WorkflowClientOptions( String namespace, @@ -212,7 +212,7 @@ private WorkflowClientOptions( String binaryChecksum, List contextPropagators, QueryRejectCondition queryRejectCondition, - ClientPlugin[] plugins) { + WorkflowClientPlugin[] plugins) { this.namespace = namespace; this.dataConverter = dataConverter; this.interceptors = interceptors; @@ -266,15 +266,15 @@ public QueryRejectCondition getQueryRejectCondition() { } /** - * Returns the client plugins configured for this client. + * Returns the workflow 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 + * @return the array of workflow client plugins, never null */ @Experimental - public ClientPlugin[] getPlugins() { + public WorkflowClientPlugin[] getPlugins() { return plugins; } diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java new file mode 100644 index 0000000000..27bb00b20b --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java @@ -0,0 +1,80 @@ +/* + * 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 javax.annotation.Nonnull; + +/** + * Plugin interface for customizing Temporal workflow client configuration. + * + *

This interface is separate from {@link + * io.temporal.serviceclient.WorkflowServiceStubs.ServiceStubsPlugin} to allow plugins that only + * need to configure the workflow client without affecting the underlying gRPC connection. + * + *

Plugins that implement both {@code ServiceStubsPlugin} and {@code WorkflowClientPlugin} will + * have their service stubs configuration applied when creating the service stubs, and their client + * configuration applied when creating the workflow client. + * + *

Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically + * propagated from the client to workers created from that client. + * + *

Example implementation: + * + *

{@code
+ * public class LoggingPlugin extends SimplePlugin {
+ *     public LoggingPlugin() {
+ *         super("my-org.logging");
+ *     }
+ *
+ *     @Override
+ *     public void configureClient(WorkflowClientOptions.Builder builder) {
+ *         // Add custom interceptor
+ *         builder.setInterceptors(new LoggingInterceptor());
+ *     }
+ * }
+ * }
+ * + * @see io.temporal.serviceclient.WorkflowServiceStubs.ServiceStubsPlugin + * @see io.temporal.worker.WorkerPlugin + * @see SimplePlugin + */ +@Experimental +public interface WorkflowClientPlugin { + + /** + * 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 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); +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index 1be916f01d..0b1e38f62d 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -20,13 +20,14 @@ package io.temporal.common; -import io.temporal.client.ClientPlugin; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; 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.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.worker.Worker; import io.temporal.worker.WorkerFactory; import io.temporal.worker.WorkerFactoryOptions; @@ -43,8 +44,8 @@ import javax.annotation.Nonnull; /** - * A plugin that implements both {@link ClientPlugin} and {@link WorkerPlugin}. This class can be - * used in two ways: + * A plugin that implements {@link WorkflowServiceStubsPlugin}, {@link WorkflowClientPlugin}, and + * {@link WorkerPlugin}. This class can be used in two ways: * *
    *
  1. Builder pattern: Use {@link #newBuilder(String)} to declaratively configure a plugin @@ -96,11 +97,13 @@ * } * } * - * @see ClientPlugin + * @see WorkflowServiceStubsPlugin + * @see WorkflowClientPlugin * @see WorkerPlugin */ @Experimental -public class SimplePlugin implements ClientPlugin, WorkerPlugin { +public class SimplePlugin + implements WorkflowServiceStubsPlugin, WorkflowClientPlugin, WorkerPlugin { private final String name; private final List> stubsCustomizers; 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 c9ae8b82a7..b1fb42cd2b 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -16,6 +16,7 @@ import io.temporal.internal.worker.WorkflowRunLockManager; import io.temporal.serviceclient.MetricsTag; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -79,7 +80,13 @@ private WorkerFactory(WorkflowClient workflowClient, WorkerFactoryOptions factor String namespace = workflowClientOptions.getNamespace(); // Extract worker plugins from client (auto-propagation) - this.plugins = extractWorkerPlugins(workflowClientOptions.getPlugins()); + List propagatedPlugins = extractWorkerPlugins(workflowClientOptions.getPlugins()); + + // Get plugins explicitly set on factory options + WorkerPlugin[] explicitPlugins = factoryOptions != null ? factoryOptions.getPlugins() : null; + + // Merge propagated plugins with explicit plugins (propagated first) + this.plugins = mergePlugins(propagatedPlugins, explicitPlugins); // Apply plugin configuration to factory options (forward order) factoryOptions = applyPluginConfiguration(factoryOptions, this.plugins); @@ -494,17 +501,17 @@ public String toString() { } /** - * Extracts worker plugins from the client plugins array. Only plugins that also implement {@link - * WorkerPlugin} are included. + * Extracts worker plugins from the workflow client plugins array. Only plugins that also + * implement {@link WorkerPlugin} are included. */ private static List extractWorkerPlugins( - io.temporal.client.ClientPlugin[] clientPlugins) { + io.temporal.client.WorkflowClientPlugin[] clientPlugins) { if (clientPlugins == null || clientPlugins.length == 0) { return Collections.emptyList(); } List workerPlugins = new ArrayList<>(); - for (io.temporal.client.ClientPlugin plugin : clientPlugins) { + for (io.temporal.client.WorkflowClientPlugin plugin : clientPlugins) { if (plugin instanceof WorkerPlugin) { workerPlugins.add((WorkerPlugin) plugin); } @@ -512,23 +519,47 @@ private static List extractWorkerPlugins( return Collections.unmodifiableList(workerPlugins); } + /** + * Merges propagated plugins with explicitly specified plugins. Propagated plugins come first + * (from client), followed by factory-specific plugins. + */ + private static List mergePlugins( + List propagated, WorkerPlugin[] explicit) { + if ((propagated == null || propagated.isEmpty()) + && (explicit == null || explicit.length == 0)) { + return Collections.emptyList(); + } + if (propagated == null || propagated.isEmpty()) { + return Collections.unmodifiableList(Arrays.asList(explicit)); + } + if (explicit == null || explicit.length == 0) { + return propagated; + } + List merged = new ArrayList<>(propagated.size() + explicit.length); + merged.addAll(propagated); + merged.addAll(Arrays.asList(explicit)); + return Collections.unmodifiableList(merged); + } + /** * Applies plugin configuration to worker factory options. Plugins are called in forward - * (registration) order. + * (registration) order. The merged plugins are set on the builder so plugins can see the complete + * list if they inspect the builder. */ 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); + // Set the merged plugins on the builder so plugins see the complete list + builder.setPlugins(plugins.toArray(new WorkerPlugin[0])); + + if (plugins != null) { + for (WorkerPlugin plugin : plugins) { + plugin.configureWorkerFactory(builder); + } } return builder.build(); } 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 8fda76d20c..0c6175678e 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java @@ -104,8 +104,8 @@ public Builder setWorkerInterceptors(WorkerInterceptor... workerInterceptors) { } /** - * Sets the worker plugins to use with workers created by this factory. Plugins can modify worker - * configuration and wrap worker lifecycle. + * 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 diff --git a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java index af971355fb..d23317c024 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java @@ -41,7 +41,7 @@ public void testSetPlugins() { WorkflowClientOptions options = WorkflowClientOptions.newBuilder().setPlugins(plugin1, plugin2).build(); - ClientPlugin[] plugins = options.getPlugins(); + WorkflowClientPlugin[] plugins = options.getPlugins(); assertEquals(2, plugins.length); assertEquals("plugin1", plugins[0].getName()); assertEquals("plugin2", plugins[1].getName()); diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java new file mode 100644 index 0000000000..41aff8728b --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java @@ -0,0 +1,280 @@ +/* + * 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 static org.junit.Assert.*; + +import io.temporal.client.WorkflowClientOptions; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.testing.TestEnvironmentOptions; +import io.temporal.testing.TestWorkflowEnvironment; +import io.temporal.worker.WorkerPlugin; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +/** + * Tests that plugins propagate through the full chain: WorkflowServiceStubsOptions → + * WorkflowClientOptions → WorkerFactory + */ +public class PluginPropagationTest { + + @Test + public void testPluginPropagatesFromServiceStubsToWorkerFactory() { + List callLog = new ArrayList<>(); + + // Create a plugin that tracks all configuration calls + SimplePlugin trackingPlugin = + SimplePlugin.newBuilder("tracking-plugin") + .customizeServiceStubs( + builder -> { + callLog.add("configureServiceStubs"); + }) + .customizeClient( + builder -> { + callLog.add("configureClient"); + }) + .customizeWorkerFactory( + builder -> { + callLog.add("configureWorkerFactory"); + }) + .customizeWorker( + builder -> { + callLog.add("configureWorker"); + }) + .build(); + + // Set the plugin ONLY on WorkflowServiceStubsOptions + WorkflowServiceStubsOptions stubsOptions = + WorkflowServiceStubsOptions.newBuilder() + .setPlugins((io.temporal.serviceclient.WorkflowServiceStubsPlugin) trackingPlugin) + .build(); + + TestEnvironmentOptions testOptions = + TestEnvironmentOptions.newBuilder().setWorkflowServiceStubsOptions(stubsOptions).build(); + + // Create the test environment - this triggers the full propagation chain + TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance(testOptions); + try { + // Create a worker to trigger configureWorker + env.newWorker("test-task-queue"); + + // Verify the plugin was called at each level + assertTrue( + "configureServiceStubs should be called", callLog.contains("configureServiceStubs")); + assertTrue( + "configureClient should be called (propagated from service stubs)", + callLog.contains("configureClient")); + assertTrue( + "configureWorkerFactory should be called (propagated from client)", + callLog.contains("configureWorkerFactory")); + assertTrue( + "configureWorker should be called (propagated from client)", + callLog.contains("configureWorker")); + + // Verify the order: service stubs -> client -> worker factory -> worker + assertEquals( + "Configuration should happen in correct order", + Arrays.asList( + "configureServiceStubs", + "configureClient", + "configureWorkerFactory", + "configureWorker"), + callLog); + } finally { + env.close(); + } + } + + @Test + public void testPluginSetOnClientOnlyDoesNotAffectServiceStubs() { + List callLog = new ArrayList<>(); + + // Create a plugin that tracks all configuration calls + SimplePlugin trackingPlugin = + SimplePlugin.newBuilder("tracking-plugin") + .customizeServiceStubs( + builder -> { + callLog.add("configureServiceStubs"); + }) + .customizeClient( + builder -> { + callLog.add("configureClient"); + }) + .customizeWorkerFactory( + builder -> { + callLog.add("configureWorkerFactory"); + }) + .build(); + + // Set the plugin ONLY on WorkflowClientOptions (not service stubs) + WorkflowClientOptions clientOptions = + WorkflowClientOptions.newBuilder() + .setPlugins((io.temporal.client.WorkflowClientPlugin) trackingPlugin) + .build(); + + TestEnvironmentOptions testOptions = + TestEnvironmentOptions.newBuilder().setWorkflowClientOptions(clientOptions).build(); + + TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance(testOptions); + try { + env.newWorker("test-task-queue"); + + // configureServiceStubs should NOT be called (plugin wasn't set there) + assertFalse( + "configureServiceStubs should NOT be called", callLog.contains("configureServiceStubs")); + + // But client and worker factory should be called + assertTrue("configureClient should be called", callLog.contains("configureClient")); + assertTrue( + "configureWorkerFactory should be called", callLog.contains("configureWorkerFactory")); + } finally { + env.close(); + } + } + + @Test + public void testMergedPluginsFromBothLevels() { + List callLog = new ArrayList<>(); + + // Plugin set on service stubs + SimplePlugin stubsPlugin = + SimplePlugin.newBuilder("stubs-plugin") + .customizeClient(builder -> callLog.add("stubs-plugin-configureClient")) + .build(); + + // Different plugin set on client + SimplePlugin clientPlugin = + SimplePlugin.newBuilder("client-plugin") + .customizeClient(builder -> callLog.add("client-plugin-configureClient")) + .build(); + + WorkflowServiceStubsOptions stubsOptions = + WorkflowServiceStubsOptions.newBuilder() + .setPlugins((io.temporal.serviceclient.WorkflowServiceStubsPlugin) stubsPlugin) + .build(); + + WorkflowClientOptions clientOptions = + WorkflowClientOptions.newBuilder() + .setPlugins((io.temporal.client.WorkflowClientPlugin) clientPlugin) + .build(); + + TestEnvironmentOptions testOptions = + TestEnvironmentOptions.newBuilder() + .setWorkflowServiceStubsOptions(stubsOptions) + .setWorkflowClientOptions(clientOptions) + .build(); + + TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance(testOptions); + try { + // Both plugins should have their configureClient called + // Propagated plugins come first, then explicit client plugins + assertEquals( + "Both plugins should be called in correct order", + Arrays.asList("stubs-plugin-configureClient", "client-plugin-configureClient"), + callLog); + } finally { + env.close(); + } + } + + @Test + public void testWorkerOnlyPluginOnFactoryOptions() { + List callLog = new ArrayList<>(); + + // Create a plugin that only uses worker-level customization + // (Even though SimplePlugin implements all interfaces, we only set worker callbacks) + SimplePlugin workerOnlyPlugin = + SimplePlugin.newBuilder("worker-only-plugin") + .customizeWorkerFactory(builder -> callLog.add("worker-only-configureWorkerFactory")) + .customizeWorker(builder -> callLog.add("worker-only-configureWorker")) + .build(); + + // Set the plugin on WorkerFactoryOptions (not on client) + io.temporal.worker.WorkerFactoryOptions factoryOptions = + io.temporal.worker.WorkerFactoryOptions.newBuilder() + .setPlugins((WorkerPlugin) workerOnlyPlugin) + .build(); + + TestEnvironmentOptions testOptions = + TestEnvironmentOptions.newBuilder().setWorkerFactoryOptions(factoryOptions).build(); + + TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance(testOptions); + try { + env.newWorker("test-task-queue"); + + // Worker-only plugin should have its methods called + assertTrue( + "configureWorkerFactory should be called", + callLog.contains("worker-only-configureWorkerFactory")); + assertTrue( + "configureWorker should be called", callLog.contains("worker-only-configureWorker")); + } finally { + env.close(); + } + } + + @Test + public void testMergedPluginsAtWorkerFactoryLevel() { + List callLog = new ArrayList<>(); + + // Plugin propagated from client + SimplePlugin clientPlugin = + SimplePlugin.newBuilder("client-plugin") + .customizeWorkerFactory(builder -> callLog.add("client-plugin-configureWorkerFactory")) + .build(); + + // Plugin set directly on factory options + SimplePlugin factoryPlugin = + SimplePlugin.newBuilder("factory-plugin") + .customizeWorkerFactory(builder -> callLog.add("factory-plugin-configureWorkerFactory")) + .build(); + + WorkflowClientOptions clientOptions = + WorkflowClientOptions.newBuilder() + .setPlugins((io.temporal.client.WorkflowClientPlugin) clientPlugin) + .build(); + + io.temporal.worker.WorkerFactoryOptions factoryOptions = + io.temporal.worker.WorkerFactoryOptions.newBuilder() + .setPlugins((WorkerPlugin) factoryPlugin) + .build(); + + TestEnvironmentOptions testOptions = + TestEnvironmentOptions.newBuilder() + .setWorkflowClientOptions(clientOptions) + .setWorkerFactoryOptions(factoryOptions) + .build(); + + TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance(testOptions); + try { + // Both plugins should be called - propagated first, then explicit + assertEquals( + "Both plugins should be called in correct order", + Arrays.asList( + "client-plugin-configureWorkerFactory", "factory-plugin-configureWorkerFactory"), + callLog); + } finally { + env.close(); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index ec41acbb1e..7372871c98 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -107,8 +107,8 @@ public void testConfigurationPhaseOrder() { // Simulate configuration phase (forward order) WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); for (Object plugin : plugins) { - if (plugin instanceof io.temporal.client.ClientPlugin) { - ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); + if (plugin instanceof io.temporal.client.WorkflowClientPlugin) { + ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); } } @@ -255,12 +255,15 @@ public void testShutdownWorkerReverseOrder() { } @Test - public void testSimplePluginImplementsBothInterfaces() { - SimplePlugin plugin = new SimplePlugin("dual-plugin") {}; + public void testSimplePluginImplementsAllInterfaces() { + SimplePlugin plugin = new SimplePlugin("full-plugin") {}; assertTrue( - "SimplePlugin should implement ClientPlugin", - plugin instanceof io.temporal.client.ClientPlugin); + "SimplePlugin should implement WorkflowServiceStubsPlugin", + plugin instanceof io.temporal.serviceclient.WorkflowServiceStubsPlugin); + assertTrue( + "SimplePlugin should implement WorkflowClientPlugin", + plugin instanceof io.temporal.client.WorkflowClientPlugin); assertTrue( "SimplePlugin should implement WorkerPlugin", plugin instanceof io.temporal.worker.WorkerPlugin); diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index b7e7c78b9d..333c997d06 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -43,14 +43,15 @@ public void testSimplePluginName() { } @Test - public void testSimplePluginImplementsBothInterfaces() { + public void testSimplePluginImplementsAllInterfaces() { SimplePlugin plugin = SimplePlugin.newBuilder("test").build(); assertTrue( - "Should implement io.temporal.client.ClientPlugin", - plugin instanceof io.temporal.client.ClientPlugin); + "Should implement WorkflowServiceStubsPlugin", + plugin instanceof io.temporal.serviceclient.WorkflowServiceStubsPlugin); assertTrue( - "Should implement io.temporal.worker.WorkerPlugin", - plugin instanceof io.temporal.worker.WorkerPlugin); + "Should implement WorkflowClientPlugin", + plugin instanceof io.temporal.client.WorkflowClientPlugin); + assertTrue("Should implement WorkerPlugin", plugin instanceof io.temporal.worker.WorkerPlugin); } @Test @@ -66,7 +67,7 @@ public void testCustomizeServiceStubs() { .build(); WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(); - ((io.temporal.client.ClientPlugin) plugin).configureServiceStubs(builder); + ((io.temporal.serviceclient.WorkflowServiceStubsPlugin) plugin).configureServiceStubs(builder); assertTrue("Customizer should have been called", customized.get()); } @@ -85,7 +86,7 @@ public void testCustomizeClient() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); assertTrue("Customizer should have been called", customized.get()); assertEquals("custom-identity", builder.build().getIdentity()); @@ -143,7 +144,7 @@ public void testMultipleCustomizers() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); assertEquals("All customizers should be called", 3, callCount.get()); } @@ -171,7 +172,7 @@ public void testAddClientInterceptors() { SimplePlugin.newBuilder("test").addClientInterceptors(interceptor).build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.ClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); WorkflowClientInterceptor[] interceptors = builder.build().getInterceptors(); assertEquals(1, interceptors.length); diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java index 7ae3077c7f..807adbd1a9 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubs.java @@ -7,7 +7,6 @@ import io.temporal.internal.testservice.InProcessGRPCServer; import java.time.Duration; import java.util.function.Supplier; -import javax.annotation.Nonnull; import javax.annotation.Nullable; /** Initializes and holds gRPC blocking and future stubs. */ @@ -34,6 +33,17 @@ static WorkflowServiceStubs newLocalServiceStubs() { * WorkflowServiceStubs#connect(Duration)} after creation or use {@link * #newConnectedServiceStubs(WorkflowServiceStubsOptions, Duration)} instead of this method. * + *

    If the options contain plugins (via {@link + * WorkflowServiceStubsOptions.Builder#setPlugins(WorkflowServiceStubsPlugin...)}), this method + * applies them in two phases: + * + *

      + *
    1. Configuration phase: Each plugin's {@code configureServiceStubs} method is called + * in forward (registration) order to modify the options builder + *
    2. Connection phase: Each plugin's {@code connectServiceClient} method is called in + * reverse order to wrap the connection (first plugin wraps all others) + *
    + * *

    Migration Note: This method doesn't respect {@link * WorkflowServiceStubsOptions.Builder#setDisableHealthCheck(boolean)}, {@link * WorkflowServiceStubsOptions.Builder#setHealthCheckAttemptTimeout(Duration)} (boolean)} and @@ -45,8 +55,34 @@ static WorkflowServiceStubs newLocalServiceStubs() { */ static WorkflowServiceStubs newServiceStubs(WorkflowServiceStubsOptions options) { enforceNonWorkflowThread(); - return WorkflowThreadMarker.protectFromWorkflowThread( - new WorkflowServiceStubsImpl(null, options), WorkflowServiceStubs.class); + + WorkflowServiceStubsPlugin[] plugins = options.getPlugins(); + if (plugins == null || plugins.length == 0) { + // No plugins - create stubs directly + return WorkflowThreadMarker.protectFromWorkflowThread( + new WorkflowServiceStubsImpl(null, options), WorkflowServiceStubs.class); + } + + // Apply plugin configuration phase (forward order) + WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(options); + for (WorkflowServiceStubsPlugin plugin : plugins) { + plugin.configureServiceStubs(builder); + } + WorkflowServiceStubsOptions finalOptions = builder.validateAndBuildWithDefaults(); + + // Build connection chain (reverse order for proper nesting) + Supplier connectionChain = + () -> + WorkflowThreadMarker.protectFromWorkflowThread( + new WorkflowServiceStubsImpl(null, finalOptions), WorkflowServiceStubs.class); + + for (int i = plugins.length - 1; i >= 0; i--) { + final Supplier next = connectionChain; + final WorkflowServiceStubsPlugin plugin = plugins[i]; + connectionChain = () -> plugin.connectServiceClient(finalOptions, next); + } + + return connectionChain.get(); } /** @@ -124,74 +160,5 @@ static WorkflowServiceStubs newInstance( new WorkflowServiceStubsImpl(service, options), WorkflowServiceStubs.class); } - /** - * Creates WorkflowService gRPC stubs with plugin support. - * - *

    This method applies plugins in two phases: - * - *

      - *
    1. Configuration phase: Each plugin's {@code configureServiceStubs} method is called - * in forward (registration) order to modify the options builder - *
    2. Connection phase: Each plugin's {@code connectServiceClient} method is called in - * reverse order to wrap the connection (first plugin wraps all others) - *
    - * - *

    This method creates stubs with "lazy" connectivity. The connection is not performed during - * the creation time and happens on the first request. - * - * @param options stub options to use - * @param plugins array of client plugins to apply - * @return the workflow service stubs - */ - static WorkflowServiceStubs newServiceStubs( - @Nonnull WorkflowServiceStubsOptions options, @Nonnull ClientPluginCallback[] plugins) { - enforceNonWorkflowThread(); - - // Apply plugin configuration phase (forward order) - WorkflowServiceStubsOptions.Builder builder = WorkflowServiceStubsOptions.newBuilder(options); - for (ClientPluginCallback plugin : plugins) { - plugin.configureServiceStubs(builder); - } - WorkflowServiceStubsOptions finalOptions = builder.validateAndBuildWithDefaults(); - - // Build connection chain (reverse order for proper nesting) - Supplier connectionChain = - () -> - WorkflowThreadMarker.protectFromWorkflowThread( - new WorkflowServiceStubsImpl(null, finalOptions), WorkflowServiceStubs.class); - - for (int i = plugins.length - 1; i >= 0; i--) { - final Supplier next = connectionChain; - final ClientPluginCallback callback = plugins[i]; - connectionChain = () -> callback.connectServiceClient(finalOptions, next); - } - - return connectionChain.get(); - } - - /** - * Callback interface for client plugins to participate in service stubs creation. This interface - * is implemented by {@code io.temporal.client.ClientPlugin} in the temporal-sdk module. - */ - interface ClientPluginCallback { - /** - * Allows the plugin to modify service stubs options before the service stubs are created. - * - * @param builder the options builder to modify - */ - void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder); - - /** - * Allows the plugin to wrap service client connection. - * - * @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) - */ - @Nonnull - WorkflowServiceStubs connectServiceClient( - @Nonnull WorkflowServiceStubsOptions options, @Nonnull Supplier next); - } - WorkflowServiceStubsOptions getOptions(); } diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java index 8663b97b2f..4d673be9ac 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java @@ -37,6 +37,12 @@ public final class WorkflowServiceStubsOptions extends ServiceStubsOptions { /** Retry options for outgoing RPC calls */ private final RpcRetryOptions rpcRetryOptions; + /** Plugins for customizing service stubs configuration and connection */ + private final WorkflowServiceStubsPlugin[] plugins; + + private static final WorkflowServiceStubsPlugin[] EMPTY_PLUGINS = + new WorkflowServiceStubsPlugin[0]; + public static Builder newBuilder() { return new Builder(); } @@ -54,12 +60,14 @@ private WorkflowServiceStubsOptions( boolean disableHealthCheck, Duration rpcLongPollTimeout, Duration rpcQueryTimeout, - RpcRetryOptions rpcRetryOptions) { + RpcRetryOptions rpcRetryOptions, + WorkflowServiceStubsPlugin[] plugins) { super(serviceStubsOptions); this.disableHealthCheck = disableHealthCheck; this.rpcLongPollTimeout = rpcLongPollTimeout; this.rpcQueryTimeout = rpcQueryTimeout; this.rpcRetryOptions = rpcRetryOptions; + this.plugins = plugins; } /** @@ -97,6 +105,15 @@ public RpcRetryOptions getRpcRetryOptions() { return rpcRetryOptions; } + /** + * Returns the service stubs plugins configured for this options. + * + * @return the array of service stubs plugins, never null + */ + public WorkflowServiceStubsPlugin[] getPlugins() { + return plugins; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -105,12 +122,16 @@ public boolean equals(Object o) { return disableHealthCheck == that.disableHealthCheck && Objects.equals(rpcLongPollTimeout, that.rpcLongPollTimeout) && Objects.equals(rpcQueryTimeout, that.rpcQueryTimeout) - && Objects.equals(rpcRetryOptions, that.rpcRetryOptions); + && Objects.equals(rpcRetryOptions, that.rpcRetryOptions) + && Arrays.equals(plugins, that.plugins); } @Override public int hashCode() { - return Objects.hash(disableHealthCheck, rpcLongPollTimeout, rpcQueryTimeout, rpcRetryOptions); + int result = + Objects.hash(disableHealthCheck, rpcLongPollTimeout, rpcQueryTimeout, rpcRetryOptions); + result = 31 * result + Arrays.hashCode(plugins); + return result; } @Override @@ -124,6 +145,8 @@ public String toString() { + rpcQueryTimeout + ", rpcRetryOptions=" + rpcRetryOptions + + ", plugins=" + + Arrays.toString(plugins) + '}'; } @@ -133,6 +156,7 @@ public static class Builder extends ServiceStubsOptions.Builder { private Duration rpcLongPollTimeout = DEFAULT_POLL_RPC_TIMEOUT; private Duration rpcQueryTimeout = DEFAULT_QUERY_RPC_TIMEOUT; private RpcRetryOptions rpcRetryOptions = DefaultStubServiceOperationRpcRetryOptions.INSTANCE; + private WorkflowServiceStubsPlugin[] plugins; private Builder() {} @@ -143,6 +167,7 @@ private Builder(ServiceStubsOptions options) { this.rpcLongPollTimeout = castedOptions.rpcLongPollTimeout; this.rpcQueryTimeout = castedOptions.rpcQueryTimeout; this.rpcRetryOptions = castedOptions.rpcRetryOptions; + this.plugins = castedOptions.plugins; } } @@ -240,6 +265,20 @@ public Builder setRpcRetryOptions(RpcRetryOptions rpcRetryOptions) { return this; } + /** + * Sets the workflow service stubs plugins to use for customizing configuration and connection. + * + *

    Plugins that implement both {@code WorkflowServiceStubsPlugin} and {@code + * WorkflowClientPlugin} will be automatically propagated to the workflow client. + * + * @param plugins the plugins to use + * @return this builder + */ + public Builder setPlugins(WorkflowServiceStubsPlugin... plugins) { + this.plugins = Objects.requireNonNull(plugins); + return this; + } + /** * Sets the rpc timeout value for query calls. Default is 10 seconds. * @@ -262,7 +301,8 @@ public WorkflowServiceStubsOptions build() { this.disableHealthCheck, this.rpcLongPollTimeout, this.rpcQueryTimeout, - this.rpcRetryOptions); + this.rpcRetryOptions, + this.plugins); } public WorkflowServiceStubsOptions validateAndBuildWithDefaults() { @@ -274,7 +314,8 @@ public WorkflowServiceStubsOptions validateAndBuildWithDefaults() { this.disableHealthCheck, this.rpcLongPollTimeout, this.rpcQueryTimeout, - retryOptions); + retryOptions, + this.plugins == null ? EMPTY_PLUGINS : this.plugins); } } } diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java new file mode 100644 index 0000000000..ee3a45a0e2 --- /dev/null +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java @@ -0,0 +1,63 @@ +/* + * 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.serviceclient; + +import java.util.function.Supplier; +import javax.annotation.Nonnull; + +/** + * Plugin interface for customizing Temporal workflow service stubs configuration and connection. + * + *

    This is a low-level plugin interface for configuring the gRPC connection to the Temporal + * server. For most use cases, use {@code WorkflowClientPlugin} or {@code WorkerPlugin} in the + * temporal-sdk module instead. + * + *

    Plugins that implement both {@code WorkflowServiceStubsPlugin} and {@code + * WorkflowClientPlugin} are automatically propagated from the service stubs to the workflow client. + */ +public interface WorkflowServiceStubsPlugin { + /** + * 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. + * + * @param builder the options builder to modify + */ + void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder); + + /** + * Allows the plugin to wrap service client connection. + * + * @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) + */ + @Nonnull + WorkflowServiceStubs connectServiceClient( + @Nonnull WorkflowServiceStubsOptions options, @Nonnull Supplier next); +} From 0eb2ec85d4b9222bd32a2c1b3c4a2772161b0a10 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 10:09:15 -0500 Subject: [PATCH 18/28] Lift up to a generic ServiceStubsPlugin --- .../serviceclient/ServiceStubsPlugin.java | 70 +++++++++++++++++++ .../WorkflowServiceStubsPlugin.java | 36 ++-------- 2 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java new file mode 100644 index 0000000000..9f68bad869 --- /dev/null +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java @@ -0,0 +1,70 @@ +/* + * 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.serviceclient; + +import java.util.function.Supplier; +import javax.annotation.Nonnull; + +/** + * Base plugin interface for customizing Temporal service stubs configuration and connection. + * + *

    This is a generic interface parameterized by the options, builder, and stubs types. Specific + * service types have their own plugin interfaces that extend this one: + * + *

      + *
    • {@link WorkflowServiceStubsPlugin} for {@link WorkflowServiceStubs} + *
    + * + * @param the options type (e.g., WorkflowServiceStubsOptions) + * @param the builder type (e.g., WorkflowServiceStubsOptions.Builder) + * @param the service stubs type (e.g., WorkflowServiceStubs) + */ +public interface ServiceStubsPlugin< + O extends ServiceStubsOptions, + B extends ServiceStubsOptions.Builder, + S extends ServiceStubs> { + + /** + * 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. + * + * @param builder the options builder to modify + */ + void configureServiceStubs(@Nonnull B builder); + + /** + * Allows the plugin to wrap service client connection. + * + * @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) + */ + @Nonnull + S connectServiceClient(@Nonnull O options, @Nonnull Supplier next); +} diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java index ee3a45a0e2..fd8a0ad5b8 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java @@ -20,9 +20,6 @@ package io.temporal.serviceclient; -import java.util.function.Supplier; -import javax.annotation.Nonnull; - /** * Plugin interface for customizing Temporal workflow service stubs configuration and connection. * @@ -32,32 +29,9 @@ * *

    Plugins that implement both {@code WorkflowServiceStubsPlugin} and {@code * WorkflowClientPlugin} are automatically propagated from the service stubs to the workflow client. + * + * @see ServiceStubsPlugin */ -public interface WorkflowServiceStubsPlugin { - /** - * 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. - * - * @param builder the options builder to modify - */ - void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder); - - /** - * Allows the plugin to wrap service client connection. - * - * @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) - */ - @Nonnull - WorkflowServiceStubs connectServiceClient( - @Nonnull WorkflowServiceStubsOptions options, @Nonnull Supplier next); -} +public interface WorkflowServiceStubsPlugin + extends ServiceStubsPlugin< + WorkflowServiceStubsOptions, WorkflowServiceStubsOptions.Builder, WorkflowServiceStubs> {} From b4a09c58594eda6c89b66e7eb2a12d4f0caab56c Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 12:52:02 -0500 Subject: [PATCH 19/28] Add ScheduleClientPlugin, and rename WorkflowClientPlugin.configureClient to WorkflowClientPlugin.configureWorkflowClient --- .../client/WorkflowClientInternalImpl.java | 2 +- .../temporal/client/WorkflowClientPlugin.java | 2 +- .../client/schedules/ScheduleClientImpl.java | 56 +++++++++++++++++++ .../schedules/ScheduleClientOptions.java | 34 ++++++++++- .../schedules/ScheduleClientPlugin.java | 54 ++++++++++++++++++ .../java/io/temporal/common/SimplePlugin.java | 42 ++++++++++++-- .../common/PluginPropagationTest.java | 22 ++++---- .../java/io/temporal/common/PluginTest.java | 8 +-- .../common/SimplePluginBuilderTest.java | 6 +- 9 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientPlugin.java 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 1d63552493..1bea2e3ca4 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -794,7 +794,7 @@ private static void applyClientPluginConfiguration( return; } for (WorkflowClientPlugin plugin : plugins) { - plugin.configureClient(builder); + plugin.configureWorkflowClient(builder); } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java index 27bb00b20b..702fa141bf 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientPlugin.java @@ -76,5 +76,5 @@ public interface WorkflowClientPlugin { * * @param builder the options builder to modify */ - void configureClient(@Nonnull WorkflowClientOptions.Builder builder); + void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java index 5ecc273313..2177b8080f 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java @@ -12,6 +12,9 @@ import io.temporal.internal.client.external.GenericWorkflowClientImpl; import io.temporal.serviceclient.MetricsTag; import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -41,6 +44,19 @@ public static ScheduleClient newInstance( } ScheduleClientImpl(WorkflowServiceStubs workflowServiceStubs, ScheduleClientOptions options) { + // Extract ScheduleClientPlugins from service stubs plugins (propagation) + ScheduleClientPlugin[] propagatedPlugins = + extractScheduleClientPlugins(workflowServiceStubs.getOptions().getPlugins()); + + // Merge propagated plugins with schedule client-specified plugins + ScheduleClientPlugin[] mergedPlugins = mergePlugins(propagatedPlugins, options.getPlugins()); + + // Apply plugin configuration phase (forward order) + ScheduleClientOptions.Builder builder = ScheduleClientOptions.newBuilder(options); + builder.setPlugins(mergedPlugins); + applyPluginConfiguration(builder, mergedPlugins); + options = builder.build(); + workflowServiceStubs = new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace()); this.workflowServiceStubs = workflowServiceStubs; @@ -55,6 +71,46 @@ public static ScheduleClient newInstance( this.scheduleClientCallsInvoker = initializeClientInvoker(); } + private static ScheduleClientPlugin[] extractScheduleClientPlugins( + WorkflowServiceStubsPlugin[] stubsPlugins) { + if (stubsPlugins == null || stubsPlugins.length == 0) { + return new ScheduleClientPlugin[0]; + } + List schedulePlugins = new ArrayList<>(); + for (WorkflowServiceStubsPlugin plugin : stubsPlugins) { + if (plugin instanceof ScheduleClientPlugin) { + schedulePlugins.add((ScheduleClientPlugin) plugin); + } + } + return schedulePlugins.toArray(new ScheduleClientPlugin[0]); + } + + private static ScheduleClientPlugin[] mergePlugins( + ScheduleClientPlugin[] propagated, ScheduleClientPlugin[] explicit) { + if ((propagated == null || propagated.length == 0) + && (explicit == null || explicit.length == 0)) { + return new ScheduleClientPlugin[0]; + } + List merged = new ArrayList<>(); + if (propagated != null) { + merged.addAll(Arrays.asList(propagated)); + } + if (explicit != null) { + merged.addAll(Arrays.asList(explicit)); + } + return merged.toArray(new ScheduleClientPlugin[0]); + } + + private static void applyPluginConfiguration( + ScheduleClientOptions.Builder builder, ScheduleClientPlugin[] plugins) { + if (plugins == null) { + return; + } + for (ScheduleClientPlugin plugin : plugins) { + plugin.configureScheduleClient(builder); + } + } + private ScheduleClientCallsInterceptor initializeClientInvoker() { ScheduleClientCallsInterceptor scheduleClientInvoker = new RootScheduleClientInvoker(genericClient, options); diff --git a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientOptions.java b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientOptions.java index 9f830eb63a..775ad59fcf 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientOptions.java @@ -1,10 +1,12 @@ package io.temporal.client.schedules; +import io.temporal.common.Experimental; import io.temporal.common.context.ContextPropagator; import io.temporal.common.converter.DataConverter; import io.temporal.common.converter.GlobalDataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import java.lang.management.ManagementFactory; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -39,12 +41,14 @@ public static final class Builder { Collections.emptyList(); private static final List EMPTY_INTERCEPTORS = Collections.emptyList(); + private static final ScheduleClientPlugin[] EMPTY_PLUGINS = new ScheduleClientPlugin[0]; private String namespace; private DataConverter dataConverter; private String identity; private List contextPropagators; private List interceptors; + private ScheduleClientPlugin[] plugins; private Builder() {} @@ -57,6 +61,7 @@ private Builder(ScheduleClientOptions options) { identity = options.identity; contextPropagators = options.contextPropagators; interceptors = options.interceptors; + plugins = options.plugins; } /** Set the namespace this client will operate on. */ @@ -101,6 +106,17 @@ public Builder setInterceptors(List interceptors) { return this; } + /** + * Set the plugins for this client. + * + * @param plugins specifies the plugins to use with the client. + */ + @Experimental + public Builder setPlugins(ScheduleClientPlugin... plugins) { + this.plugins = plugins; + return this; + } + public ScheduleClientOptions build() { String name = identity == null ? ManagementFactory.getRuntimeMXBean().getName() : identity; return new ScheduleClientOptions( @@ -108,7 +124,8 @@ public ScheduleClientOptions build() { dataConverter == null ? GlobalDataConverter.get() : dataConverter, name, contextPropagators == null ? EMPTY_CONTEXT_PROPAGATORS : contextPropagators, - interceptors == null ? EMPTY_INTERCEPTORS : interceptors); + interceptors == null ? EMPTY_INTERCEPTORS : interceptors, + plugins == null ? EMPTY_PLUGINS : plugins); } } @@ -117,18 +134,21 @@ public ScheduleClientOptions build() { private final String identity; private final List contextPropagators; private final List interceptors; + private final ScheduleClientPlugin[] plugins; private ScheduleClientOptions( String namespace, DataConverter dataConverter, String identity, List contextPropagators, - List interceptors) { + List interceptors, + ScheduleClientPlugin[] plugins) { this.namespace = namespace; this.dataConverter = dataConverter; this.identity = identity; this.contextPropagators = contextPropagators; this.interceptors = interceptors; + this.plugins = plugins; } /** @@ -175,4 +195,14 @@ public List getContextPropagators() { public List getInterceptors() { return interceptors; } + + /** + * Get the plugins of this client + * + * @return The plugins to use with the client. + */ + @Experimental + public ScheduleClientPlugin[] getPlugins() { + return plugins == null ? new ScheduleClientPlugin[0] : Arrays.copyOf(plugins, plugins.length); + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientPlugin.java b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientPlugin.java new file mode 100644 index 0000000000..a97bdc365e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientPlugin.java @@ -0,0 +1,54 @@ +/* + * 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.schedules; + +import io.temporal.common.Experimental; +import javax.annotation.Nonnull; + +/** + * Plugin interface for customizing Temporal schedule client configuration. + * + *

    Plugins that implement both {@link io.temporal.serviceclient.WorkflowServiceStubsPlugin} and + * {@code ScheduleClientPlugin} are automatically propagated from the service stubs to the schedule + * client. + * + * @see io.temporal.serviceclient.WorkflowServiceStubsPlugin + */ +@Experimental +public interface ScheduleClientPlugin { + + /** + * 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 schedule client options before the client is created. Called during + * configuration phase in forward (registration) order. + * + * @param builder the options builder to modify + */ + void configureScheduleClient(@Nonnull ScheduleClientOptions.Builder builder); +} diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index 0b1e38f62d..4a2f2b5d8f 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -22,6 +22,8 @@ import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowClientPlugin; +import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.context.ContextPropagator; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -44,8 +46,8 @@ import javax.annotation.Nonnull; /** - * A plugin that implements {@link WorkflowServiceStubsPlugin}, {@link WorkflowClientPlugin}, and - * {@link WorkerPlugin}. This class can be used in two ways: + * A plugin that implements {@link WorkflowServiceStubsPlugin}, {@link WorkflowClientPlugin}, {@link + * ScheduleClientPlugin}, and {@link WorkerPlugin}. This class can be used in two ways: * *

      *
    1. Builder pattern: Use {@link #newBuilder(String)} to declaratively configure a plugin @@ -75,7 +77,7 @@ * } * * @Override - * public void configureClient(WorkflowClientOptions.Builder builder) { + * public void configureWorkflowClient(WorkflowClientOptions.Builder builder) { * builder.setInterceptors(new TracingClientInterceptor(tracer)); * } * } @@ -99,15 +101,20 @@ * * @see WorkflowServiceStubsPlugin * @see WorkflowClientPlugin + * @see ScheduleClientPlugin * @see WorkerPlugin */ @Experimental public class SimplePlugin - implements WorkflowServiceStubsPlugin, WorkflowClientPlugin, WorkerPlugin { + implements WorkflowServiceStubsPlugin, + WorkflowClientPlugin, + ScheduleClientPlugin, + WorkerPlugin { private final String name; private final List> stubsCustomizers; private final List> clientCustomizers; + private final List> scheduleCustomizers; private final List> factoryCustomizers; private final List> workerCustomizers; private final List> workerInitializers; @@ -131,6 +138,7 @@ protected SimplePlugin(@Nonnull String name) { this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); this.stubsCustomizers = Collections.emptyList(); this.clientCustomizers = Collections.emptyList(); + this.scheduleCustomizers = Collections.emptyList(); this.factoryCustomizers = Collections.emptyList(); this.workerCustomizers = Collections.emptyList(); this.workerInitializers = Collections.emptyList(); @@ -155,6 +163,7 @@ protected SimplePlugin(@Nonnull Builder builder) { this.name = builder.name; this.stubsCustomizers = new ArrayList<>(builder.stubsCustomizers); this.clientCustomizers = new ArrayList<>(builder.clientCustomizers); + this.scheduleCustomizers = new ArrayList<>(builder.scheduleCustomizers); this.factoryCustomizers = new ArrayList<>(builder.factoryCustomizers); this.workerCustomizers = new ArrayList<>(builder.workerCustomizers); this.workerInitializers = new ArrayList<>(builder.workerInitializers); @@ -192,7 +201,7 @@ public void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder b } @Override - public void configureClient(@Nonnull WorkflowClientOptions.Builder builder) { + public void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder) { // Apply customizers for (Consumer customizer : clientCustomizers) { customizer.accept(builder); @@ -217,6 +226,14 @@ public void configureClient(@Nonnull WorkflowClientOptions.Builder builder) { } } + @Override + public void configureScheduleClient(@Nonnull ScheduleClientOptions.Builder builder) { + // Apply customizers + for (Consumer customizer : scheduleCustomizers) { + customizer.accept(builder); + } + } + @Override public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { // Apply customizers @@ -301,6 +318,8 @@ public static final class Builder { new ArrayList<>(); private final List> clientCustomizers = new ArrayList<>(); + private final List> scheduleCustomizers = + new ArrayList<>(); private final List> factoryCustomizers = new ArrayList<>(); private final List> workerCustomizers = new ArrayList<>(); @@ -342,6 +361,19 @@ public Builder customizeClient(@Nonnull Consumer return this; } + /** + * Adds a customizer for {@link ScheduleClientOptions}. Multiple customizers are applied in the + * order they are added. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public Builder customizeScheduleClient( + @Nonnull Consumer customizer) { + scheduleCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + /** * Adds a customizer for {@link WorkerFactoryOptions}. Multiple customizers are applied in the * order they are added. diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java index 41aff8728b..1026b407d5 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginPropagationTest.java @@ -51,7 +51,7 @@ public void testPluginPropagatesFromServiceStubsToWorkerFactory() { }) .customizeClient( builder -> { - callLog.add("configureClient"); + callLog.add("configureWorkflowClient"); }) .customizeWorkerFactory( builder -> { @@ -82,8 +82,8 @@ public void testPluginPropagatesFromServiceStubsToWorkerFactory() { assertTrue( "configureServiceStubs should be called", callLog.contains("configureServiceStubs")); assertTrue( - "configureClient should be called (propagated from service stubs)", - callLog.contains("configureClient")); + "configureWorkflowClient should be called (propagated from service stubs)", + callLog.contains("configureWorkflowClient")); assertTrue( "configureWorkerFactory should be called (propagated from client)", callLog.contains("configureWorkerFactory")); @@ -96,7 +96,7 @@ public void testPluginPropagatesFromServiceStubsToWorkerFactory() { "Configuration should happen in correct order", Arrays.asList( "configureServiceStubs", - "configureClient", + "configureWorkflowClient", "configureWorkerFactory", "configureWorker"), callLog); @@ -118,7 +118,7 @@ public void testPluginSetOnClientOnlyDoesNotAffectServiceStubs() { }) .customizeClient( builder -> { - callLog.add("configureClient"); + callLog.add("configureWorkflowClient"); }) .customizeWorkerFactory( builder -> { @@ -144,7 +144,8 @@ public void testPluginSetOnClientOnlyDoesNotAffectServiceStubs() { "configureServiceStubs should NOT be called", callLog.contains("configureServiceStubs")); // But client and worker factory should be called - assertTrue("configureClient should be called", callLog.contains("configureClient")); + assertTrue( + "configureWorkflowClient should be called", callLog.contains("configureWorkflowClient")); assertTrue( "configureWorkerFactory should be called", callLog.contains("configureWorkerFactory")); } finally { @@ -159,13 +160,13 @@ public void testMergedPluginsFromBothLevels() { // Plugin set on service stubs SimplePlugin stubsPlugin = SimplePlugin.newBuilder("stubs-plugin") - .customizeClient(builder -> callLog.add("stubs-plugin-configureClient")) + .customizeClient(builder -> callLog.add("stubs-plugin-configureWorkflowClient")) .build(); // Different plugin set on client SimplePlugin clientPlugin = SimplePlugin.newBuilder("client-plugin") - .customizeClient(builder -> callLog.add("client-plugin-configureClient")) + .customizeClient(builder -> callLog.add("client-plugin-configureWorkflowClient")) .build(); WorkflowServiceStubsOptions stubsOptions = @@ -186,11 +187,12 @@ public void testMergedPluginsFromBothLevels() { TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance(testOptions); try { - // Both plugins should have their configureClient called + // Both plugins should have their configureWorkflowClient called // Propagated plugins come first, then explicit client plugins assertEquals( "Both plugins should be called in correct order", - Arrays.asList("stubs-plugin-configureClient", "client-plugin-configureClient"), + Arrays.asList( + "stubs-plugin-configureWorkflowClient", "client-plugin-configureWorkflowClient"), callLog); } finally { env.close(); diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index 7372871c98..d9e3967608 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -58,9 +58,9 @@ public void testSimplePluginDefaultBehavior() throws Exception { WorkflowServiceStubsOptions.Builder stubsBuilder = WorkflowServiceStubsOptions.newBuilder(); plugin.configureServiceStubs(stubsBuilder); - // Test configureClient doesn't throw (no customizers) + // Test configureWorkflowClient doesn't throw (no customizers) WorkflowClientOptions.Builder clientBuilder = WorkflowClientOptions.newBuilder(); - plugin.configureClient(clientBuilder); + plugin.configureWorkflowClient(clientBuilder); // Test configureWorkerFactory doesn't throw (no customizers) WorkerFactoryOptions.Builder factoryBuilder = WorkerFactoryOptions.newBuilder(); @@ -108,7 +108,7 @@ public void testConfigurationPhaseOrder() { WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); for (Object plugin : plugins) { if (plugin instanceof io.temporal.client.WorkflowClientPlugin) { - ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureWorkflowClient(builder); } } @@ -272,7 +272,7 @@ public void testSimplePluginImplementsAllInterfaces() { private SimplePlugin createTrackingPlugin(String name, List order) { return new SimplePlugin(name) { @Override - public void configureClient(WorkflowClientOptions.Builder builder) { + public void configureWorkflowClient(WorkflowClientOptions.Builder builder) { order.add(name + "-config"); } }; diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index 333c997d06..25631ea3b2 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -86,7 +86,7 @@ public void testCustomizeClient() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureWorkflowClient(builder); assertTrue("Customizer should have been called", customized.get()); assertEquals("custom-identity", builder.build().getIdentity()); @@ -144,7 +144,7 @@ public void testMultipleCustomizers() { .build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureWorkflowClient(builder); assertEquals("All customizers should be called", 3, callCount.get()); } @@ -172,7 +172,7 @@ public void testAddClientInterceptors() { SimplePlugin.newBuilder("test").addClientInterceptors(interceptor).build(); WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - ((io.temporal.client.WorkflowClientPlugin) plugin).configureClient(builder); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureWorkflowClient(builder); WorkflowClientInterceptor[] interceptors = builder.build().getInterceptors(); assertEquals(1, interceptors.length); From b9264c685db79123705824e14218cb77485fbce1 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 12:54:11 -0500 Subject: [PATCH 20/28] Remove ServiceStubsPlugin super interface --- .../serviceclient/ServiceStubsPlugin.java | 70 ------------------- .../WorkflowServiceStubsPlugin.java | 37 ++++++++-- 2 files changed, 32 insertions(+), 75 deletions(-) delete mode 100644 temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java deleted file mode 100644 index 9f68bad869..0000000000 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsPlugin.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.serviceclient; - -import java.util.function.Supplier; -import javax.annotation.Nonnull; - -/** - * Base plugin interface for customizing Temporal service stubs configuration and connection. - * - *

      This is a generic interface parameterized by the options, builder, and stubs types. Specific - * service types have their own plugin interfaces that extend this one: - * - *

        - *
      • {@link WorkflowServiceStubsPlugin} for {@link WorkflowServiceStubs} - *
      - * - * @param the options type (e.g., WorkflowServiceStubsOptions) - * @param the builder type (e.g., WorkflowServiceStubsOptions.Builder) - * @param the service stubs type (e.g., WorkflowServiceStubs) - */ -public interface ServiceStubsPlugin< - O extends ServiceStubsOptions, - B extends ServiceStubsOptions.Builder, - S extends ServiceStubs> { - - /** - * 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. - * - * @param builder the options builder to modify - */ - void configureServiceStubs(@Nonnull B builder); - - /** - * Allows the plugin to wrap service client connection. - * - * @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) - */ - @Nonnull - S connectServiceClient(@Nonnull O options, @Nonnull Supplier next); -} diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java index fd8a0ad5b8..ea1ce31294 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsPlugin.java @@ -20,6 +20,9 @@ package io.temporal.serviceclient; +import java.util.function.Supplier; +import javax.annotation.Nonnull; + /** * Plugin interface for customizing Temporal workflow service stubs configuration and connection. * @@ -29,9 +32,33 @@ * *

      Plugins that implement both {@code WorkflowServiceStubsPlugin} and {@code * WorkflowClientPlugin} are automatically propagated from the service stubs to the workflow client. - * - * @see ServiceStubsPlugin */ -public interface WorkflowServiceStubsPlugin - extends ServiceStubsPlugin< - WorkflowServiceStubsOptions, WorkflowServiceStubsOptions.Builder, WorkflowServiceStubs> {} +public interface WorkflowServiceStubsPlugin { + + /** + * 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. + * + * @param builder the options builder to modify + */ + void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder); + + /** + * Allows the plugin to wrap service client connection. + * + * @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) + */ + @Nonnull + WorkflowServiceStubs connectServiceClient( + @Nonnull WorkflowServiceStubsOptions options, @Nonnull Supplier next); +} From cf149a0f6e6eff0a4733418481d7b97df881ecb3 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 13:03:47 -0500 Subject: [PATCH 21/28] Document order of validation and plugin application --- .../io/temporal/client/WorkflowClientOptions.java | 11 +++++++++++ .../java/io/temporal/worker/WorkerFactoryOptions.java | 11 +++++++++++ .../serviceclient/WorkflowServiceStubsOptions.java | 11 +++++++++++ 3 files changed, 33 insertions(+) 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 83b8af4e7d..ca67852751 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java @@ -165,6 +165,17 @@ public WorkflowClientOptions build() { plugins == null ? EMPTY_PLUGINS : plugins); } + /** + * Validates options and builds with defaults applied. + * + *

      Note: If plugins are configured via {@link #setPlugins(WorkflowClientPlugin...)}, they + * will have an opportunity to modify options after this method is called, when the options are + * passed to {@link WorkflowClient#newInstance}. This means validation performed here occurs + * before plugin modifications. In most cases, users should simply call {@link #build()} and let + * the client creation handle validation. + * + * @return validated options with defaults applied + */ public WorkflowClientOptions validateAndBuildWithDefaults() { String name = identity == null ? ManagementFactory.getRuntimeMXBean().getName() : identity; return new WorkflowClientOptions( 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 0c6175678e..ee24188110 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactoryOptions.java @@ -168,6 +168,17 @@ public WorkerFactoryOptions build() { false); } + /** + * Validates options and builds with defaults applied. + * + *

      Note: If plugins are configured via {@link #setPlugins(WorkerPlugin...)}, they will have + * an opportunity to modify options after this method is called, when the options are passed to + * {@link WorkerFactory#newInstance}. This means validation performed here occurs before plugin + * modifications. In most cases, users should simply call {@link #build()} and let the factory + * creation handle validation. + * + * @return validated options with defaults applied + */ public WorkerFactoryOptions validateAndBuildWithDefaults() { return new WorkerFactoryOptions( workflowCacheSize, diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java index 4d673be9ac..f22a2ac67e 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/WorkflowServiceStubsOptions.java @@ -305,6 +305,17 @@ public WorkflowServiceStubsOptions build() { this.plugins); } + /** + * Validates options and builds with defaults applied. + * + *

      Note: If plugins are configured via {@link #setPlugins(WorkflowServiceStubsPlugin...)}, + * they will have an opportunity to modify options after this method is called, when the options + * are passed to {@link WorkflowServiceStubs#newServiceStubs(WorkflowServiceStubsOptions)}. This + * means validation performed here occurs before plugin modifications. In most cases, users + * should simply call {@link #build()} and let the service stubs creation handle validation. + * + * @return validated options with defaults applied + */ public WorkflowServiceStubsOptions validateAndBuildWithDefaults() { ServiceStubsOptions serviceStubsOptions = super.validateAndBuildWithDefaults(); RpcRetryOptions retryOptions = From 8b4b73f383195d34dfa48c02ca5c7ae553b25ea7 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 13:16:19 -0500 Subject: [PATCH 22/28] abstract SimplePlugin --- .../main/java/io/temporal/common/SimplePlugin.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index 4a2f2b5d8f..e9947bd27e 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -105,7 +105,7 @@ * @see WorkerPlugin */ @Experimental -public class SimplePlugin +public abstract class SimplePlugin implements WorkflowServiceStubsPlugin, WorkflowClientPlugin, ScheduleClientPlugin, @@ -564,7 +564,14 @@ public Builder addContextPropagators(ContextPropagator... propagators) { * @return a new plugin instance */ public SimplePlugin build() { - return new SimplePlugin(this); + return new SimplePluginImpl(this); + } + } + + /** Private concrete implementation returned by the builder. */ + private static final class SimplePluginImpl extends SimplePlugin { + SimplePluginImpl(Builder builder) { + super(builder); } } } From ac094ecfa685c62b3b0d9b860e3445183fab9dee Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 13:28:08 -0500 Subject: [PATCH 23/28] Add data converters, activities, workflows, Nexus services --- .../java/io/temporal/common/SimplePlugin.java | 111 ++++++++++++++++++ .../common/SimplePluginBuilderTest.java | 106 +++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index e9947bd27e..7ccc847d5d 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -25,6 +25,7 @@ import io.temporal.client.schedules.ScheduleClientOptions; import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.context.ContextPropagator; +import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; import io.temporal.serviceclient.WorkflowServiceStubs; @@ -125,6 +126,10 @@ public abstract class SimplePlugin private final List workerInterceptors; private final List clientInterceptors; private final List contextPropagators; + private final DataConverter dataConverter; + private final List> workflowImplementationTypes; + private final List activitiesImplementations; + private final List nexusServiceImplementations; /** * Creates a new plugin with the specified name. Use this constructor when subclassing to override @@ -149,6 +154,10 @@ protected SimplePlugin(@Nonnull String name) { this.workerInterceptors = Collections.emptyList(); this.clientInterceptors = Collections.emptyList(); this.contextPropagators = Collections.emptyList(); + this.dataConverter = null; + this.workflowImplementationTypes = Collections.emptyList(); + this.activitiesImplementations = Collections.emptyList(); + this.nexusServiceImplementations = Collections.emptyList(); } /** @@ -174,6 +183,10 @@ protected SimplePlugin(@Nonnull Builder builder) { this.workerInterceptors = new ArrayList<>(builder.workerInterceptors); this.clientInterceptors = new ArrayList<>(builder.clientInterceptors); this.contextPropagators = new ArrayList<>(builder.contextPropagators); + this.dataConverter = builder.dataConverter; + this.workflowImplementationTypes = new ArrayList<>(builder.workflowImplementationTypes); + this.activitiesImplementations = new ArrayList<>(builder.activitiesImplementations); + this.nexusServiceImplementations = new ArrayList<>(builder.nexusServiceImplementations); } /** @@ -207,6 +220,11 @@ public void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder build customizer.accept(builder); } + // Set data converter + if (dataConverter != null) { + builder.setDataConverter(dataConverter); + } + // Add client interceptors if (!clientInterceptors.isEmpty()) { WorkflowClientInterceptor[] existing = builder.build().getInterceptors(); @@ -260,6 +278,23 @@ public void configureWorker(@Nonnull String taskQueue, @Nonnull WorkerOptions.Bu @Override public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { + // Register workflow implementation types + if (!workflowImplementationTypes.isEmpty()) { + worker.registerWorkflowImplementationTypes( + workflowImplementationTypes.toArray(new Class[0])); + } + + // Register activities implementations + if (!activitiesImplementations.isEmpty()) { + worker.registerActivitiesImplementations(activitiesImplementations.toArray()); + } + + // Register nexus service implementations + for (Object nexusService : nexusServiceImplementations) { + worker.registerNexusServiceImplementation(nexusService); + } + + // Apply custom initializers for (BiConsumer initializer : workerInitializers) { initializer.accept(taskQueue, worker); } @@ -331,6 +366,10 @@ public static final class Builder { private final List workerInterceptors = new ArrayList<>(); private final List clientInterceptors = new ArrayList<>(); private final List contextPropagators = new ArrayList<>(); + private DataConverter dataConverter; + private final List> workflowImplementationTypes = new ArrayList<>(); + private final List activitiesImplementations = new ArrayList<>(); + private final List nexusServiceImplementations = new ArrayList<>(); private Builder(@Nonnull String name) { this.name = Objects.requireNonNull(name, "Plugin name cannot be null"); @@ -558,6 +597,78 @@ public Builder addContextPropagators(ContextPropagator... propagators) { return this; } + /** + * Sets the data converter to use for serializing workflow and activity arguments and results. + * This overrides any data converter previously set on the client options. + * + * @param dataConverter the data converter to use + * @return this builder for chaining + */ + public Builder setDataConverter(@Nonnull DataConverter dataConverter) { + this.dataConverter = Objects.requireNonNull(dataConverter); + return this; + } + + /** + * Registers workflow implementation types. These workflows will be registered on all workers + * created by the factory. + * + *

      Example: + * + *

      {@code
      +     * SimplePlugin.newBuilder("my-plugin")
      +     *     .registerWorkflowImplementationTypes(MyWorkflowImpl.class, OtherWorkflowImpl.class)
      +     *     .build();
      +     * }
      + * + * @param workflowImplementationTypes workflow implementation classes to register + * @return this builder for chaining + */ + public Builder registerWorkflowImplementationTypes(Class... workflowImplementationTypes) { + this.workflowImplementationTypes.addAll(Arrays.asList(workflowImplementationTypes)); + return this; + } + + /** + * Registers activity implementations. These activities will be registered on all workers + * created by the factory. + * + *

      Example: + * + *

      {@code
      +     * SimplePlugin.newBuilder("my-plugin")
      +     *     .registerActivitiesImplementations(new MyActivityImpl(), new OtherActivityImpl())
      +     *     .build();
      +     * }
      + * + * @param activitiesImplementations activity implementation instances to register + * @return this builder for chaining + */ + public Builder registerActivitiesImplementations(Object... activitiesImplementations) { + this.activitiesImplementations.addAll(Arrays.asList(activitiesImplementations)); + return this; + } + + /** + * Registers a Nexus service implementation. The service will be registered on all workers + * created by the factory. + * + *

      Example: + * + *

      {@code
      +     * SimplePlugin.newBuilder("my-plugin")
      +     *     .registerNexusServiceImplementation(new MyNexusServiceImpl())
      +     *     .build();
      +     * }
      + * + * @param nexusServiceImplementation the Nexus service implementation to register + * @return this builder for chaining + */ + public Builder registerNexusServiceImplementation(@Nonnull Object nexusServiceImplementation) { + this.nexusServiceImplementations.add(Objects.requireNonNull(nexusServiceImplementation)); + return this; + } + /** * Builds the plugin with the configured settings. * diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index 25631ea3b2..3c9a49b86a 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -21,13 +21,16 @@ package io.temporal.common; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import io.temporal.client.WorkflowClientOptions; +import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkerInterceptorBase; import io.temporal.common.interceptors.WorkflowClientInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptorBase; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.worker.Worker; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; import java.util.concurrent.atomic.AtomicBoolean; @@ -409,4 +412,107 @@ public void testNullName() { public void testNullCustomizer() { SimplePlugin.newBuilder("test").customizeClient(null); } + + @Test + public void testSetDataConverter() { + DataConverter customConverter = mock(DataConverter.class); + + SimplePlugin plugin = SimplePlugin.newBuilder("test").setDataConverter(customConverter).build(); + + WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); + ((io.temporal.client.WorkflowClientPlugin) plugin).configureWorkflowClient(builder); + + assertSame(customConverter, builder.build().getDataConverter()); + } + + @Test(expected = NullPointerException.class) + public void testNullDataConverter() { + SimplePlugin.newBuilder("test").setDataConverter(null); + } + + @Test + public void testRegisterWorkflowImplementationTypes() { + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .registerWorkflowImplementationTypes(String.class, Integer.class) + .build(); + + Worker mockWorker = mock(Worker.class); + ((io.temporal.worker.WorkerPlugin) plugin).initializeWorker("test-queue", mockWorker); + + verify(mockWorker).registerWorkflowImplementationTypes(String.class, Integer.class); + } + + @Test + public void testRegisterActivitiesImplementations() { + Object activity1 = new Object(); + Object activity2 = new Object(); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .registerActivitiesImplementations(activity1, activity2) + .build(); + + Worker mockWorker = mock(Worker.class); + ((io.temporal.worker.WorkerPlugin) plugin).initializeWorker("test-queue", mockWorker); + + verify(mockWorker).registerActivitiesImplementations(activity1, activity2); + } + + @Test + public void testRegisterNexusServiceImplementation() { + Object nexusService = new Object(); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test").registerNexusServiceImplementation(nexusService).build(); + + Worker mockWorker = mock(Worker.class); + ((io.temporal.worker.WorkerPlugin) plugin).initializeWorker("test-queue", mockWorker); + + verify(mockWorker).registerNexusServiceImplementation(nexusService); + } + + @Test + public void testRegisterMultipleNexusServiceImplementations() { + Object nexusService1 = new Object(); + Object nexusService2 = new Object(); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .registerNexusServiceImplementation(nexusService1) + .registerNexusServiceImplementation(nexusService2) + .build(); + + Worker mockWorker = mock(Worker.class); + ((io.temporal.worker.WorkerPlugin) plugin).initializeWorker("test-queue", mockWorker); + + verify(mockWorker).registerNexusServiceImplementation(nexusService1); + verify(mockWorker).registerNexusServiceImplementation(nexusService2); + } + + @Test(expected = NullPointerException.class) + public void testNullNexusServiceImplementation() { + SimplePlugin.newBuilder("test").registerNexusServiceImplementation(null); + } + + @Test + public void testRegistrationsWithCustomInitializer() { + AtomicBoolean customInitializerCalled = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .registerWorkflowImplementationTypes(String.class) + .registerActivitiesImplementations(new Object()) + .initializeWorker((taskQueue, worker) -> customInitializerCalled.set(true)) + .build(); + + Worker mockWorker = mock(Worker.class); + ((io.temporal.worker.WorkerPlugin) plugin).initializeWorker("test-queue", mockWorker); + + // Verify registrations happen before custom initializer + verify(mockWorker).registerWorkflowImplementationTypes(String.class); + verify(mockWorker).registerActivitiesImplementations(any()); + assertTrue( + "Custom initializer should be called after registrations", customInitializerCalled.get()); + } } From 9760c33e44a1e44e08526ea473a1b6de5cf547a8 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 14:27:06 -0500 Subject: [PATCH 24/28] bug fix --- .../io/temporal/client/WorkflowClientInternalImpl.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 1bea2e3ca4..6f0db96b5b 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -823,10 +823,15 @@ private static WorkflowClientPlugin[] extractClientPlugins( */ private static WorkflowClientPlugin[] mergePlugins( WorkflowClientPlugin[] propagated, WorkflowClientPlugin[] explicit) { - if (propagated == null || propagated.length == 0) { + boolean propagatedEmpty = propagated == null || propagated.length == 0; + boolean explicitEmpty = explicit == null || explicit.length == 0; + if (propagatedEmpty && explicitEmpty) { + return new WorkflowClientPlugin[0]; + } + if (propagatedEmpty) { return explicit; } - if (explicit == null || explicit.length == 0) { + if (explicitEmpty) { return propagated; } WorkflowClientPlugin[] merged = new WorkflowClientPlugin[propagated.length + explicit.length]; From 44227e8def342089bd56b6ba8a85a45a412042bd Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 14:37:14 -0500 Subject: [PATCH 25/28] Add duplicate warnings --- .../client/WorkflowClientInternalImpl.java | 17 +++++++++++++++ .../client/schedules/ScheduleClientImpl.java | 21 +++++++++++++++++++ .../io/temporal/worker/WorkerFactory.java | 15 +++++++++++++ 3 files changed, 53 insertions(+) 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 6f0db96b5b..f651d0fd9f 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -37,9 +37,13 @@ import java.util.stream.StreamSupport; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; final class WorkflowClientInternalImpl implements WorkflowClient, WorkflowClientInternal { + private static final Logger log = LoggerFactory.getLogger(WorkflowClientInternalImpl.class); + private final GenericWorkflowClient genericClient; private final WorkflowClientOptions options; private final ManualActivityCompletionClientFactory manualActivityCompletionClientFactory; @@ -834,6 +838,19 @@ private static WorkflowClientPlugin[] mergePlugins( if (explicitEmpty) { return propagated; } + // Warn about duplicate plugin types + Set> propagatedTypes = new HashSet<>(); + for (WorkflowClientPlugin p : propagated) { + propagatedTypes.add(p.getClass()); + } + for (WorkflowClientPlugin p : explicit) { + if (propagatedTypes.contains(p.getClass())) { + log.warn( + "Plugin type {} is present in both propagated plugins (from service stubs) and " + + "explicit plugins. It may run twice which may not be the intended behavior.", + p.getClass().getName()); + } + } WorkflowClientPlugin[] merged = new WorkflowClientPlugin[propagated.length + explicit.length]; System.arraycopy(propagated, 0, merged, 0, propagated.length); System.arraycopy(explicit, 0, merged, propagated.length, explicit.length); diff --git a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java index 2177b8080f..9f1fbefe8f 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java @@ -15,11 +15,17 @@ import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Stream; import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; final class ScheduleClientImpl implements ScheduleClient { + + private static final Logger log = LoggerFactory.getLogger(ScheduleClientImpl.class); private final WorkflowServiceStubs workflowServiceStubs; private final ScheduleClientOptions options; private final GenericWorkflowClient genericClient; @@ -91,6 +97,21 @@ private static ScheduleClientPlugin[] mergePlugins( && (explicit == null || explicit.length == 0)) { return new ScheduleClientPlugin[0]; } + // Warn about duplicate plugin types + if (propagated != null && propagated.length > 0 && explicit != null && explicit.length > 0) { + Set> propagatedTypes = new HashSet<>(); + for (ScheduleClientPlugin p : propagated) { + propagatedTypes.add(p.getClass()); + } + for (ScheduleClientPlugin p : explicit) { + if (propagatedTypes.contains(p.getClass())) { + log.warn( + "Plugin type {} is present in both propagated plugins (from service stubs) and " + + "explicit plugins. It may run twice which may not be the intended behavior.", + p.getClass().getName()); + } + } + } List merged = new ArrayList<>(); if (propagated != null) { merged.addAll(Arrays.asList(propagated)); 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 b1fb42cd2b..bd611c6e53 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -19,9 +19,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; @@ -535,6 +537,19 @@ private static List mergePlugins( if (explicit == null || explicit.length == 0) { return propagated; } + // Warn about duplicate plugin types + Set> propagatedTypes = new HashSet<>(); + for (WorkerPlugin p : propagated) { + propagatedTypes.add(p.getClass()); + } + for (WorkerPlugin p : explicit) { + if (propagatedTypes.contains(p.getClass())) { + log.warn( + "Plugin type {} is present in both propagated plugins (from client) and " + + "explicit plugins. It may run twice which may not be the intended behavior.", + p.getClass().getName()); + } + } List merged = new ArrayList<>(propagated.size() + explicit.length); merged.addAll(propagated); merged.addAll(Arrays.asList(explicit)); From f1d36420ba6a5bea86ba02837ea4aef42787a293 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Fri, 16 Jan 2026 15:00:40 -0500 Subject: [PATCH 26/28] cleanup tests some --- .../WorkflowClientOptionsPluginTest.java | 22 ------------- .../java/io/temporal/common/PluginTest.java | 32 ------------------- 2 files changed, 54 deletions(-) diff --git a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java index d23317c024..0ae9d22b3d 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/WorkflowClientOptionsPluginTest.java @@ -70,28 +70,6 @@ public void testValidateAndBuildWithDefaults() { assertEquals("plugin", options.getPlugins()[0].getName()); } - @Test - public void testEqualsWithPlugins() { - SimplePlugin plugin = new TestPlugin("plugin"); - - WorkflowClientOptions options1 = WorkflowClientOptions.newBuilder().setPlugins(plugin).build(); - - WorkflowClientOptions options2 = WorkflowClientOptions.newBuilder().setPlugins(plugin).build(); - - assertEquals(options1, options2); - assertEquals(options1.hashCode(), options2.hashCode()); - } - - @Test - public void testToStringWithPlugins() { - SimplePlugin plugin = new TestPlugin("my-plugin"); - - WorkflowClientOptions options = WorkflowClientOptions.newBuilder().setPlugins(plugin).build(); - - String str = options.toString(); - assertTrue("toString should contain plugins", str.contains("plugins")); - } - private static class TestPlugin extends SimplePlugin { TestPlugin(String name) { super(name); diff --git a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java index d9e3967608..018b2ef150 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/PluginTest.java @@ -33,23 +33,6 @@ public class PluginTest { - @Test - public void testSimplePluginName() { - SimplePlugin plugin = new SimplePlugin("test-plugin") {}; - assertEquals("test-plugin", plugin.getName()); - } - - @Test - public void testSimplePluginToString() { - SimplePlugin plugin = new SimplePlugin("my-plugin") {}; - assertTrue(plugin.toString().contains("my-plugin")); - } - - @Test(expected = NullPointerException.class) - public void testSimplePluginNullName() { - new SimplePlugin((String) null) {}; - } - @Test public void testSimplePluginDefaultBehavior() throws Exception { SimplePlugin plugin = new SimplePlugin("test") {}; @@ -254,21 +237,6 @@ public void testShutdownWorkerReverseOrder() { order); } - @Test - public void testSimplePluginImplementsAllInterfaces() { - SimplePlugin plugin = new SimplePlugin("full-plugin") {}; - - assertTrue( - "SimplePlugin should implement WorkflowServiceStubsPlugin", - plugin instanceof io.temporal.serviceclient.WorkflowServiceStubsPlugin); - assertTrue( - "SimplePlugin should implement WorkflowClientPlugin", - plugin instanceof io.temporal.client.WorkflowClientPlugin); - assertTrue( - "SimplePlugin should implement WorkerPlugin", - plugin instanceof io.temporal.worker.WorkerPlugin); - } - private SimplePlugin createTrackingPlugin(String name, List order) { return new SimplePlugin(name) { @Override From 76d048230d9ecb1e45b3849e7dcb0cd5eb9fa061 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Mon, 19 Jan 2026 11:36:49 -0500 Subject: [PATCH 27/28] first stab at replayer --- .../java/io/temporal/common/SimplePlugin.java | 101 ++++++++++ .../main/java/io/temporal/worker/Worker.java | 62 +++++- .../io/temporal/worker/WorkerFactory.java | 88 ++++++++- .../java/io/temporal/worker/WorkerPlugin.java | 72 +++++++ .../common/SimplePluginBuilderTest.java | 179 ++++++++++++++++++ .../testing/TestWorkflowEnvironment.java | 20 ++ .../TestWorkflowEnvironmentInternal.java | 5 + .../io/temporal/testing/WorkflowReplayer.java | 64 ++++++- 8 files changed, 579 insertions(+), 12 deletions(-) diff --git a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java index 7ccc847d5d..369bc0270a 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/common/SimplePlugin.java @@ -123,6 +123,9 @@ public abstract class SimplePlugin private final List> workerShutdownCallbacks; private final List> workerFactoryStartCallbacks; private final List> workerFactoryShutdownCallbacks; + private final List> replayWorkerCustomizers; + private final List> replayWorkerInitializers; + private final List> replayExecutionCallbacks; private final List workerInterceptors; private final List clientInterceptors; private final List contextPropagators; @@ -151,6 +154,9 @@ protected SimplePlugin(@Nonnull String name) { this.workerShutdownCallbacks = Collections.emptyList(); this.workerFactoryStartCallbacks = Collections.emptyList(); this.workerFactoryShutdownCallbacks = Collections.emptyList(); + this.replayWorkerCustomizers = Collections.emptyList(); + this.replayWorkerInitializers = Collections.emptyList(); + this.replayExecutionCallbacks = Collections.emptyList(); this.workerInterceptors = Collections.emptyList(); this.clientInterceptors = Collections.emptyList(); this.contextPropagators = Collections.emptyList(); @@ -180,6 +186,9 @@ protected SimplePlugin(@Nonnull Builder builder) { this.workerShutdownCallbacks = new ArrayList<>(builder.workerShutdownCallbacks); this.workerFactoryStartCallbacks = new ArrayList<>(builder.workerFactoryStartCallbacks); this.workerFactoryShutdownCallbacks = new ArrayList<>(builder.workerFactoryShutdownCallbacks); + this.replayWorkerCustomizers = new ArrayList<>(builder.replayWorkerCustomizers); + this.replayWorkerInitializers = new ArrayList<>(builder.replayWorkerInitializers); + this.replayExecutionCallbacks = new ArrayList<>(builder.replayExecutionCallbacks); this.workerInterceptors = new ArrayList<>(builder.workerInterceptors); this.clientInterceptors = new ArrayList<>(builder.clientInterceptors); this.contextPropagators = new ArrayList<>(builder.contextPropagators); @@ -340,6 +349,41 @@ public void shutdownWorkerFactory(WorkerFactory factory, Runnable next) throws E next.run(); } + @Override + public void configureReplayWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { + if (replayWorkerCustomizers.isEmpty()) { + // Default: delegate to configureWorker + WorkerPlugin.super.configureReplayWorker(taskQueue, builder); + } else { + for (Consumer customizer : replayWorkerCustomizers) { + customizer.accept(builder); + } + } + } + + @Override + public void initializeReplayWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { + if (replayWorkerInitializers.isEmpty()) { + // Default: delegate to initializeWorker + WorkerPlugin.super.initializeReplayWorker(taskQueue, worker); + } else { + for (BiConsumer initializer : replayWorkerInitializers) { + initializer.accept(taskQueue, worker); + } + } + } + + @Override + public void replayWorkflowExecution( + @Nonnull Worker worker, @Nonnull WorkflowExecutionHistory history, @Nonnull Runnable next) + throws Exception { + next.run(); + for (BiConsumer callback : replayExecutionCallbacks) { + callback.accept(worker, history); + } + } + @Override public String toString() { return getClass().getSimpleName() + "{name='" + name + "'}"; @@ -363,6 +407,10 @@ public static final class Builder { private final List> workerShutdownCallbacks = new ArrayList<>(); private final List> workerFactoryStartCallbacks = new ArrayList<>(); private final List> workerFactoryShutdownCallbacks = new ArrayList<>(); + private final List> replayWorkerCustomizers = new ArrayList<>(); + private final List> replayWorkerInitializers = new ArrayList<>(); + private final List> replayExecutionCallbacks = + new ArrayList<>(); private final List workerInterceptors = new ArrayList<>(); private final List clientInterceptors = new ArrayList<>(); private final List contextPropagators = new ArrayList<>(); @@ -561,6 +609,59 @@ public Builder onWorkerFactoryShutdown(@Nonnull Consumer callback return this; } + // ==================== Replay Methods ==================== + + /** + * Adds a customizer for {@link WorkerOptions} that is applied only when creating replay + * workers. If no replay-specific customizers are set, the regular worker customizers are used. + * + *

      Use this when replay workers need different configuration than normal workers. + * + * @param customizer a consumer that modifies the options builder + * @return this builder for chaining + */ + public Builder customizeReplayWorker(@Nonnull Consumer customizer) { + replayWorkerCustomizers.add(Objects.requireNonNull(customizer)); + return this; + } + + /** + * Adds an initializer that is called after a replay worker is created. If no replay-specific + * initializers are set, the regular worker initializers are used. + * + *

      Use this when replay workers need different initialization than normal workers. + * + * @param initializer a consumer that receives the task queue name and worker + * @return this builder for chaining + */ + public Builder initializeReplayWorker(@Nonnull BiConsumer initializer) { + replayWorkerInitializers.add(Objects.requireNonNull(initializer)); + return this; + } + + /** + * Adds a callback that is invoked after a workflow execution is replayed. This can be used for + * logging, metrics, or other observability around replay operations. + * + *

      Example: + * + *

      {@code
      +     * SimplePlugin.newBuilder("my-plugin")
      +     *     .onReplayWorkflowExecution((worker, history) -> {
      +     *         logger.info("Replayed workflow: {}", history.getWorkflowExecution().getWorkflowId());
      +     *     })
      +     *     .build();
      +     * }
      + * + * @param callback a consumer that receives the worker and history after replay completes + * @return this builder for chaining + */ + public Builder onReplayWorkflowExecution( + @Nonnull BiConsumer callback) { + replayExecutionCallbacks.add(Objects.requireNonNull(callback)); + return this; + } + /** * Adds worker interceptors. Interceptors are appended to any existing interceptors in the * configuration. diff --git a/temporal-sdk/src/main/java/io/temporal/worker/Worker.java b/temporal-sdk/src/main/java/io/temporal/worker/Worker.java index d2aaf4728e..c421c29044 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/Worker.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/Worker.java @@ -42,6 +42,7 @@ public final class Worker { private static final Logger log = LoggerFactory.getLogger(Worker.class); private final WorkerOptions options; private final String taskQueue; + private final List plugins; final SyncWorkflowWorker workflowWorker; final SyncActivityWorker activityWorker; final SyncNexusWorker nexusWorker; @@ -67,9 +68,11 @@ public final class Worker { @Nonnull WorkflowExecutorCache cache, boolean useStickyTaskQueue, WorkflowThreadExecutor workflowThreadExecutor, - List contextPropagators) { + List contextPropagators, + @Nonnull List plugins) { Objects.requireNonNull(client, "client should not be null"); + this.plugins = Objects.requireNonNull(plugins, "plugins should not be null"); Preconditions.checkArgument( !Strings.isNullOrEmpty(taskQueue), "taskQueue should not be an empty string"); this.taskQueue = taskQueue; @@ -469,12 +472,57 @@ public String toString() { @SuppressWarnings("deprecation") public void replayWorkflowExecution(io.temporal.internal.common.WorkflowExecutionHistory history) throws Exception { - workflowWorker.queryWorkflowExecution( - history, - WorkflowClient.QUERY_TYPE_REPLAY_ONLY, - String.class, - String.class, - new Object[] {}); + // Convert to public history type for plugin API + WorkflowExecutionHistory publicHistory = + new WorkflowExecutionHistory( + history.getHistory(), history.getWorkflowExecution().getWorkflowId()); + + // Build plugin chain in reverse order (first plugin wraps all others) + // Wrap checked exception in RuntimeException for Runnable compatibility + Runnable chain = + () -> { + try { + workflowWorker.queryWorkflowExecution( + history, + WorkflowClient.QUERY_TYPE_REPLAY_ONLY, + String.class, + String.class, + new Object[] {}); + } catch (Exception e) { + throw new ReplayException(e); + } + }; + + for (int i = plugins.size() - 1; i >= 0; i--) { + WorkerPlugin plugin = plugins.get(i); + Runnable next = chain; + chain = + () -> { + try { + plugin.replayWorkflowExecution(this, publicHistory, next); + } catch (Exception e) { + throw new ReplayException(e); + } + }; + } + + try { + chain.run(); + } catch (ReplayException e) { + throw e.getCause(); + } + } + + /** Internal exception to wrap checked exceptions during replay. */ + private static class ReplayException extends RuntimeException { + ReplayException(Exception cause) { + super(cause); + } + + @Override + public synchronized Exception getCause() { + return (Exception) super.getCause(); + } } /** 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 bd611c6e53..cfff0ab5f5 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -7,6 +7,7 @@ import io.temporal.api.workflowservice.v1.DescribeNamespaceRequest; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.common.Experimental; import io.temporal.common.converter.DataConverter; import io.temporal.internal.client.WorkflowClientInternal; import io.temporal.internal.sync.WorkflowThreadExecutor; @@ -175,7 +176,8 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) { cache, true, workflowThreadExecutor, - workflowClient.getOptions().getContextPropagators()); + workflowClient.getOptions().getContextPropagators(), + plugins); workers.put(taskQueue, worker); // Go through the plugins to call plugin initializeWorker hooks (e.g. register workflows, @@ -194,6 +196,71 @@ public synchronized Worker newWorker(String taskQueue, WorkerOptions options) { } } + /** + * Creates a worker specifically for replay operations. This method should be used when replaying + * workflow histories to ensure plugins receive the replay-specific configuration callbacks. + * + *

      Unlike {@link #newWorker(String, WorkerOptions)}, this method: + * + *

        + *
      • Calls {@link WorkerPlugin#configureReplayWorker} instead of {@link + * WorkerPlugin#configureWorker} + *
      • Calls {@link WorkerPlugin#initializeReplayWorker} instead of {@link + * WorkerPlugin#initializeWorker} + *
      + * + *

      This allows plugins to apply different configuration for replay scenarios (e.g., disabling + * certain interceptors that shouldn't run during replay). + * + * @param taskQueue task queue name for the replay worker + * @param options Options for configuring the replay worker + * @return Worker configured for replay + */ + @Experimental + public synchronized Worker newReplayWorker(String taskQueue, WorkerOptions options) { + Preconditions.checkArgument( + !Strings.isNullOrEmpty(taskQueue), "taskQueue should not be an empty string"); + Preconditions.checkState( + state == State.Initial, + String.format(statusErrorMessage, "create new worker", state.name(), State.Initial.name())); + + // Apply replay-specific plugin configuration to worker options (forward order) + options = applyReplayWorkerPluginConfiguration(taskQueue, options, this.plugins); + + // Only one worker can exist for a task queue + Worker existingWorker = workers.get(taskQueue); + if (existingWorker == null) { + Worker worker = + new Worker( + workflowClient, + taskQueue, + factoryOptions, + options, + metricsScope, + runLocks, + cache, + true, + workflowThreadExecutor, + workflowClient.getOptions().getContextPropagators(), + plugins); + workers.put(taskQueue, worker); + + // Go through the plugins to call plugin initializeReplayWorker hooks (e.g. register + // workflows) + for (WorkerPlugin plugin : plugins) { + plugin.initializeReplayWorker(taskQueue, worker); + } + + return worker; + } else { + log.warn( + "Only one worker can be registered for a task queue, " + + "subsequent calls to WorkerFactory#newReplayWorker with the same task queue are ignored and " + + "initially created worker is returned"); + return existingWorker; + } + } + /** * @param taskQueue task queue name to lookup an existing worker for * @return a worker created previously through {@link #newWorker(String)} for the given task @@ -598,6 +665,25 @@ private static WorkerOptions applyWorkerPluginConfiguration( return builder.build(); } + /** + * Applies replay-specific plugin configuration to worker options. Plugins are called in forward + * (registration) order. + */ + private static WorkerOptions applyReplayWorkerPluginConfiguration( + 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.configureReplayWorker(taskQueue, builder); + } + return builder.build(); + } + enum State { Initial, Started, diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java index 5446645f04..32646914fc 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerPlugin.java @@ -22,6 +22,7 @@ import io.temporal.common.Experimental; import io.temporal.common.SimplePlugin; +import io.temporal.common.WorkflowExecutionHistory; import javax.annotation.Nonnull; /** @@ -218,4 +219,75 @@ void startWorker(@Nonnull String taskQueue, @Nonnull Worker worker, @Nonnull Run */ void shutdownWorkerFactory(@Nonnull WorkerFactory factory, @Nonnull Runnable next) throws Exception; + + // ==================== Replay Methods ==================== + + /** + * Allows the plugin to modify worker options when configuring a worker for replay. Called during + * replay configuration in forward (registration) order. + * + *

      By default, this delegates to {@link #configureWorker}. Override this method if the plugin + * needs replay-specific configuration that differs from normal worker configuration. + * + *

      This is useful when a plugin needs to apply the same settings to replay that it applies to + * normal workers (e.g., data converters, interceptors) to ensure replay behavior matches + * execution behavior. + * + * @param taskQueue the task queue name for the replay worker + * @param builder the options builder to modify + */ + default void configureReplayWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) { + configureWorker(taskQueue, builder); + } + + /** + * Called after a replay worker is created, allowing plugins to register workflows and other + * components needed for replay. + * + *

      By default, this delegates to {@link #initializeWorker}. Override this method if the plugin + * needs replay-specific initialization that differs from normal worker initialization. + * + * @param taskQueue the task queue name for the replay worker + * @param worker the newly created replay worker + */ + default void initializeReplayWorker(@Nonnull String taskQueue, @Nonnull Worker worker) { + initializeWorker(taskQueue, worker); + } + + /** + * Allows the plugin to wrap workflow execution replay. Called in reverse order (first plugin + * wraps all others) when replaying a workflow history. + * + *

      This method allows plugins to perform setup/teardown around replay, add logging, metrics, or + * other observability for replay operations. + * + *

      Example: + * + *

      {@code
      +   * @Override
      +   * public void replayWorkflowExecution(
      +   *     Worker worker, WorkflowExecutionHistory history, Runnable next) throws Exception {
      +   *     logger.info("Replaying workflow: {}", history.getWorkflowExecution().getWorkflowId());
      +   *     long start = System.currentTimeMillis();
      +   *     try {
      +   *         next.run();
      +   *         logger.info("Replay succeeded in {}ms", System.currentTimeMillis() - start);
      +   *     } catch (Exception e) {
      +   *         logger.error("Replay failed after {}ms", System.currentTimeMillis() - start, e);
      +   *         throw e;
      +   *     }
      +   * }
      +   * }
      + * + * @param worker the worker performing the replay + * @param history the workflow execution history being replayed + * @param next runnable that performs the next in chain (eventually performs the actual replay) + * @throws Exception if replay fails + */ + default void replayWorkflowExecution( + @Nonnull Worker worker, @Nonnull WorkflowExecutionHistory history, @Nonnull Runnable next) + throws Exception { + next.run(); + } } diff --git a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java index 3c9a49b86a..60f7d7df3a 100644 --- a/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java +++ b/temporal-sdk/src/test/java/io/temporal/common/SimplePluginBuilderTest.java @@ -33,8 +33,10 @@ import io.temporal.worker.Worker; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; public class SimplePluginBuilderTest { @@ -515,4 +517,181 @@ public void testRegistrationsWithCustomInitializer() { assertTrue( "Custom initializer should be called after registrations", customInitializerCalled.get()); } + + // ==================== Replay Tests ==================== + + @Test + public void testCustomizeReplayWorker() { + AtomicBoolean customized = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .customizeReplayWorker( + builder -> { + customized.set(true); + builder.setMaxConcurrentActivityExecutionSize(25); + }) + .build(); + + WorkerOptions.Builder builder = WorkerOptions.newBuilder(); + ((WorkerPlugin) plugin).configureReplayWorker("test-queue", builder); + + assertTrue("Replay customizer should have been called", customized.get()); + assertEquals(25, builder.build().getMaxConcurrentActivityExecutionSize()); + } + + @Test + public void testCustomizeReplayWorkerDelegatesToConfigureWorkerWhenEmpty() { + AtomicBoolean workerCustomized = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .customizeWorker(builder -> workerCustomized.set(true)) + // No replay customizer set + .build(); + + WorkerOptions.Builder builder = WorkerOptions.newBuilder(); + ((WorkerPlugin) plugin).configureReplayWorker("test-queue", builder); + + assertTrue( + "Should delegate to configureWorker when no replay customizers", workerCustomized.get()); + } + + @Test + public void testCustomizeReplayWorkerDoesNotDelegateWhenSet() { + AtomicBoolean workerCustomized = new AtomicBoolean(false); + AtomicBoolean replayCustomized = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .customizeWorker(builder -> workerCustomized.set(true)) + .customizeReplayWorker(builder -> replayCustomized.set(true)) + .build(); + + WorkerOptions.Builder builder = WorkerOptions.newBuilder(); + ((WorkerPlugin) plugin).configureReplayWorker("test-queue", builder); + + assertFalse( + "Should NOT delegate to configureWorker when replay customizer is set", + workerCustomized.get()); + assertTrue("Replay customizer should be called", replayCustomized.get()); + } + + @Test + public void testInitializeReplayWorker() { + AtomicBoolean initialized = new AtomicBoolean(false); + AtomicReference capturedTaskQueue = new AtomicReference<>(); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .initializeReplayWorker( + (taskQueue, worker) -> { + initialized.set(true); + capturedTaskQueue.set(taskQueue); + }) + .build(); + + ((WorkerPlugin) plugin).initializeReplayWorker("replay-queue", null); + + assertTrue("Replay initializer should have been called", initialized.get()); + assertEquals("replay-queue", capturedTaskQueue.get()); + } + + @Test + public void testInitializeReplayWorkerDelegatesToInitializeWorkerWhenEmpty() { + AtomicBoolean workerInitialized = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .initializeWorker((taskQueue, worker) -> workerInitialized.set(true)) + // No replay initializer set + .build(); + + ((WorkerPlugin) plugin).initializeReplayWorker("test-queue", null); + + assertTrue( + "Should delegate to initializeWorker when no replay initializers", workerInitialized.get()); + } + + @Test + public void testInitializeReplayWorkerDoesNotDelegateWhenSet() { + AtomicBoolean workerInitialized = new AtomicBoolean(false); + AtomicBoolean replayInitialized = new AtomicBoolean(false); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .initializeWorker((taskQueue, worker) -> workerInitialized.set(true)) + .initializeReplayWorker((taskQueue, worker) -> replayInitialized.set(true)) + .build(); + + ((WorkerPlugin) plugin).initializeReplayWorker("test-queue", null); + + assertFalse( + "Should NOT delegate to initializeWorker when replay initializer is set", + workerInitialized.get()); + assertTrue("Replay initializer should be called", replayInitialized.get()); + } + + @Test + public void testOnReplayWorkflowExecution() throws Exception { + AtomicBoolean callbackCalled = new AtomicBoolean(false); + AtomicReference capturedWorker = new AtomicReference<>(); + AtomicReference capturedHistory = new AtomicReference<>(); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .onReplayWorkflowExecution( + (worker, history) -> { + callbackCalled.set(true); + capturedWorker.set(worker); + capturedHistory.set(history); + }) + .build(); + + Worker mockWorker = mock(Worker.class); + WorkflowExecutionHistory mockHistory = mock(WorkflowExecutionHistory.class); + AtomicBoolean nextCalled = new AtomicBoolean(false); + + ((WorkerPlugin) plugin) + .replayWorkflowExecution(mockWorker, mockHistory, () -> nextCalled.set(true)); + + assertTrue("next should be called", nextCalled.get()); + assertTrue("Callback should have been called", callbackCalled.get()); + assertSame(mockWorker, capturedWorker.get()); + assertSame(mockHistory, capturedHistory.get()); + } + + @Test + public void testMultipleOnReplayWorkflowExecutionCallbacks() throws Exception { + AtomicInteger callCount = new AtomicInteger(0); + + SimplePlugin plugin = + SimplePlugin.newBuilder("test") + .onReplayWorkflowExecution((worker, history) -> callCount.incrementAndGet()) + .onReplayWorkflowExecution((worker, history) -> callCount.incrementAndGet()) + .onReplayWorkflowExecution((worker, history) -> callCount.incrementAndGet()) + .build(); + + Worker mockWorker = mock(Worker.class); + WorkflowExecutionHistory mockHistory = mock(WorkflowExecutionHistory.class); + + ((WorkerPlugin) plugin).replayWorkflowExecution(mockWorker, mockHistory, () -> {}); + + assertEquals("All callbacks should be called", 3, callCount.get()); + } + + @Test(expected = NullPointerException.class) + public void testNullCustomizeReplayWorker() { + SimplePlugin.newBuilder("test").customizeReplayWorker(null); + } + + @Test(expected = NullPointerException.class) + public void testNullInitializeReplayWorker() { + SimplePlugin.newBuilder("test").initializeReplayWorker(null); + } + + @Test(expected = NullPointerException.class) + public void testNullOnReplayWorkflowExecution() { + SimplePlugin.newBuilder("test").onReplayWorkflowExecution(null); + } } diff --git a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java index b2dadcf059..29163195e2 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java @@ -101,6 +101,26 @@ static TestWorkflowEnvironment newInstance(TestEnvironmentOptions options) { */ Worker newWorker(String taskQueue, WorkerOptions options); + /** + * Creates a new Worker instance specifically for replay operations. This method should be used + * when replaying workflow histories to ensure plugins receive the replay-specific configuration + * callbacks. + * + *

      Unlike {@link #newWorker(String, WorkerOptions)}, this method: + * + *

        + *
      • Calls {@link io.temporal.worker.WorkerPlugin#configureReplayWorker} instead of {@link + * io.temporal.worker.WorkerPlugin#configureWorker} + *
      • Calls {@link io.temporal.worker.WorkerPlugin#initializeReplayWorker} instead of {@link + * io.temporal.worker.WorkerPlugin#initializeWorker} + *
      + * + * @param taskQueue task queue for the replay worker + * @param options Options for configuring the replay worker + * @return Worker configured for replay + */ + Worker newReplayWorker(String taskQueue, WorkerOptions options); + /** Creates a WorkflowClient that is connected to the in-memory test Temporal service. */ WorkflowClient getWorkflowClient(); diff --git a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java index f11525f15b..2780cc8c40 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java @@ -130,6 +130,11 @@ public Worker newWorker(String taskQueue, WorkerOptions options) { return workerFactory.newWorker(taskQueue, options); } + @Override + public Worker newReplayWorker(String taskQueue, WorkerOptions options) { + return workerFactory.newReplayWorker(taskQueue, options); + } + @Override public WorkflowClient getWorkflowClient() { WorkflowClientOptions options; diff --git a/temporal-testing/src/main/java/io/temporal/testing/WorkflowReplayer.java b/temporal-testing/src/main/java/io/temporal/testing/WorkflowReplayer.java index 31d461fd59..d1d4572181 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/WorkflowReplayer.java +++ b/temporal-testing/src/main/java/io/temporal/testing/WorkflowReplayer.java @@ -5,6 +5,7 @@ import io.temporal.api.taskqueue.v1.TaskQueue; import io.temporal.common.WorkflowExecutionHistory; import io.temporal.worker.Worker; +import io.temporal.worker.WorkerOptions; import java.io.File; /** Replays a workflow given its history. Useful for backwards compatibility testing. */ @@ -114,7 +115,32 @@ public static void replayWorkflowExecution( Class workflowClass, Class... moreWorkflowClasses) throws Exception { - TestWorkflowEnvironment testEnv = TestWorkflowEnvironment.newInstance(); + replayWorkflowExecution( + history, (TestEnvironmentOptions) null, workflowClass, moreWorkflowClasses); + } + + /** + * Replays workflow from a {@link WorkflowExecutionHistory}. RunId must match the one used + * to generate the serialized history. + * + * @param history object that contains the workflow ids and the events. + * @param testEnvironmentOptions options for the test environment, including any plugins to apply. + * If null, default options are used. + * @param workflowClass workflow implementation class to replay + * @param moreWorkflowClasses optional additional workflow implementation classes + * @throws Exception if replay failed for any reason. + */ + @SuppressWarnings("deprecation") + public static void replayWorkflowExecution( + io.temporal.internal.common.WorkflowExecutionHistory history, + TestEnvironmentOptions testEnvironmentOptions, + Class workflowClass, + Class... moreWorkflowClasses) + throws Exception { + TestWorkflowEnvironment testEnv = + testEnvironmentOptions != null + ? TestWorkflowEnvironment.newInstance(testEnvironmentOptions) + : TestWorkflowEnvironment.newInstance(); try { replayWorkflowExecution(history, testEnv, workflowClass, moreWorkflowClasses); } finally { @@ -138,7 +164,9 @@ public static void replayWorkflowExecution( Class workflowClass, Class... moreWorkflowClasses) throws Exception { - Worker worker = testWorkflowEnvironment.newWorker(getQueueName((history))); + Worker worker = + testWorkflowEnvironment.newReplayWorker( + getQueueName((history)), WorkerOptions.newBuilder().build()); worker.registerWorkflowImplementationTypes( ObjectArrays.concat(moreWorkflowClasses, workflowClass)); replayWorkflowExecution(history, worker); @@ -174,8 +202,36 @@ public static ReplayResults replayWorkflowExecutions( boolean failFast, Class... workflowClasses) throws Exception { - try (TestWorkflowEnvironment testEnv = TestWorkflowEnvironment.newInstance()) { - Worker worker = testEnv.newWorker("replay-task-queue-name"); + return replayWorkflowExecutions( + histories, failFast, (TestEnvironmentOptions) null, workflowClasses); + } + + /** + * Replays workflows provided by an iterable. + * + * @param histories The histories to be replayed + * @param failFast If true, throws upon the first error encountered (if any) during replay. If + * false, all histories will be replayed and the returned object contains information about + * any failures. + * @param testEnvironmentOptions options for the test environment, including any plugins to apply. + * If null, default options are used. + * @param workflowClasses workflow implementation classes to register + * @return If `failFast` is false, contains any replay failures encountered. + * @throws Exception If replay failed and `failFast` is true. + */ + @SuppressWarnings("deprecation") + public static ReplayResults replayWorkflowExecutions( + Iterable histories, + boolean failFast, + TestEnvironmentOptions testEnvironmentOptions, + Class... workflowClasses) + throws Exception { + try (TestWorkflowEnvironment testEnv = + testEnvironmentOptions != null + ? TestWorkflowEnvironment.newInstance(testEnvironmentOptions) + : TestWorkflowEnvironment.newInstance()) { + Worker worker = + testEnv.newReplayWorker("replay-task-queue-name", WorkerOptions.newBuilder().build()); worker.registerWorkflowImplementationTypes(workflowClasses); return replayWorkflowExecutions(histories, failFast, worker); } From e8ed0c1570f50dce135a9bd9a61198264659bab9 Mon Sep 17 00:00:00 2001 From: Donald Pinckney Date: Tue, 20 Jan 2026 15:55:37 -0500 Subject: [PATCH 28/28] warn on duplicant *instances* not *types* --- .../client/WorkflowClientInternalImpl.java | 15 ++++++--------- .../client/schedules/ScheduleClientImpl.java | 15 ++++++--------- .../java/io/temporal/worker/WorkerFactory.java | 15 ++++++--------- 3 files changed, 18 insertions(+), 27 deletions(-) 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 f651d0fd9f..489bab71c3 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -838,17 +838,14 @@ private static WorkflowClientPlugin[] mergePlugins( if (explicitEmpty) { return propagated; } - // Warn about duplicate plugin types - Set> propagatedTypes = new HashSet<>(); - for (WorkflowClientPlugin p : propagated) { - propagatedTypes.add(p.getClass()); - } + // Warn about duplicate plugin instances (same object in both lists) + Set propagatedSet = new HashSet<>(Arrays.asList(propagated)); for (WorkflowClientPlugin p : explicit) { - if (propagatedTypes.contains(p.getClass())) { + if (propagatedSet.contains(p)) { log.warn( - "Plugin type {} is present in both propagated plugins (from service stubs) and " - + "explicit plugins. It may run twice which may not be the intended behavior.", - p.getClass().getName()); + "Plugin instance {} is present in both propagated plugins (from service stubs) and " + + "explicit plugins. It will run twice which may not be the intended behavior.", + p.getName()); } } WorkflowClientPlugin[] merged = new WorkflowClientPlugin[propagated.length + explicit.length]; diff --git a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java index 9f1fbefe8f..8784cf6a95 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/schedules/ScheduleClientImpl.java @@ -97,18 +97,15 @@ private static ScheduleClientPlugin[] mergePlugins( && (explicit == null || explicit.length == 0)) { return new ScheduleClientPlugin[0]; } - // Warn about duplicate plugin types + // Warn about duplicate plugin instances (same object in both lists) if (propagated != null && propagated.length > 0 && explicit != null && explicit.length > 0) { - Set> propagatedTypes = new HashSet<>(); - for (ScheduleClientPlugin p : propagated) { - propagatedTypes.add(p.getClass()); - } + Set propagatedSet = new HashSet<>(Arrays.asList(propagated)); for (ScheduleClientPlugin p : explicit) { - if (propagatedTypes.contains(p.getClass())) { + if (propagatedSet.contains(p)) { log.warn( - "Plugin type {} is present in both propagated plugins (from service stubs) and " - + "explicit plugins. It may run twice which may not be the intended behavior.", - p.getClass().getName()); + "Plugin instance {} is present in both propagated plugins (from service stubs) and " + + "explicit plugins. It will run twice which may not be the intended behavior.", + p.getName()); } } } 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 cfff0ab5f5..ce5444023d 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerFactory.java @@ -604,17 +604,14 @@ private static List mergePlugins( if (explicit == null || explicit.length == 0) { return propagated; } - // Warn about duplicate plugin types - Set> propagatedTypes = new HashSet<>(); - for (WorkerPlugin p : propagated) { - propagatedTypes.add(p.getClass()); - } + // Warn about duplicate plugin instances (same object in both lists) + Set propagatedSet = new HashSet<>(propagated); for (WorkerPlugin p : explicit) { - if (propagatedTypes.contains(p.getClass())) { + if (propagatedSet.contains(p)) { log.warn( - "Plugin type {} is present in both propagated plugins (from client) and " - + "explicit plugins. It may run twice which may not be the intended behavior.", - p.getClass().getName()); + "Plugin instance {} is present in both propagated plugins (from client) and " + + "explicit plugins. It will run twice which may not be the intended behavior.", + p.getName()); } } List merged = new ArrayList<>(propagated.size() + explicit.length);