Skip to content

Commit c42400f

Browse files
committed
Add plugin hooks for activity, Nexus, and worker factory options
1 parent a5c353b commit c42400f

21 files changed

Lines changed: 654 additions & 69 deletions

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ to docs, or any other relevant information.
3131
to the given name.
3232
- Added experimental `WorkflowUnsafe.isReadOnly()`, which reports whether the calling code is read-only.
3333
replay.
34+
- Added experimental `ActivityClientPlugin` and `NexusClientPlugin`, which plugins on the service stubs use to configure
35+
`ActivityClient` and `NexusClient`. `SimplePlugin` implements both, and its builder gained
36+
`addScheduleClientInterceptors`, `addActivityClientInterceptors`, and `addNexusClientInterceptors`.
3437

3538
### Changed
3639
- Release notes for all future releases are now in a single CHANGELOG.md file. `releases` directory with old release
@@ -43,6 +46,8 @@ to docs, or any other relevant information.
4346
- `TemporalDevServer`, `TemporalDevServerOptions`, `TestWorkflowEnvironment.startLocal`, and the `useDevServer` methods
4447
of `TestWorkflowRule` and `TestWorkflowExtension` are no longer marked experimental. `TemporalDevServerOptions` remain
4548
specific to the Temporal CLI dev server implementation and may no longer be valid if that implementation changes.
49+
- `SimplePlugin` now applies its data converter customizer to schedule and Nexus clients, and its context
50+
propagators to schedule and activity clients. Previously `SimplePlugin.configureScheduleClient` did nothing.
4651

4752
### Fixed
4853
- Kotlin `Async` method references compiled with Kotlin 2.4 now retain their inline behavior,

‎temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.temporal.internal.client.RootActivityClientInvoker;
1212
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
1313
import io.temporal.internal.client.external.ManualActivityCompletionClientFactory;
14+
import io.temporal.internal.common.PluginUtils;
1415
import io.temporal.internal.util.MethodExtractor;
1516
import io.temporal.serviceclient.MetricsTag;
1617
import io.temporal.serviceclient.WorkflowServiceStubs;
@@ -23,20 +24,51 @@
2324
import java.util.Map;
2425
import java.util.stream.Stream;
2526
import javax.annotation.Nullable;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2629

