Skip to content

Conversation

@donald-pinckney
Copy link

@donald-pinckney donald-pinckney commented Jan 14, 2026

Plugin System for Java Temporal SDK

What was changed

Added a plugin system for the Java Temporal SDK, modeled after the Python SDK's plugin architecture but adapted to Java idioms.

New Plugin Interfaces

  • WorkflowServiceStubsPlugin (temporal-serviceclient) - Configuration and connection lifecycle for service stubs
  • WorkflowClientPlugin (temporal-sdk) - Configuration for workflow clients
  • ScheduleClientPlugin (temporal-sdk) - Configuration for schedule clients
  • WorkerPlugin (temporal-sdk) - Configuration and lifecycle for worker factories and workers

SimplePlugin

  • SimplePlugin (temporal-sdk) - Abstract convenience class implementing all plugin interfaces
    • Can be used via builder: SimplePlugin.newBuilder("name").build()
    • Can be subclassed for custom behavior
    • Provides convenience methods for common operations (interceptors, data converters, workflow/activity registration)

Modified Files

  • WorkflowServiceStubsOptions - Added plugins field
  • WorkflowServiceStubs - Applies plugin configuration and connection lifecycle
  • WorkflowClientOptions - Added plugins field
  • WorkflowClientInternalImpl - Applies plugin configuration, propagates from service stubs
  • ScheduleClientOptions - Added plugins field
  • ScheduleClientImpl - Applies plugin configuration, propagates from service stubs
  • WorkerFactoryOptions - Added plugins field
  • WorkerFactory - Full plugin lifecycle (configuration, worker init, start/shutdown)

Test Files

  • PluginTest.java - Tests subclassing pattern and lifecycle ordering
  • SimplePluginBuilderTest.java - Tests builder API comprehensively
  • PluginPropagationTest.java - Integration tests for plugin propagation chain
  • WorkflowClientOptionsPluginTest.java - Tests options class plugin field handling

Why?

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 through the creation chain
  • Bundle multiple customizations (interceptors, context propagators, data converters, workflow/activity registrations) into reusable units

Checklist

  1. Closes Plugin support #2626, tracked in Plugins to support controlling multiple configuration points at once features#652

  2. How was this tested:

    • Unit tests for plugin interfaces and SimplePlugin builder
    • Integration tests for plugin propagation through full chain
    • All tests pass locally
  3. Any docs updates needed?

    • Documentation for the new plugin API should be added

Design

Plugin Interfaces

Each level in the creation chain has its own plugin interface:

WorkflowServiceStubsPlugin     →  WorkflowClientPlugin  →  WorkerPlugin
                               →  ScheduleClientPlugin

Plugins set at higher levels propagate down automatically. For example, a plugin set on WorkflowServiceStubsOptions that also implements WorkflowClientPlugin will have its configureWorkflowClient() method called when creating a WorkflowClient.

SimplePlugin

SimplePlugin is an abstract class that implements all plugin interfaces with sensible defaults. It can be used two ways:

Builder pattern for declarative configuration:

SimplePlugin plugin = SimplePlugin.newBuilder("my-org.tracing")
    .setDataConverter(myConverter)
    .addWorkerInterceptors(new TracingInterceptor())
    .addClientInterceptors(new LoggingInterceptor())
    .registerWorkflowImplementationTypes(MyWorkflow.class)
    .registerActivitiesImplementations(new MyActivityImpl())
    .onWorkerStart((taskQueue, worker) -> logger.info("Worker started: {}", taskQueue))
    .build();

Subclassing for custom behavior:

public class TracingPlugin extends SimplePlugin {
    private final Tracer tracer;

    public TracingPlugin(Tracer tracer) {
        super("my-org.tracing");
        this.tracer = tracer;
    }

    @Override
    public void configureWorkflowClient(WorkflowClientOptions.Builder builder) {
        builder.setInterceptors(new TracingClientInterceptor(tracer));
    }

    @Override
    public void configureWorkerFactory(WorkerFactoryOptions.Builder builder) {
        builder.setWorkerInterceptors(new TracingWorkerInterceptor(tracer));
    }
}

Usage Example

// Create plugin
SimplePlugin metricsPlugin = SimplePlugin.newBuilder("my-org.metrics")
    .customizeServiceStubs(b -> b.setMetricsScope(myScope))
    .addWorkerInterceptors(new MetricsInterceptor())
    .build();

// Set on service stubs - will propagate to client and workers
WorkflowServiceStubsOptions stubsOptions = WorkflowServiceStubsOptions.newBuilder()
    .setTarget("localhost:7233")
    .setPlugins(metricsPlugin)
    .build();

WorkflowServiceStubs stubs = WorkflowServiceStubs.newServiceStubs(stubsOptions);
WorkflowClient client = WorkflowClient.newInstance(stubs);  // plugin propagates here
WorkerFactory factory = WorkerFactory.newInstance(client);   // and here

@CLAassistant
Copy link

CLAassistant commented Jan 14, 2026

CLA assistant check
All committers have signed the CLA.

@donald-pinckney donald-pinckney marked this pull request as ready for review January 14, 2026 15:22
@donald-pinckney donald-pinckney requested a review from a team as a code owner January 14, 2026 15:22
// Apply plugin configuration phase (forward order), then validate
WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(options);
builder.setPlugins(mergedPlugins);
applyClientPluginConfiguration(builder, mergedPlugins);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cretz One subtle thing here with plugin propagation is what the plugins see, for which plugins are set on the builder when plugin.configureWorkflowClient(builder) is called on the plugin.

In this Java implementation, the plugins will all see the mergedPlugins (propagated + explicit).

In Python, the plugins only see the plugins that are in the explicit config, and not the propagated ones:

plugins_from_client = cast(
            list[Plugin],
            [p for p in client.config()["plugins"] if isinstance(p, Plugin)],
        )
        for client_plugin in plugins_from_client:
            if type(client_plugin) in [type(p) for p in plugins]:
                warnings.warn(
                    f"The same plugin type {type(client_plugin)} is present from both client and worker. It may run twice and may not be the intended behavior."
                )
        plugins = plugins_from_client + list(plugins)
        self._initial_config = config.copy()

        self._plugins = plugins
        for plugin in plugins:
            config = plugin.configure_worker(config)

(that code is for _worker.py, but point still stands).

Do you have an intuition for which is the correct behavior and why? I would think that the Java behavior is more natural.

@donald-pinckney
Copy link
Author

donald-pinckney commented Jan 16, 2026

@cretz I think i've addressed everything. Could you do a re-review of the code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin support

4 participants