Skip to content

Commit 58542ea

Browse files
patbeqoclaude
andcommitted
Add plugin hooks for activity and Nexus clients
Adds ActivityClientPlugin and NexusClientPlugin, propagated from the service stubs like ScheduleClientPlugin, with setPlugins/getPlugins on their options. SimplePlugin implements both and its builder gains addScheduleClientInterceptors, addActivityClientInterceptors, and addNexusClientInterceptors; configureScheduleClient now applies its interceptors instead of being a no-op. Replaces the hand-rolled plugin extraction loops with PluginUtils.extractPlugins and migrates the existing three sites. StartNexusOperationExecutionInput now copies headers into a mutable, case-insensitive map, matching ExecuteNexusOperationInput, so header-adding interceptors can populate it in place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 5193120 commit 58542ea

16 files changed

Lines changed: 430 additions & 68 deletions

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

Lines changed: 20 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,39 @@
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+
ActivityClientPlugin[] plugins =
46+
PluginUtils.mergePlugins(
47+
PluginUtils.extractPlugins(stubs.getOptions().getPlugins(), ActivityClientPlugin.class),
48+
options.getPlugins(),
49+
ActivityClientPlugin::getName,
50+
log,
51+
"service stubs",
52+
ActivityClientPlugin.class);
53+
// Plugins configure the user-provided options before the merged plugins are recorded on them.
54+
ActivityClientOptions.Builder builder = ActivityClientOptions.newBuilder(options);
55+
for (ActivityClientPlugin plugin : plugins) {
56+
plugin.configureActivityClient(builder);
57+
}
58+
options = builder.setPlugins(plugins).build();
59+
4060
this.stubs = stubs;
4161
this.options = options;
4262
this.metricsScope =

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

Lines changed: 38 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 = 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 Arrays.copyOf(plugins, plugins.length);
208+
}
209+
180210
@Override
181211
public boolean equals(Object o) {
182212
if (this == o) return true;
@@ -186,12 +216,14 @@ 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 31 * Objects.hash(namespace, dataConverter, identity, contextPropagators, interceptors)
226+
+ Arrays.hashCode(plugins);
195227
}
196228

197229
@Override
@@ -209,6 +241,8 @@ public String toString() {
209241
+ contextPropagators
210242
+ ", interceptors="
211243
+ interceptors
244+
+ ", plugins="
245+
+ Arrays.toString(plugins)
212246
+ '}';
213247
}
214248
}
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: 15 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,20 @@ public class NexusClientImpl implements NexusClient {
3940

4041
public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOptions options) {
4142
enforceNonWorkflowThread();
43+
NexusClientPlugin[] plugins =
44+
PluginUtils.mergePlugins(
45+
PluginUtils.extractPlugins(service.getOptions().getPlugins(), NexusClientPlugin.class),
46+
options.getPlugins(),
47+
NexusClientPlugin::getName,
48+
log,
49+
"service stubs",
50+
NexusClientPlugin.class);
51+
// Plugins configure the user-provided options before the merged plugins are recorded on them.
52+
NexusClientOptions.Builder builder = NexusClientOptions.newBuilder(options);
53+
for (NexusClientPlugin plugin : plugins) {
54+
plugin.configureNexusClient(builder);
55+
}
56+
options = builder.setPlugins(plugins).build();
4257
return WorkflowThreadMarker.protectFromWorkflowThread(
4358
new NexusClientImpl(service, options.toResolvedOptions()), NexusClient.class);
4459
}

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.temporal.internal.payload.storage.ExternalStorageRunner;
1010
import io.temporal.payload.storage.ExternalStorage;
1111
import java.lang.management.ManagementFactory;
12+
import java.util.Arrays;
1213
import java.util.Collections;
1314
import java.util.List;
1415
import javax.annotation.Nullable;
@@ -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 Arrays.copyOf(plugins, plugins.length);
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.
@@ -135,6 +144,7 @@ public static class Builder {
135144
private DataConverter dataConverter = GlobalDataConverter.get();
136145
private String identity;
137146
private ExternalStorage externalStorage;
147+
private NexusClientPlugin[] plugins = new NexusClientPlugin[0];
138148

139149
private Builder() {}
140150

@@ -147,6 +157,7 @@ private Builder(NexusClientOptions options) {
147157
dataConverter = options.dataConverter;
148158
identity = options.identity;
149159
externalStorage = options.externalStorage;
160+
plugins = options.plugins;
150161
}
151162

152163
/** Set the namespace this client will operate on. */
@@ -195,6 +206,12 @@ public NexusClientOptions.Builder setExternalStorage(
195206
return this;
196207
}
197208

209+
/** Set the plugins for this client, but don't allow null arrays to happen. */
210+
public NexusClientOptions.Builder setPlugins(NexusClientPlugin... plugins) {
211+
this.plugins = plugins == null ? new NexusClientPlugin[0] : plugins;
212+
return this;
213+
}
214+
198215
public NexusClientOptions build() {
199216
String resolvedIdentity =
200217
identity == null ? ManagementFactory.getRuntimeMXBean().getName() : identity;
@@ -203,7 +220,8 @@ public NexusClientOptions build() {
203220
interceptors,
204221
dataConverter,
205222
resolvedIdentity,
206-
externalStorage);
223+
externalStorage,
224+
plugins);
207225
}
208226
}
209227
}
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 Nexus client configuration.
8+
*
9+
* <p>Plugins that implement both {@link io.temporal.serviceclient.WorkflowServiceStubsPlugin} and
10+
* {@code NexusClientPlugin} are automatically propagated from the service stubs to the Nexus
11+
* client.
12+
*
13+
* @see io.temporal.serviceclient.WorkflowServiceStubsPlugin
14+
*/
15+
@Experimental
16+
public interface NexusClientPlugin {
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 Nexus 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 configureNexusClient(@Nonnull NexusClientOptions.Builder builder);
34+
}

0 commit comments

Comments
 (0)