2730
/**
2831
* Implementation of {@link ActivityClient} that delegates calls through the activity interceptor
2932
* chain and ultimately to the Temporal service.
3033
*/
3134
class ActivityClientImpl implements ActivityClient, ActivityClientInternal {
3235

36+
private static final Logger log = LoggerFactory.getLogger(ActivityClientImpl.class);
37+
3338
private final WorkflowServiceStubs stubs;
3439
private final ActivityClientOptions options;
3540
private final ActivityClientCallsInterceptor invoker;
3641
private final ManualActivityCompletionClientFactory manualActivityCompletionClientFactory;
3742
private final Scope metricsScope;
3843

3944
ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) {
45+
// Extract the ActivityClientPlugins propagated from the service stubs plugins.
46+
ActivityClientPlugin[] propagatedPlugins =
47+
PluginUtils.extractPlugins(
48+
stubs.getOptions().getPlugins(),
49+
ActivityClientPlugin.class,
50+
ActivityClientPlugin[]::new);
51+
52+
// Merge propagated plugins with activity client-specified plugins.
53+
ActivityClientPlugin[] mergedPlugins =
54+
PluginUtils.mergePlugins(
55+
propagatedPlugins,
56+
options.getPlugins(),
57+
ActivityClientPlugin::getName,
58+
log,
59+
"service stubs",
60+
ActivityClientPlugin.class);
61+
62+
// Apply plugin configuration phase (forward order) on user-provided options,
63+
// so plugins see unmodified state before defaults and plugin merging.
64+
ActivityClientOptions.Builder builder = ActivityClientOptions.newBuilder(options);
65+
for (ActivityClientPlugin plugin : mergedPlugins) {
66+
plugin.configureActivityClient(builder);
67+
}
68+
// Set merged plugins after configuration, then build.
69+
builder.setPlugins(mergedPlugins);
70+
options = builder.build();
71+
4072
this.stubs = stubs;
4173
this.options = options;
4274
this.metricsScope =

‎temporal-sdk/src/main/java/io/temporal/client/ActivityClientOptions.java‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.temporal.client;
22

3+
import io.temporal.common.Experimental;
34
import io.temporal.common.context.ContextPropagator;
45
import io.temporal.common.converter.DataConverter;
56
import io.temporal.common.converter.GlobalDataConverter;
67
import io.temporal.common.interceptors.ActivityClientInterceptor;
78
import java.lang.management.ManagementFactory;
9+
import java.util.Arrays;
810
import java.util.Collections;
911
import java.util.List;
1012
import java.util.Objects;
@@ -40,12 +42,14 @@ public static final class Builder {
4042
Collections.emptyList();
4143
private static final List<ActivityClientInterceptor> EMPTY_INTERCEPTORS =
4244
Collections.emptyList();
45+
private static final ActivityClientPlugin[] EMPTY_PLUGINS = new ActivityClientPlugin[0];
4346

4447
private String namespace;
4548
private DataConverter dataConverter;
4649
private String identity;
4750
private List<ContextPropagator> contextPropagators;
4851
private List<ActivityClientInterceptor> interceptors;
52+
private ActivityClientPlugin[] plugins;
4953

5054
private Builder() {}
5155

@@ -58,6 +62,7 @@ private Builder(ActivityClientOptions options) {
5862
identity = options.identity;
5963
contextPropagators = options.contextPropagators;
6064
interceptors = options.interceptors;
65+
plugins = options.plugins;
6166
}
6267

6368
/** Set the namespace this client will operate on. */
@@ -102,14 +107,26 @@ public Builder setInterceptors(List<ActivityClientInterceptor> interceptors) {
102107
return this;
103108
}
104109

110+
/**
111+
* Set the plugins for this client.
112+
*
113+
* @param plugins specifies the plugins to use with the client.
114+
*/
115+
@Experimental
116+
public Builder setPlugins(ActivityClientPlugin... plugins) {
117+
this.plugins = Objects.requireNonNull(plugins);
118+
return this;
119+
}
120+
105121
public ActivityClientOptions build() {
106122
String name = identity == null ? ManagementFactory.getRuntimeMXBean().getName() : identity;
107123
return new ActivityClientOptions(
108124
namespace == null ? DEFAULT_NAMESPACE : namespace,
109125
dataConverter == null ? GlobalDataConverter.get() : dataConverter,
110126
name,
111127
contextPropagators == null ? EMPTY_CONTEXT_PROPAGATORS : contextPropagators,
112-
interceptors == null ? EMPTY_INTERCEPTORS : interceptors);
128+
interceptors == null ? EMPTY_INTERCEPTORS : interceptors,
129+
plugins == null ? EMPTY_PLUGINS : plugins);
113130
}
114131
}
115132

