Skip to content

Commit 67d56dd

Browse files
Plugin Support for Java SDK (#2761)
Adds support for plugins to the Java SDK!
1 parent f5df5c4 commit 67d56dd

19 files changed

+2930
-31
lines changed

temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import io.temporal.internal.client.external.GenericWorkflowClient;
2222
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
2323
import io.temporal.internal.client.external.ManualActivityCompletionClientFactory;
24+
import io.temporal.internal.common.PluginUtils;
2425
import io.temporal.internal.sync.StubMarker;
2526
import io.temporal.serviceclient.MetricsTag;
2627
import io.temporal.serviceclient.WorkflowServiceStubs;
28+
import io.temporal.serviceclient.WorkflowServiceStubsPlugin;
2729
import io.temporal.worker.WorkerFactory;
2830
import io.temporal.workflow.*;
2931
import java.lang.annotation.Annotation;
@@ -36,9 +38,13 @@
3638
import java.util.stream.StreamSupport;
3739
import javax.annotation.Nonnull;
3840
import javax.annotation.Nullable;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
3943

4044
final class WorkflowClientInternalImpl implements WorkflowClient, WorkflowClientInternal {
4145

46+
private static final Logger log = LoggerFactory.getLogger(WorkflowClientInternalImpl.class);
47+
4248
private final GenericWorkflowClient genericClient;
4349
private final WorkflowClientOptions options;
4450
private final ManualActivityCompletionClientFactory manualActivityCompletionClientFactory;
@@ -65,7 +71,29 @@ public static WorkflowClient newInstance(
6571

6672
WorkflowClientInternalImpl(
6773
WorkflowServiceStubs workflowServiceStubs, WorkflowClientOptions options) {
68-
options = WorkflowClientOptions.newBuilder(options).validateAndBuildWithDefaults();
74+
// Extract WorkflowClientPlugins from service stubs plugins (propagation)
75+
WorkflowClientPlugin[] propagatedPlugins =
76+
extractClientPlugins(workflowServiceStubs.getOptions().getPlugins());
77+
78+
// Merge propagated plugins with client-specified plugins
79+
WorkflowClientPlugin[] mergedPlugins =
80+
PluginUtils.mergePlugins(
81+
propagatedPlugins,
82+
options.getPlugins(),
83+
WorkflowClientPlugin::getName,
84+
log,
85+
"service stubs",
86+
WorkflowClientPlugin.class);
87+
88+
// Apply plugin configuration phase (forward order) on user-provided options,
89+
// so plugins see unmodified state before defaults and plugin merging
90+
WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options);
91+
for (WorkflowClientPlugin plugin : mergedPlugins) {
92+
plugin.configureWorkflowClient(builder);
93+
}
94+
// Set merged plugins after configuration, then validate
95+
builder.setPlugins(mergedPlugins);
96+
options = builder.validateAndBuildWithDefaults();
6997
workflowServiceStubs =
7098
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
7199
this.options = options;
@@ -771,4 +799,23 @@ public NexusStartWorkflowResponse startNexus(
771799
WorkflowInvocationHandler.closeAsyncInvocation();
772800
}
773801
}
802+
803+
/**
804+
* Extracts WorkflowClientPlugins from service stubs plugins. Only plugins that also implement
805+
* {@link WorkflowClientPlugin} are included. This enables plugin propagation from service stubs
806+
* to workflow client.
807+
*/
808+
private static WorkflowClientPlugin[] extractClientPlugins(
809+
WorkflowServiceStubsPlugin[] stubsPlugins) {
810+
if (stubsPlugins == null || stubsPlugins.length == 0) {
811+
return new WorkflowClientPlugin[0];
812+
}
813+
List<WorkflowClientPlugin> clientPlugins = new ArrayList<>();
814+
for (WorkflowServiceStubsPlugin plugin : stubsPlugins) {
815+
if (plugin instanceof WorkflowClientPlugin) {
816+
clientPlugins.add((WorkflowClientPlugin) plugin);
817+
}
818+
}
819+
return clientPlugins.toArray(new WorkflowClientPlugin[0]);
820+
}
774821
}

temporal-sdk/src/main/java/io/temporal/client/WorkflowClientOptions.java

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.temporal.client;
22

33
import io.temporal.api.enums.v1.QueryRejectCondition;
4+
import io.temporal.common.Experimental;
45
import io.temporal.common.context.ContextPropagator;
56
import io.temporal.common.converter.DataConverter;
67
import io.temporal.common.converter.GlobalDataConverter;
@@ -47,6 +48,7 @@ public static final class Builder {
4748
private String binaryChecksum;
4849
private List<ContextPropagator> contextPropagators;
4950
private QueryRejectCondition queryRejectCondition;
51+
private WorkflowClientPlugin[] plugins;
5052

5153
private Builder() {}
5254

@@ -61,6 +63,7 @@ private Builder(WorkflowClientOptions options) {
6163
binaryChecksum = options.binaryChecksum;
6264
contextPropagators = options.contextPropagators;
6365
queryRejectCondition = options.queryRejectCondition;
66+
plugins = options.plugins;
6467
}
6568

6669
public Builder setNamespace(String namespace) {
@@ -132,6 +135,24 @@ public Builder setQueryRejectCondition(QueryRejectCondition queryRejectCondition
132135
return this;
133136
}
134137

138+
/**
139+
* Sets the workflow client plugins to use with this client. Plugins can modify client
140+
* configuration.
141+
*
142+
* <p>Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically
143+
* propagated to workers created from this client.
144+
*
145+
* @param plugins the workflow client plugins to use
146+
* @return this builder for chaining
147+
* @see WorkflowClientPlugin
148+
* @see io.temporal.worker.WorkerPlugin
149+
*/
150+
@Experimental
151+
public Builder setPlugins(WorkflowClientPlugin... plugins) {
152+
this.plugins = Objects.requireNonNull(plugins);
153+
return this;
154+
}
155+
135156
public WorkflowClientOptions build() {
136157
return new WorkflowClientOptions(
137158
namespace,
@@ -140,9 +161,21 @@ public WorkflowClientOptions build() {
140161
identity,
141162
binaryChecksum,
142163
contextPropagators,
143-
queryRejectCondition);
164+
queryRejectCondition,
165+
plugins == null ? EMPTY_PLUGINS : plugins);
144166
}
145167

168+
/**
169+
* Validates options and builds with defaults applied.
170+
*
171+
* <p>Note: If plugins are configured via {@link #setPlugins(WorkflowClientPlugin...)}, they
172+
* will have an opportunity to modify options after this method is called, when the options are
173+
* passed to {@link WorkflowClient#newInstance}. This means validation performed here occurs
174+
* before plugin modifications. In most cases, users should simply call {@link #build()} and let
175+
* the client creation handle validation.
176+
*
177+
* @return validated options with defaults applied
178+
*/
146179
public WorkflowClientOptions validateAndBuildWithDefaults() {
147180
String name = identity == null ? ManagementFactory.getRuntimeMXBean().getName() : identity;
148181
return new WorkflowClientOptions(
@@ -154,7 +187,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() {
154187
contextPropagators == null ? EMPTY_CONTEXT_PROPAGATORS : contextPropagators,
155188
queryRejectCondition == null
156189
? QueryRejectCondition.QUERY_REJECT_CONDITION_UNSPECIFIED
157-
: queryRejectCondition);
190+
: queryRejectCondition,
191+
plugins == null ? EMPTY_PLUGINS : plugins);
158192
}
159193
}
160194

@@ -163,6 +197,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() {
163197

164198
private static final List<ContextPropagator> EMPTY_CONTEXT_PROPAGATORS = Collections.emptyList();
165199

200+
private static final WorkflowClientPlugin[] EMPTY_PLUGINS = new WorkflowClientPlugin[0];
201+
166202
private final String namespace;
167203

168204
private final DataConverter dataConverter;
@@ -177,21 +213,25 @@ public WorkflowClientOptions validateAndBuildWithDefaults() {
177213

178214
private final QueryRejectCondition queryRejectCondition;
179215

216+
private final WorkflowClientPlugin[] plugins;
217+
180218
private WorkflowClientOptions(
181219
String namespace,
182220
DataConverter dataConverter,
183221
WorkflowClientInterceptor[] interceptors,
184222
String identity,
185223
String binaryChecksum,
186224
List<ContextPropagator> contextPropagators,
187-
QueryRejectCondition queryRejectCondition) {
225+
QueryRejectCondition queryRejectCondition,
226+
WorkflowClientPlugin[] plugins) {
188227
this.namespace = namespace;
189228
this.dataConverter = dataConverter;
190229
this.interceptors = interceptors;
191230
this.identity = identity;
192231
this.binaryChecksum = binaryChecksum;
193232
this.contextPropagators = contextPropagators;
194233
this.queryRejectCondition = queryRejectCondition;
234+
this.plugins = plugins;
195235
}
196236

197237
/**
@@ -236,6 +276,19 @@ public QueryRejectCondition getQueryRejectCondition() {
236276
return queryRejectCondition;
237277
}
238278

279+
/**
280+
* Returns the workflow client plugins configured for this client.
281+
*
282+
* <p>Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically
283+
* propagated to workers created from this client.
284+
*
285+
* @return the array of workflow client plugins, never null
286+
*/
287+
@Experimental
288+
public WorkflowClientPlugin[] getPlugins() {
289+
return plugins;
290+
}
291+
239292
@Override
240293
public String toString() {
241294
return "WorkflowClientOptions{"
@@ -256,6 +309,8 @@ public String toString() {
256309
+ contextPropagators
257310
+ ", queryRejectCondition="
258311
+ queryRejectCondition
312+
+ ", plugins="
313+
+ Arrays.toString(plugins)
259314
+ '}';
260315
}
261316

@@ -270,7 +325,8 @@ public boolean equals(Object o) {
270325
&& com.google.common.base.Objects.equal(identity, that.identity)
271326
&& com.google.common.base.Objects.equal(binaryChecksum, that.binaryChecksum)
272327
&& com.google.common.base.Objects.equal(contextPropagators, that.contextPropagators)
273-
&& queryRejectCondition == that.queryRejectCondition;
328+
&& queryRejectCondition == that.queryRejectCondition
329+
&& Arrays.equals(plugins, that.plugins);
274330
}
275331

276332
@Override
@@ -282,6 +338,7 @@ public int hashCode() {
282338
identity,
283339
binaryChecksum,
284340
contextPropagators,
285-
queryRejectCondition);
341+
queryRejectCondition,
342+
Arrays.hashCode(plugins));
286343
}
287344
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
3+
*
4+
* Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this material except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package io.temporal.client;
22+
23+
import io.temporal.common.Experimental;
24+
import io.temporal.common.SimplePlugin;
25+
import javax.annotation.Nonnull;
26+
27+
/**
28+
* Plugin interface for customizing Temporal workflow client configuration.
29+
*
30+
* <p>This interface is separate from {@link
31+
* io.temporal.serviceclient.WorkflowServiceStubs.ServiceStubsPlugin} to allow plugins that only
32+
* need to configure the workflow client without affecting the underlying gRPC connection.
33+
*
34+
* <p>Plugins that implement both {@code ServiceStubsPlugin} and {@code WorkflowClientPlugin} will
35+
* have their service stubs configuration applied when creating the service stubs, and their client
36+
* configuration applied when creating the workflow client.
37+
*
38+
* <p>Plugins that also implement {@link io.temporal.worker.WorkerPlugin} are automatically
39+
* propagated from the client to workers created from that client.
40+
*
41+
* <p>Example implementation:
42+
*
43+
* <pre>{@code
44+
* public class LoggingPlugin extends SimplePlugin {
45+
* public LoggingPlugin() {
46+
* super("my-org.logging");
47+
* }
48+
*
49+
* @Override
50+
* public void configureClient(WorkflowClientOptions.Builder builder) {
51+
* // Add custom interceptor
52+
* builder.setInterceptors(new LoggingInterceptor());
53+
* }
54+
* }
55+
* }</pre>
56+
*
57+
* @see io.temporal.serviceclient.WorkflowServiceStubs.ServiceStubsPlugin
58+
* @see io.temporal.worker.WorkerPlugin
59+
* @see SimplePlugin
60+
*/
61+
@Experimental
62+
public interface WorkflowClientPlugin {
63+
64+
/**
65+
* Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended
66+
* format: "organization.plugin-name" (e.g., "io.temporal.tracing")
67+
*
68+
* @return fully qualified plugin name
69+
*/
70+
@Nonnull
71+
String getName();
72+
73+
/**
74+
* Allows the plugin to modify workflow client options before the client is created. Called during
75+
* configuration phase in forward (registration) order.
76+
*
77+
* @param builder the options builder to modify
78+
*/
79+
void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder);
80+
}

0 commit comments

Comments
 (0)