diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java index 03bcbe8f1..37a7b5ce7 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/AutoConfigurationUtils.java @@ -168,4 +168,47 @@ static String temporalCustomizerBeanName(String beanPrefix, Class optionsBuil bindingCustomizerName.substring(bindingCustomizerName.lastIndexOf(".") + 1); return beanPrefix + bindingCustomizerName; } + + /** + * Filter out plugins that implement a higher-level plugin interface, as those are handled at that + * higher level via propagation. + * + *

The plugin hierarchy is: WorkflowServiceStubsPlugin -> WorkflowClientPlugin -> WorkerPlugin. + * A plugin implementing a higher-level interface will be registered at that level and propagate + * down automatically, so it should not also be registered at lower levels. + * + * @param plugins the list of plugins to filter (may be null) + * @param excludeType the plugin interface to exclude + * @return the filtered list, or null if input was null or all plugins were filtered out + */ + static @Nullable List filterPlugins(@Nullable List plugins, Class excludeType) { + if (plugins == null) { + return null; + } + if (plugins.isEmpty()) { + return null; + } + List filtered = new ArrayList<>(); + for (T plugin : plugins) { + if (!excludeType.isInstance(plugin)) { + filtered.add(plugin); + } + } + return filtered.isEmpty() ? null : filtered; + } + + /** + * Sort plugins by @Order and @Priority annotations for consistent ordering. + * + * @param plugins the list of plugins to sort (may be null) + * @return the sorted list, or null if input was null + */ + static @Nullable List sortPlugins(@Nullable List plugins) { + if (plugins == null || plugins.isEmpty()) { + return plugins; + } + List sorted = new ArrayList<>(plugins); + AnnotationAwareOrderComparator.sort(sorted); + return sorted; + } } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java index a0079036c..674ec7b15 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/NonRootBeanPostProcessor.java @@ -5,11 +5,14 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; @@ -23,7 +26,9 @@ import io.temporal.worker.WorkerFactory; import io.temporal.worker.WorkerFactoryOptions.Builder; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -50,6 +55,10 @@ public class NonRootBeanPostProcessor implements BeanPostProcessor, BeanFactoryA private @Nullable Tracer tracer; private @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment; private @Nullable Scope metricsScope; + private @Nullable List serviceStubsPlugins; + private @Nullable List workflowClientPlugins; + private @Nullable List scheduleClientPlugins; + private @Nullable List workerPlugins; public NonRootBeanPostProcessor(@Nonnull TemporalProperties temporalProperties) { this.temporalProperties = temporalProperties; @@ -78,6 +87,20 @@ public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull Stri findBean( "temporalTestWorkflowEnvironmentAdapter", TestWorkflowEnvironmentAdapter.class); } + // Collect all plugin types + serviceStubsPlugins = findAllBeans(WorkflowServiceStubsPlugin.class); + // Filter plugins so each is only registered at its highest applicable level + workflowClientPlugins = + AutoConfigurationUtils.filterPlugins( + findAllBeans(WorkflowClientPlugin.class), WorkflowServiceStubsPlugin.class); + scheduleClientPlugins = + AutoConfigurationUtils.filterPlugins( + findAllBeans(ScheduleClientPlugin.class), WorkflowServiceStubsPlugin.class); + workerPlugins = + AutoConfigurationUtils.filterPlugins( + AutoConfigurationUtils.filterPlugins( + findAllBeans(WorkerPlugin.class), WorkflowServiceStubsPlugin.class), + WorkflowClientPlugin.class); namespaceProperties.forEach(this::injectBeanByNonRootNamespace); } } @@ -124,7 +147,8 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { connectionProperties, metricsScope, testWorkflowEnvironment, - workflowServiceStubsCustomizers); + workflowServiceStubsCustomizers, + serviceStubsPlugins); WorkflowServiceStubs workflowServiceStubs = serviceStubsTemplate.getWorkflowServiceStubs(); NonRootNamespaceTemplate namespaceTemplate = @@ -142,7 +166,10 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { workerCustomizers, workflowClientCustomizers, scheduleClientCustomizers, - workflowImplementationCustomizers); + workflowImplementationCustomizers, + workflowClientPlugins, + scheduleClientPlugins, + workerPlugins); ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate(); WorkflowClient workflowClient = clientTemplate.getWorkflowClient(); @@ -200,6 +227,16 @@ private T findBeanByNamespace(String beanPrefix, Class clazz) { return null; } + private @Nullable List findAllBeans(Class clazz) { + try { + List beans = new ArrayList<>(beanFactory.getBeansOfType(clazz).values()); + return AutoConfigurationUtils.sortPlugins(beans); + } catch (NoSuchBeanDefinitionException ignore) { + // No beans of this type defined - this is expected for optional plugins + } + return null; + } + private List> findBeanByNameSpaceForTemporalCustomizer( String beanPrefix, Class genericOptionsBuilderClass) { String beanName = diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java index 718fd6445..c5cd2a492 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/RootNamespaceAutoConfiguration.java @@ -3,13 +3,16 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; import io.temporal.spring.boot.autoconfigure.template.ClientTemplate; @@ -20,6 +23,7 @@ import io.temporal.worker.WorkerFactory; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; import java.util.Collection; import java.util.List; @@ -87,7 +91,10 @@ public NamespaceTemplate rootNamespaceTemplate( scheduleCustomizerMap, @Autowired(required = false) @Nullable Map> - workflowImplementationCustomizerMap) { + workflowImplementationCustomizerMap, + @Autowired(required = false) @Nullable List workflowClientPlugins, + @Autowired(required = false) @Nullable List scheduleClientPlugins, + @Autowired(required = false) @Nullable List workerPlugins) { DataConverter chosenDataConverter = AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties); List chosenClientInterceptors = @@ -121,6 +128,31 @@ public NamespaceTemplate rootNamespaceTemplate( WorkflowImplementationOptions.Builder.class, properties); + // Sort plugins by @Order/@Priority for consistent ordering + List sortedClientPlugins = + AutoConfigurationUtils.sortPlugins(workflowClientPlugins); + List sortedSchedulePlugins = + AutoConfigurationUtils.sortPlugins(scheduleClientPlugins); + List sortedWorkerPlugins = AutoConfigurationUtils.sortPlugins(workerPlugins); + + // Filter plugins so each is only registered at its highest applicable level. + // WorkflowServiceStubsPlugin is handled at ServiceStubsAutoConfiguration level and propagates + // down. + // WorkflowClientPlugin (not WorkflowServiceStubsPlugin) is handled here and propagates to + // workers. + // ScheduleClientPlugin (not WorkflowServiceStubsPlugin) is handled here. + // WorkerPlugin (not WorkflowServiceStubsPlugin, not WorkflowClientPlugin) is handled here. + List filteredClientPlugins = + AutoConfigurationUtils.filterPlugins(sortedClientPlugins, WorkflowServiceStubsPlugin.class); + List filteredSchedulePlugins = + AutoConfigurationUtils.filterPlugins( + sortedSchedulePlugins, WorkflowServiceStubsPlugin.class); + List filteredWorkerPlugins = + AutoConfigurationUtils.filterPlugins( + AutoConfigurationUtils.filterPlugins( + sortedWorkerPlugins, WorkflowServiceStubsPlugin.class), + WorkflowClientPlugin.class); + return new NamespaceTemplate( properties, workflowServiceStubs, @@ -134,7 +166,10 @@ public NamespaceTemplate rootNamespaceTemplate( workerCustomizer, clientCustomizer, scheduleCustomizer, - workflowImplementationCustomizer); + workflowImplementationCustomizer, + filteredClientPlugins, + filteredSchedulePlugins, + filteredWorkerPlugins); } /** Client */ diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java index 761883df0..022cd7f9c 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/ServiceStubsAutoConfiguration.java @@ -4,6 +4,7 @@ import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; import io.temporal.serviceclient.WorkflowServiceStubsOptions.Builder; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; import io.temporal.spring.boot.autoconfigure.template.ServiceStubsTemplate; @@ -42,15 +43,19 @@ public ServiceStubsTemplate serviceStubsTemplate( TestWorkflowEnvironmentAdapter testWorkflowEnvironment, @Autowired(required = false) @Nullable Map> - workflowServiceStubsCustomizerMap) { + workflowServiceStubsCustomizerMap, + @Autowired(required = false) @Nullable List plugins) { List> workflowServiceStubsCustomizer = AutoConfigurationUtils.chooseTemporalCustomizerBeans( beanFactory, workflowServiceStubsCustomizerMap, Builder.class, properties); + // Sort plugins by @Order/@Priority for consistent ordering + List sortedPlugins = AutoConfigurationUtils.sortPlugins(plugins); return new ServiceStubsTemplate( properties.getConnection(), metricsScope, testWorkflowEnvironment, - workflowServiceStubsCustomizer); + workflowServiceStubsCustomizer, + sortedPlugins); } @Bean(name = "temporalWorkflowServiceStubs") diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java index c372b797f..ab21591a7 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/TestServerAutoConfiguration.java @@ -3,11 +3,14 @@ import com.uber.m3.tally.Scope; import io.opentracing.Tracer; 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.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.TemporalProperties; import io.temporal.spring.boot.autoconfigure.template.TestWorkflowEnvironmentAdapter; @@ -16,6 +19,7 @@ import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.worker.WorkerFactoryOptions; +import io.temporal.worker.WorkerPlugin; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -79,7 +83,10 @@ public TestWorkflowEnvironment testWorkflowEnvironment( Map> clientCustomizerMap, @Autowired(required = false) @Nullable Map> - scheduleCustomizerMap) { + scheduleCustomizerMap, + @Autowired(required = false) @Nullable List workflowClientPlugins, + @Autowired(required = false) @Nullable List scheduleClientPlugins, + @Autowired(required = false) @Nullable List workerPlugins) { DataConverter chosenDataConverter = AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties); List chosenClientInterceptors = @@ -104,6 +111,35 @@ public TestWorkflowEnvironment testWorkflowEnvironment( AutoConfigurationUtils.chooseTemporalCustomizerBeans( beanFactory, scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties); + // Sort plugins by @Order/@Priority for consistent ordering + List sortedClientPlugins = + AutoConfigurationUtils.sortPlugins(workflowClientPlugins); + List sortedSchedulePlugins = + AutoConfigurationUtils.sortPlugins(scheduleClientPlugins); + List sortedWorkerPlugins = AutoConfigurationUtils.sortPlugins(workerPlugins); + + // Filter plugins so each is only registered at its highest applicable level. + // Note: TestWorkflowEnvironment creates its own internal test server and does not accept + // WorkflowServiceStubsPlugin directly. Any beans implementing WorkflowServiceStubsPlugin + // will be ignored in test mode - only their lower-level plugin functionality (if any) is used. + if (sortedClientPlugins != null + && sortedClientPlugins.stream().anyMatch(WorkflowServiceStubsPlugin.class::isInstance)) { + log.warn( + "WorkflowServiceStubsPlugin beans are present but will be ignored in test mode. " + + "TestWorkflowEnvironment creates its own test server and does not support " + + "WorkflowServiceStubsPlugin. Only WorkflowClientPlugin functionality will be used."); + } + List filteredClientPlugins = + AutoConfigurationUtils.filterPlugins(sortedClientPlugins, WorkflowServiceStubsPlugin.class); + List filteredSchedulePlugins = + AutoConfigurationUtils.filterPlugins( + sortedSchedulePlugins, WorkflowServiceStubsPlugin.class); + List filteredWorkerPlugins = + AutoConfigurationUtils.filterPlugins( + AutoConfigurationUtils.filterPlugins( + sortedWorkerPlugins, WorkflowServiceStubsPlugin.class), + WorkflowClientPlugin.class); + TestEnvironmentOptions.Builder options = TestEnvironmentOptions.newBuilder() .setWorkflowClientOptions( @@ -114,7 +150,9 @@ public TestWorkflowEnvironment testWorkflowEnvironment( chosenScheduleClientInterceptors, otTracer, clientCustomizer, - scheduleCustomizer) + scheduleCustomizer, + filteredClientPlugins, + filteredSchedulePlugins) .createWorkflowClientOptions()); if (metricsScope != null) { @@ -123,7 +161,11 @@ public TestWorkflowEnvironment testWorkflowEnvironment( options.setWorkerFactoryOptions( new WorkerFactoryOptionsTemplate( - properties, chosenWorkerInterceptors, otTracer, workerFactoryCustomizer) + properties, + chosenWorkerInterceptors, + otTracer, + workerFactoryCustomizer, + filteredWorkerPlugins) .createWorkerFactoryOptions()); if (testEnvOptionsCustomizers != null) { diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java index f117fbc33..c9f68cb62 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ClientTemplate.java @@ -4,8 +4,10 @@ import io.opentracing.Tracer; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowClientPlugin; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -33,8 +35,9 @@ public ClientTemplate( @Nullable WorkflowServiceStubs workflowServiceStubs, @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, @Nullable List> clientCustomizers, - @Nullable - List> scheduleCustomizers) { + @Nullable List> scheduleCustomizers, + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins) { this.optionsTemplate = new WorkflowClientOptionsTemplate( namespace, @@ -43,11 +46,42 @@ public ClientTemplate( scheduleClientInterceptors, tracer, clientCustomizers, - scheduleCustomizers); + scheduleCustomizers, + workflowClientPlugins, + scheduleClientPlugins); this.workflowServiceStubs = workflowServiceStubs; this.testWorkflowEnvironment = testWorkflowEnvironment; } + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public ClientTemplate( + @Nonnull String namespace, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable Tracer tracer, + @Nullable WorkflowServiceStubs workflowServiceStubs, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable List> clientCustomizers, + @Nullable + List> scheduleCustomizers) { + this( + namespace, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + tracer, + workflowServiceStubs, + testWorkflowEnvironment, + clientCustomizers, + scheduleCustomizers, + null, + null); + } + public WorkflowClient getWorkflowClient() { if (workflowClient == null) { this.workflowClient = createWorkflowClient(); diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java index eef891870..d80c69955 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NamespaceTemplate.java @@ -2,7 +2,9 @@ import io.opentracing.Tracer; 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.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; @@ -12,6 +14,7 @@ import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; import java.util.List; import javax.annotation.Nonnull; @@ -36,6 +39,9 @@ public class NamespaceTemplate { scheduleCustomizers; private final @Nullable List> workflowImplementationCustomizers; + private final @Nullable List workflowClientPlugins; + private final @Nullable List scheduleClientPlugins; + private final @Nullable List workerPlugins; private ClientTemplate clientTemplate; private WorkersTemplate workersTemplate; @@ -56,7 +62,10 @@ public NamespaceTemplate( @Nullable List> scheduleCustomizers, @Nullable List> - workflowImplementationCustomizers) { + workflowImplementationCustomizers, + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins, + @Nullable List workerPlugins) { this.namespaceProperties = namespaceProperties; this.workflowServiceStubs = workflowServiceStubs; this.dataConverter = dataConverter; @@ -71,6 +80,49 @@ public NamespaceTemplate( this.clientCustomizers = clientCustomizers; this.scheduleCustomizers = scheduleCustomizers; this.workflowImplementationCustomizers = workflowImplementationCustomizers; + this.workflowClientPlugins = workflowClientPlugins; + this.scheduleClientPlugins = scheduleClientPlugins; + this.workerPlugins = workerPlugins; + } + + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public NamespaceTemplate( + @Nonnull NamespaceProperties namespaceProperties, + @Nonnull WorkflowServiceStubs workflowServiceStubs, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> workerFactoryCustomizers, + @Nullable List> workerCustomizers, + @Nullable List> clientCustomizers, + @Nullable List> scheduleCustomizers, + @Nullable + List> + workflowImplementationCustomizers) { + this( + namespaceProperties, + workflowServiceStubs, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + workerInterceptors, + tracer, + testWorkflowEnvironment, + workerFactoryCustomizers, + workerCustomizers, + clientCustomizers, + scheduleCustomizers, + workflowImplementationCustomizers, + null, + null, + null); } public ClientTemplate getClientTemplate() { @@ -85,7 +137,9 @@ public ClientTemplate getClientTemplate() { workflowServiceStubs, testWorkflowEnvironment, clientCustomizers, - scheduleCustomizers); + scheduleCustomizers, + workflowClientPlugins, + scheduleClientPlugins); } return clientTemplate; } @@ -101,7 +155,8 @@ public WorkersTemplate getWorkersTemplate() { testWorkflowEnvironment, workerFactoryCustomizers, workerCustomizers, - workflowImplementationCustomizers); + workflowImplementationCustomizers, + workerPlugins); } return this.workersTemplate; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java index f2292e385..7f62bf97b 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NonRootNamespaceTemplate.java @@ -2,7 +2,9 @@ import io.opentracing.Tracer; 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.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; @@ -12,6 +14,7 @@ import io.temporal.spring.boot.autoconfigure.properties.NonRootNamespaceProperties; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.worker.WorkerPlugin; import io.temporal.worker.WorkflowImplementationOptions; import java.util.List; import javax.annotation.Nonnull; @@ -40,7 +43,10 @@ public NonRootNamespaceTemplate( @Nullable List> scheduleCustomizers, @Nullable List> - workflowImplementationCustomizers) { + workflowImplementationCustomizers, + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins, + @Nullable List workerPlugins) { super( namespaceProperties, workflowServiceStubs, @@ -54,10 +60,55 @@ public NonRootNamespaceTemplate( workerCustomizers, clientCustomizers, scheduleCustomizers, - workflowImplementationCustomizers); + workflowImplementationCustomizers, + workflowClientPlugins, + scheduleClientPlugins, + workerPlugins); this.beanFactory = beanFactory; } + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public NonRootNamespaceTemplate( + @Nonnull BeanFactory beanFactory, + @Nonnull NonRootNamespaceProperties namespaceProperties, + @Nonnull WorkflowServiceStubs workflowServiceStubs, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> workerFactoryCustomizers, + @Nullable List> workerCustomizers, + @Nullable List> clientCustomizers, + @Nullable List> scheduleCustomizers, + @Nullable + List> + workflowImplementationCustomizers) { + this( + beanFactory, + namespaceProperties, + workflowServiceStubs, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + workerInterceptors, + tracer, + testWorkflowEnvironment, + workerFactoryCustomizers, + workerCustomizers, + clientCustomizers, + scheduleCustomizers, + workflowImplementationCustomizers, + null, + null, + null); + } + @Override public WorkersTemplate getWorkersTemplate() { WorkersTemplate workersTemplate = super.getWorkersTemplate(); diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java index 684faac63..dbcd8ebb2 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubOptionsTemplate.java @@ -5,6 +5,7 @@ import io.temporal.internal.common.ShadingHelpers; import io.temporal.serviceclient.SimpleSslContextBuilder; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; import java.io.ByteArrayInputStream; @@ -24,16 +25,32 @@ public class ServiceStubOptionsTemplate { private final @Nullable Scope metricsScope; private final @Nullable List> workflowServiceStubsCustomizer; + private final @Nullable List plugins; public ServiceStubOptionsTemplate( @Nonnull ConnectionProperties connectionProperties, @Nullable Scope metricsScope, @Nullable List> - workflowServiceStubsCustomizer) { + workflowServiceStubsCustomizer, + @Nullable List plugins) { this.connectionProperties = connectionProperties; this.metricsScope = metricsScope; this.workflowServiceStubsCustomizer = workflowServiceStubsCustomizer; + this.plugins = plugins; + } + + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public ServiceStubOptionsTemplate( + @Nonnull ConnectionProperties connectionProperties, + @Nullable Scope metricsScope, + @Nullable + List> + workflowServiceStubsCustomizer) { + this(connectionProperties, metricsScope, workflowServiceStubsCustomizer, null); } public WorkflowServiceStubsOptions createServiceStubOptions() { @@ -59,6 +76,10 @@ public WorkflowServiceStubsOptions createServiceStubOptions() { stubsOptionsBuilder.setMetricsScope(metricsScope); } + if (plugins != null && !plugins.isEmpty()) { + stubsOptionsBuilder.setPlugins(plugins.toArray(new WorkflowServiceStubsPlugin[0])); + } + if (workflowServiceStubsCustomizer != null) { for (TemporalOptionsCustomizer workflowServiceStubsCustomizer : workflowServiceStubsCustomizer) { diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java index 7d6597143..1db7c4b34 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/ServiceStubsTemplate.java @@ -3,6 +3,7 @@ import com.uber.m3.tally.Scope; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.ConnectionProperties; import java.util.List; @@ -18,6 +19,7 @@ public class ServiceStubsTemplate { private final @Nullable List> workflowServiceStubsCustomizers; + private final @Nullable List plugins; private WorkflowServiceStubs workflowServiceStubs; @@ -27,11 +29,32 @@ public ServiceStubsTemplate( @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, @Nullable List> - workflowServiceStubsCustomizers) { + workflowServiceStubsCustomizers, + @Nullable List plugins) { this.connectionProperties = connectionProperties; this.metricsScope = metricsScope; this.testWorkflowEnvironment = testWorkflowEnvironment; this.workflowServiceStubsCustomizers = workflowServiceStubsCustomizers; + this.plugins = plugins; + } + + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public ServiceStubsTemplate( + @Nonnull ConnectionProperties connectionProperties, + @Nullable Scope metricsScope, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> + workflowServiceStubsCustomizers) { + this( + connectionProperties, + metricsScope, + testWorkflowEnvironment, + workflowServiceStubsCustomizers, + null); } public WorkflowServiceStubs getWorkflowServiceStubs() { @@ -54,7 +77,10 @@ private WorkflowServiceStubs createServiceStubs() { workflowServiceStubs = WorkflowServiceStubs.newServiceStubs( new ServiceStubOptionsTemplate( - connectionProperties, metricsScope, workflowServiceStubsCustomizers) + connectionProperties, + metricsScope, + workflowServiceStubsCustomizers, + plugins) .createServiceStubOptions()); } } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java index b7b44d44f..baa037402 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerFactoryOptionsTemplate.java @@ -7,6 +7,7 @@ import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties; import io.temporal.worker.WorkerFactoryOptions; +import io.temporal.worker.WorkerPlugin; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -18,16 +19,31 @@ public class WorkerFactoryOptionsTemplate { private final @Nullable List workerInterceptors; private final @Nullable Tracer tracer; private final @Nullable List> customizer; + private final @Nullable List plugins; public WorkerFactoryOptionsTemplate( @Nonnull NamespaceProperties namespaceProperties, @Nullable List workerInterceptors, @Nullable Tracer tracer, - @Nullable List> customizer) { + @Nullable List> customizer, + @Nullable List plugins) { this.namespaceProperties = namespaceProperties; this.workerInterceptors = workerInterceptors; this.tracer = tracer; this.customizer = customizer; + this.plugins = plugins; + } + + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public WorkerFactoryOptionsTemplate( + @Nonnull NamespaceProperties namespaceProperties, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable List> customizer) { + this(namespaceProperties, workerInterceptors, tracer, customizer, null); } public WorkerFactoryOptions createWorkerFactoryOptions() { @@ -56,6 +72,10 @@ public WorkerFactoryOptions createWorkerFactoryOptions() { } options.setWorkerInterceptors(interceptors.toArray(new WorkerInterceptor[0])); + if (plugins != null && !plugins.isEmpty()) { + options.setPlugins(plugins.toArray(new WorkerPlugin[0])); + } + if (customizer != null) { for (TemporalOptionsCustomizer customizer : customizer) { options = customizer.customize(options); diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java index 870ede282..0192fd4bf 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkersTemplate.java @@ -62,6 +62,7 @@ public class WorkersTemplate implements BeanFactoryAware, EnvironmentAware { private final @Nullable List> workerCustomizers; private final @Nullable List> workflowImplementationCustomizers; + private final @Nullable List plugins; private ConfigurableListableBeanFactory beanFactory; private Environment environment; @@ -81,7 +82,8 @@ public WorkersTemplate( @Nullable List> workerCustomizers, @Nullable List> - workflowImplementationCustomizers) { + workflowImplementationCustomizers, + @Nullable List plugins) { this.namespaceProperties = namespaceProperties; this.workerInterceptors = workerInterceptors; this.tracer = tracer; @@ -91,6 +93,35 @@ public WorkersTemplate( this.workerFactoryCustomizers = workerFactoryCustomizers; this.workerCustomizers = workerCustomizers; this.workflowImplementationCustomizers = workflowImplementationCustomizers; + this.plugins = plugins; + } + + /** + * @deprecated Use constructor with plugins parameter + */ + @Deprecated + public WorkersTemplate( + @Nonnull NamespaceProperties namespaceProperties, + @Nullable ClientTemplate clientTemplate, + @Nullable List workerInterceptors, + @Nullable Tracer tracer, + @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment, + @Nullable + List> workerFactoryCustomizers, + @Nullable List> workerCustomizers, + @Nullable + List> + workflowImplementationCustomizers) { + this( + namespaceProperties, + clientTemplate, + workerInterceptors, + tracer, + testWorkflowEnvironment, + workerFactoryCustomizers, + workerCustomizers, + workflowImplementationCustomizers, + null); } public NamespaceProperties getNamespaceProperties() { @@ -126,7 +157,11 @@ WorkerFactory createWorkerFactory(WorkflowClient workflowClient) { } else { WorkerFactoryOptions workerFactoryOptions = new WorkerFactoryOptionsTemplate( - namespaceProperties, workerInterceptors, tracer, workerFactoryCustomizers) + namespaceProperties, + workerInterceptors, + tracer, + workerFactoryCustomizers, + plugins) .createWorkerFactoryOptions(); return WorkerFactory.newInstance(workflowClient, workerFactoryOptions); } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java index 02ef555d9..ca4e56457 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkflowClientOptionsTemplate.java @@ -2,7 +2,9 @@ import io.opentracing.Tracer; 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.converter.DataConverter; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -25,6 +27,8 @@ public class WorkflowClientOptionsTemplate { clientCustomizers; private final @Nullable List> scheduleCustomizers; + private final @Nullable List workflowClientPlugins; + private final @Nullable List scheduleClientPlugins; public WorkflowClientOptionsTemplate( @Nonnull String namespace, @@ -33,8 +37,9 @@ public WorkflowClientOptionsTemplate( @Nullable List scheduleClientInterceptors, @Nullable Tracer tracer, @Nullable List> clientCustomizers, - @Nullable - List> scheduleCustomizers) { + @Nullable List> scheduleCustomizers, + @Nullable List workflowClientPlugins, + @Nullable List scheduleClientPlugins) { this.namespace = namespace; this.dataConverter = dataConverter; this.workflowClientInterceptors = workflowClientInterceptors; @@ -42,6 +47,33 @@ public WorkflowClientOptionsTemplate( this.tracer = tracer; this.clientCustomizers = clientCustomizers; this.scheduleCustomizers = scheduleCustomizers; + this.workflowClientPlugins = workflowClientPlugins; + this.scheduleClientPlugins = scheduleClientPlugins; + } + + /** + * @deprecated Use constructor with plugins parameters + */ + @Deprecated + public WorkflowClientOptionsTemplate( + @Nonnull String namespace, + @Nullable DataConverter dataConverter, + @Nullable List workflowClientInterceptors, + @Nullable List scheduleClientInterceptors, + @Nullable Tracer tracer, + @Nullable List> clientCustomizers, + @Nullable + List> scheduleCustomizers) { + this( + namespace, + dataConverter, + workflowClientInterceptors, + scheduleClientInterceptors, + tracer, + clientCustomizers, + scheduleCustomizers, + null, + null); } public WorkflowClientOptions createWorkflowClientOptions() { @@ -62,6 +94,10 @@ public WorkflowClientOptions createWorkflowClientOptions() { options.setInterceptors(interceptors.toArray(new WorkflowClientInterceptor[0])); + if (workflowClientPlugins != null && !workflowClientPlugins.isEmpty()) { + options.setPlugins(workflowClientPlugins.toArray(new WorkflowClientPlugin[0])); + } + if (clientCustomizers != null) { for (TemporalOptionsCustomizer customizer : clientCustomizers) { @@ -79,6 +115,10 @@ public ScheduleClientOptions createScheduleClientOptions() { options.setInterceptors(scheduleClientInterceptors); } + if (scheduleClientPlugins != null && !scheduleClientPlugins.isEmpty()) { + options.setPlugins(scheduleClientPlugins.toArray(new ScheduleClientPlugin[0])); + } + if (scheduleCustomizers != null) { for (TemporalOptionsCustomizer customizer : scheduleCustomizers) { diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/PluginAutoWiringTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/PluginAutoWiringTest.java new file mode 100644 index 000000000..5b9dc6515 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/PluginAutoWiringTest.java @@ -0,0 +1,415 @@ +package io.temporal.spring.boot.autoconfigure; + +import static org.junit.jupiter.api.Assertions.*; + +import io.temporal.client.WorkflowClient; +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.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.serviceclient.WorkflowServiceStubsPlugin; +import io.temporal.worker.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.annotation.Order; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = PluginAutoWiringTest.Configuration.class) +@ActiveProfiles(profiles = "auto-discovery-by-task-queue") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class PluginAutoWiringTest { + @Autowired ConfigurableApplicationContext applicationContext; + + @Autowired WorkflowClient workflowClient; + + @Autowired WorkerFactory temporalWorkerFactory; + + @BeforeEach + void setUp() { + applicationContext.start(); + } + + @Test + @Timeout(value = 10) + public void testPluginsAreAutoWired() { + // Verify that plugin beans are present in the application context + assertNotNull(applicationContext.getBean("clientPlugin")); + assertNotNull(applicationContext.getBean("workerPlugin")); + assertNotNull(applicationContext.getBean("comboPlugin")); + } + + @Test + @Timeout(value = 10) + public void testClientPluginGetsCalled() { + // Verify client plugin got its configure method called by checking the recorded invocations + assertTrue( + Configuration.clientPluginInvocations.contains("configureWorkflowClient"), + "Client plugin configureWorkflowClient should have been called"); + } + + @Test + @Timeout(value = 10) + public void testWorkerPluginGetsCalled() { + // Verify worker plugin got its configure method called + assertTrue( + Configuration.workerPluginInvocations.contains("configureWorkerFactory"), + "Worker plugin configureWorkerFactory should have been called"); + } + + @Test + @Timeout(value = 10) + public void testPluginFiltering() { + // The combo plugin implements WorkflowServiceStubsPlugin, WorkflowClientPlugin, + // ScheduleClientPlugin, + // and WorkerPlugin. Because it implements WorkflowServiceStubsPlugin (the highest level), it + // gets + // filtered out from all lower levels (client, schedule, worker) to avoid double-registration. + // + // In normal (non-test) mode, the plugin would be registered at the stubs level and the SDK's + // plugin propagation system would call its lower-level methods. + // + // In test mode, WorkflowServiceStubsPlugin is not supported (TestWorkflowEnvironment creates + // its + // own server), so the combo plugin's configuration methods are NOT called at all. + // + // This verifies the filtering is working correctly: + // - Combo plugin should NOT have client/worker methods called directly (filtered out) + assertFalse( + Configuration.comboPluginInvocations.contains("configureWorkflowClient"), + "Combo plugin should be filtered out at client level (implements WorkflowServiceStubsPlugin)"); + assertFalse( + Configuration.comboPluginInvocations.contains("configureWorkerFactory"), + "Combo plugin should be filtered out at worker level (implements WorkflowServiceStubsPlugin)"); + + // The regular (non-combo) client and worker plugins should still be called + assertTrue( + Configuration.clientPluginInvocations.contains("configureWorkflowClient"), + "Regular client plugin should still be called"); + assertTrue( + Configuration.workerPluginInvocations.contains("configureWorkerFactory"), + "Regular worker plugin should still be called"); + } + + @Test + @Timeout(value = 10) + public void testPluginOrdering() { + // Verify that plugin ordering is applied (via @Order annotation) + // The ordered plugins should have their callbacks in the expected order + List record = Configuration.orderedPluginInvocations; + + int firstIndex = record.indexOf("first"); + int secondIndex = record.indexOf("second"); + int thirdIndex = record.indexOf("third"); + + assertTrue(firstIndex >= 0, "First ordered plugin should have been called"); + assertTrue(secondIndex >= 0, "Second ordered plugin should have been called"); + assertTrue(thirdIndex >= 0, "Third ordered plugin should have been called"); + + assertTrue(firstIndex < secondIndex, "First should be called before second"); + assertTrue(secondIndex < thirdIndex, "Second should be called before third"); + } + + @ComponentScan( + excludeFilters = + @ComponentScan.Filter( + pattern = "io\\.temporal\\.spring\\.boot\\.autoconfigure\\.byworkername\\..*", + type = FilterType.REGEX)) + public static class Configuration { + // Track invocations for verification + static List clientPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + static List workerPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + static List comboPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + static List orderedPluginInvocations = Collections.synchronizedList(new ArrayList<>()); + + // WorkflowClientPlugin only - middle level + @Bean + public TestClientPlugin clientPlugin() { + return new TestClientPlugin(clientPluginInvocations); + } + + // WorkerPlugin only - lowest level + @Bean + public TestWorkerPlugin workerPlugin() { + return new TestWorkerPlugin(workerPluginInvocations); + } + + // Combo plugin implementing all interfaces - tests filtering behavior + @Bean + public TestComboPlugin comboPlugin() { + return new TestComboPlugin(comboPluginInvocations); + } + + // Ordered plugins to test @Order support + @Bean + @Order(1) + public WorkerPlugin firstOrderedPlugin() { + return new OrderedWorkerPlugin("first", orderedPluginInvocations); + } + + @Bean + @Order(2) + public WorkerPlugin secondOrderedPlugin() { + return new OrderedWorkerPlugin("second", orderedPluginInvocations); + } + + @Bean + @Order(3) + public WorkerPlugin thirdOrderedPlugin() { + return new OrderedWorkerPlugin("third", orderedPluginInvocations); + } + } + + public static class TestClientPlugin implements WorkflowClientPlugin { + private final List invocations; + + public TestClientPlugin(List invocations) { + this.invocations = invocations; + } + + @Nonnull + @Override + public String getName() { + return "test.client-plugin"; + } + + @Override + public void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder) { + invocations.add("configureWorkflowClient"); + } + } + + public static class TestWorkerPlugin implements WorkerPlugin { + private final List invocations; + + public TestWorkerPlugin(List invocations) { + this.invocations = invocations; + } + + @Nonnull + @Override + public String getName() { + return "test.worker-plugin"; + } + + @Override + public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { + invocations.add("configureWorkerFactory"); + } + + @Override + public void configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) {} + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {} + + @Override + public void startWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void startWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void shutdownWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void replayWorkflowExecution( + @Nonnull Worker worker, + @Nonnull io.temporal.common.WorkflowExecutionHistory history, + @Nonnull ReplayCallback next) + throws Exception { + next.replay(worker, history); + } + } + + // Plugin that implements multiple interfaces - tests filtering behavior + public static class TestComboPlugin + implements WorkflowServiceStubsPlugin, + WorkflowClientPlugin, + ScheduleClientPlugin, + WorkerPlugin { + private final List invocations; + + public TestComboPlugin(List invocations) { + this.invocations = invocations; + } + + @Nonnull + @Override + public String getName() { + return "test.combo-plugin"; + } + + @Override + public void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder) { + invocations.add("configureServiceStubs"); + } + + @Nonnull + @Override + public WorkflowServiceStubs connectServiceClient( + @Nonnull WorkflowServiceStubsOptions options, + @Nonnull Supplier next) { + invocations.add("connectServiceClient"); + return next.get(); + } + + @Override + public void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder) { + invocations.add("configureWorkflowClient"); + } + + @Override + public void configureScheduleClient(@Nonnull ScheduleClientOptions.Builder builder) { + invocations.add("configureScheduleClient"); + } + + @Override + public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { + invocations.add("configureWorkerFactory"); + } + + @Override + public void configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) {} + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {} + + @Override + public void startWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void startWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void shutdownWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void replayWorkflowExecution( + @Nonnull Worker worker, + @Nonnull io.temporal.common.WorkflowExecutionHistory history, + @Nonnull ReplayCallback next) + throws Exception { + next.replay(worker, history); + } + } + + // Plugin for testing ordering + public static class OrderedWorkerPlugin implements WorkerPlugin { + private final String name; + private final List orderRecord; + + public OrderedWorkerPlugin(String name, List orderRecord) { + this.name = name; + this.orderRecord = orderRecord; + } + + @Nonnull + @Override + public String getName() { + return "test.ordered." + name; + } + + @Override + public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) { + orderRecord.add(name); + } + + @Override + public void configureWorker( + @Nonnull String taskQueue, @Nonnull WorkerOptions.Builder builder) {} + + @Override + public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {} + + @Override + public void startWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void shutdownWorker( + @Nonnull String taskQueue, + @Nonnull Worker worker, + @Nonnull BiConsumer next) { + next.accept(taskQueue, worker); + } + + @Override + public void startWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void shutdownWorkerFactory( + @Nonnull WorkerFactory factory, @Nonnull Consumer next) { + next.accept(factory); + } + + @Override + public void replayWorkflowExecution( + @Nonnull Worker worker, + @Nonnull io.temporal.common.WorkflowExecutionHistory history, + @Nonnull ReplayCallback next) + throws Exception { + next.replay(worker, history); + } + } +}