@@ -118,18 +135,21 @@ public ActivityClientOptions build() {
118135
private final String identity;
119136
private final List<ContextPropagator> contextPropagators;
120137
private final List<ActivityClientInterceptor> interceptors;
138+
private final ActivityClientPlugin[] plugins;
121139

122140
private ActivityClientOptions(
123141
String namespace,
124142
DataConverter dataConverter,
125143
String identity,
126144
List<ContextPropagator> contextPropagators,
127-
List<ActivityClientInterceptor> interceptors) {
145+
List<ActivityClientInterceptor> interceptors,
146+
ActivityClientPlugin[] plugins) {
128147
this.namespace = namespace;
129148
this.dataConverter = dataConverter;
130149
this.identity = identity;
131150
this.contextPropagators = contextPropagators;
132151
this.interceptors = interceptors;
152+
this.plugins = plugins;
133153
}
134154

135155
/**
@@ -177,6 +197,16 @@ public List<ActivityClientInterceptor> getInterceptors() {
177197
return interceptors;
178198
}
179199

200+
/**
201+
* Get the plugins of this client.
202+
*
203+
* @return The plugins to use with the client.
204+
*/
205+
@Experimental
206+
public ActivityClientPlugin[] getPlugins() {
207+
return plugins;
208+
}
209+
180210
@Override
181211
public boolean equals(Object o) {
182212
if (this == o) return true;
@@ -186,12 +216,19 @@ public boolean equals(Object o) {
186216
&& Objects.equals(dataConverter, that.dataConverter)
187217
&& Objects.equals(identity, that.identity)
188218
&& Objects.equals(contextPropagators, that.contextPropagators)
189-
&& Objects.equals(interceptors, that.interceptors);
219+
&& Objects.equals(interceptors, that.interceptors)
220+
&& Arrays.equals(plugins, that.plugins);
190221
}
191222

192223
@Override
193224
public int hashCode() {
194-
return Objects.hash(namespace, dataConverter, identity, contextPropagators, interceptors);
225+
return Objects.hash(
226+
namespace,
227+
dataConverter,
228+
identity,
229+
contextPropagators,
230+
interceptors,
231+
Arrays.hashCode(plugins));
195232
}
196233

197234
@Override
@@ -209,6 +246,8 @@ public String toString() {
209246
+ contextPropagators
210247
+ ", interceptors="
211248
+ interceptors
249+
+ ", plugins="
250+
+ Arrays.toString(plugins)
212251
+ '}';
213252
}
214253
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.temporal.client;
2+
3+
import io.temporal.common.Experimental;
4+
import javax.annotation.Nonnull;
5+
6+
/**
7+
* Plugin interface for customizing Temporal activity client configuration.
8+
*
9+
* <p>Plugins that implement both {@link io.temporal.serviceclient.WorkflowServiceStubsPlugin} and
10+
* {@code ActivityClientPlugin} are automatically propagated from the service stubs to the activity
11+
* client.
12+
*
13+
* @see io.temporal.serviceclient.WorkflowServiceStubsPlugin
14+
*/
15+
@Experimental
16+
public interface ActivityClientPlugin {
17+
18+
/**
19+
* Returns a unique name for this plugin. Used for logging and duplicate detection. Recommended
20+
* format: "organization.plugin-name" (e.g., "io.temporal.tracing")
21+
*
22+
* @return fully qualified plugin name
23+
*/
24+
@Nonnull
25+
String getName();
26+
27+
/**
28+
* Allows the plugin to modify activity client options before the client is created. Called during
29+
* configuration phase in forward (registration) order.
30+
*
31+
* @param builder the options builder to modify
32+
*/
33+
void configureActivityClient(@Nonnull ActivityClientOptions.Builder builder);
34+
}

‎temporal-sdk/src/main/java/io/temporal/client/NexusClientImpl.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.temporal.internal.client.RootNexusClientInvoker;
1818
import io.temporal.internal.client.external.GenericWorkflowClient;
1919
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
20+
import io.temporal.internal.common.PluginUtils;
2021
import io.temporal.serviceclient.MetricsTag;
2122
import io.temporal.serviceclient.WorkflowServiceStubs;
2223
import java.util.List;
@@ -39,6 +40,32 @@ public class NexusClientImpl implements NexusClient {
3940

4041
public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOptions options) {
4142
enforceNonWorkflowThread();
43+
44+
// Extract the NexusClientPlugins propagated from the service stubs plugins.
45+
NexusClientPlugin[] propagatedPlugins =
46+
PluginUtils.extractPlugins(
47+
service.getOptions().getPlugins(), NexusClientPlugin.class, NexusClientPlugin[]::new);
48+
49+
// Merge propagated plugins with Nexus client-specified plugins.
50+
NexusClientPlugin[] mergedPlugins =
51+
PluginUtils.mergePlugins(
52+
propagatedPlugins,
53+
options.getPlugins(),
54+
NexusClientPlugin::getName,
55+
log,
56+
"service stubs",
57+
NexusClientPlugin.class);
58+
59+
// Apply plugin configuration phase (forward order) on user-provided options,
60+
// so plugins see unmodified state before defaults and plugin merging.
61+
NexusClientOptions.Builder builder = NexusClientOptions.newBuilder(options);
62+
for (NexusClientPlugin plugin : mergedPlugins) {
63+
plugin.configureNexusClient(builder);
64+
}
65+
// Set merged plugins after configuration, then build.
66+
builder.setPlugins(mergedPlugins);
67+
options = builder.build();
68+
4269
return WorkflowThreadMarker.protectFromWorkflowThread(
4370
new NexusClientImpl(service, options.toResolvedOptions()), NexusClient.class);
4471
}

‎temporal-sdk/src/main/java/io/temporal/client/NexusClientOptions.java‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.lang.management.ManagementFactory;
1212
import java.util.Collections;
1313
import java.util.List;
14+
import java.util.Objects;
1415
import javax.annotation.Nullable;
1516

1617
/**
@@ -42,18 +43,21 @@ public class NexusClientOptions {
4243
private final DataConverter dataConverter;
4344
private final String identity;
4445
private final @Nullable ExternalStorage externalStorage;
46+
private final NexusClientPlugin[] plugins;
4547

4648
private NexusClientOptions(
4749
String namespace,
4850
List<NexusClientInterceptor> interceptors,
4951
DataConverter dataConverter,
5052
String identity,
51-
@Nullable ExternalStorage externalStorage) {
53+
@Nullable ExternalStorage externalStorage,
54+
NexusClientPlugin[] plugins) {
5255
this.namespace = namespace;
5356
this.interceptors = interceptors;
5457
this.dataConverter = dataConverter;
5558
this.identity = identity;
5659
this.externalStorage = externalStorage;
60+
this.plugins = plugins;
5761
}
5862

5963
/** Get the namespace this client will operate on. */
@@ -88,6 +92,11 @@ public String getIdentity() {
8892
return identity;
8993
}
9094

95+
/** Get the plugins of this client. */
96+
public NexusClientPlugin[] getPlugins() {
97+
return plugins;
98+
}
99+
91100
/**
92101
* Converts this {@link NexusClientOptions} instance into a {@link NexusClientResolvedOptions}
93102
* instance, which contains the fully resolved runtime settings used by the internal Nexus client.
@@ -130,11 +139,14 @@ public static NexusClientOptions getDefaultInstance() {
130139

131140
/** Builder for {@link NexusClientOptions}. */
132141
public static class Builder {
142+
private static final NexusClientPlugin[] EMPTY_PLUGINS = new NexusClientPlugin[0];
143+
133144
private String namespace;
134145
private List<NexusClientInterceptor> interceptors = Collections.emptyList();
135146
private DataConverter dataConverter = GlobalDataConverter.get();
136147
private String identity;
137148
private ExternalStorage externalStorage;
149+
private NexusClientPlugin[] plugins;
138150

139151
private Builder() {}
140152

@@ -147,6 +159,7 @@ private Builder(NexusClientOptions options) {
147159
dataConverter = options.dataConverter;
148160
identity = options.identity;
149161
externalStorage = options.externalStorage;
162+
plugins = options.plugins;
150163
}
151164

152165
/** Set the namespace this client will operate on. */
@@ -195,6 +208,16 @@ public NexusClientOptions.Builder setExternalStorage(
195208
return this;
196209
}
197210

211+
/**
212+
* Set the plugins for this client.
213+
*
214+
* @param plugins specifies the plugins to use with the client.
215+
*/
216+
public NexusClientOptions.Builder setPlugins(NexusClientPlugin... plugins) {
217+
this.plugins = Objects.requireNonNull(plugins);
218+
return this;
219+
}
220+
198221
public NexusClientOptions build() {
199222
String resolvedIdentity =
200223
identity == null ? ManagementFactory.getRuntimeMXBean().getName() : identity;
@@ -203,7 +226,8 @@ public NexusClientOptions build() {
203226
interceptors,
204227
dataConverter,
205228
resolvedIdentity,
206-
externalStorage);
229+
externalStorage,
230+
plugins == null ? EMPTY_PLUGINS : plugins);
207231
}
208232
}
209233
}

0 commit comments

Comments
 (0)