diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc9d1e623..098a600b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,9 @@ to docs, or any other relevant information. set by Temporal SDK in worker context. - Spring Boot: the `workers` attribute of `@WorkflowImpl`, `@ActivityImpl` and `@NexusServiceImpl` now resolves property placeholders such as `${my.worker.name}`, as `taskQueues` already does. +- Spring Boot now provides singleton `NexusClient` beans for the root and configured non-root namespaces, + with namespace-specific data converters and `TemporalOptionsCustomizer` + customization. `NexusClientInterceptor` beans are automatically registered for the root client. - `Workflow.sleep(Duration, TimerOptions)` and `Workflow.await(Duration, TimerOptions, Supplier)` let workflows set a summary on the timer behind a sleep or a timed await, like `Workflow.newTimer(Duration, TimerOptions)` already does. - Add nonblocking `Async.await` overloads whose promises complete when a workflow condition becomes true or a timeout expires. diff --git a/temporal-spring-boot-autoconfigure/README.md b/temporal-spring-boot-autoconfigure/README.md index a8dc71c98a..e694dbdb68 100644 --- a/temporal-spring-boot-autoconfigure/README.md +++ b/temporal-spring-boot-autoconfigure/README.md @@ -2,6 +2,23 @@ For documentation on the Temporal Spring Boot Integration, please visit [https://docs.temporal.io/develop/java/spring-boot-integration](https://docs.temporal.io/develop/java/spring-boot-integration) +# NexusClient + +Inject `NexusClient` for the root namespace, or use `@Qualifier("NexusClient")` +for a non-root namespace (namespace replaces alias when omitted). Each client is a singleton +using its namespace's connection and data converter. + +```java + @Bean + TemporalOptionsCustomizer nexusClientCustomizer() { + return builder -> builder.setIdentity("my-application"); + } +``` + +For non-root customization, name the bean `NexusClientCustomizer`. +`NexusClientInterceptor` beans apply to the root client only; configure non-root interceptors +through the namespace's options customizer. + # Running Multiple Name Space (experimental) Along with the root namespace, you can configure multiple non-root namespaces in the application.yml file. Different namespaces can have different configurations including but not limited to different connection options, registered workflows/activities, data converters etc. diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/TemporalOptionsCustomizer.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/TemporalOptionsCustomizer.java index 772f352c5c..55f13aafaf 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/TemporalOptionsCustomizer.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/TemporalOptionsCustomizer.java @@ -18,8 +18,9 @@ * * @param Temporal Options Builder to customize. Respected types: {@link * WorkflowServiceStubsOptions.Builder}, {@link WorkflowClientOptions.Builder}, {@link - * TestEnvironmentOptions.Builder}, {@link WorkerOptions.Builder}, {@link - * WorkerFactoryOptions.Builder}, {@link WorkflowImplementationOptions.Builder} + * io.temporal.client.NexusClientOptions.Builder}, {@link TestEnvironmentOptions.Builder}, + * {@link WorkerOptions.Builder}, {@link WorkerFactoryOptions.Builder}, {@link + * WorkflowImplementationOptions.Builder} * @see WorkerOptionsCustomizer to get access to a name and task queue of a worker that's being * customized */ 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 2aeef3b18d..28965ff647 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 @@ -3,6 +3,8 @@ import com.google.common.base.MoreObjects; import com.uber.m3.tally.Scope; import io.opentracing.Tracer; +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientOptions; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowClientPlugin; @@ -134,6 +136,8 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { } List> scheduleClientCustomizers = findBeanByNameSpaceForTemporalCustomizer(beanPrefix, ScheduleClientOptions.Builder.class); + List> nexusClientCustomizers = + findBeanByNameSpaceForTemporalCustomizer(beanPrefix, NexusClientOptions.Builder.class); List> workflowImplementationCustomizers = findBeanByNameSpaceForTemporalCustomizer( @@ -169,11 +173,14 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { workflowImplementationCustomizers, workflowClientPlugins, scheduleClientPlugins, - workerPlugins); + workerPlugins, + null, + nexusClientCustomizers); ClientTemplate clientTemplate = namespaceTemplate.getClientTemplate(); WorkflowClient workflowClient = clientTemplate.getWorkflowClient(); ScheduleClient scheduleClient = clientTemplate.getScheduleClient(); + NexusClient nexusClient = clientTemplate.getNexusClient(); WorkersTemplate workersTemplate = namespaceTemplate.getWorkersTemplate(); WorkerFactory workerFactory = workersTemplate.getWorkerFactory(); @@ -192,6 +199,7 @@ private void injectBeanByNonRootNamespace(NonRootNamespaceProperties ns) { beanPrefix + WorkflowClient.class.getSimpleName(), workflowClient); beanFactory.registerSingleton( beanPrefix + ScheduleClient.class.getSimpleName(), scheduleClient); + beanFactory.registerSingleton(beanPrefix + NexusClient.class.getSimpleName(), nexusClient); beanFactory.registerSingleton(beanPrefix + WorkerFactory.class.getSimpleName(), workerFactory); } 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 9ff3908bb5..e3881b9405 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 @@ -1,6 +1,8 @@ package io.temporal.spring.boot.autoconfigure; import io.opentracing.Tracer; +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientOptions; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowClientPlugin; @@ -8,6 +10,7 @@ import io.temporal.client.schedules.ScheduleClientOptions; import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; +import io.temporal.common.interceptors.NexusClientInterceptor; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -93,7 +96,11 @@ public NamespaceTemplate rootNamespaceTemplate( workflowImplementationCustomizerMap, @Autowired(required = false) @Nullable List workflowClientPlugins, @Autowired(required = false) @Nullable List scheduleClientPlugins, - @Autowired(required = false) @Nullable List workerPlugins) { + @Autowired(required = false) @Nullable List workerPlugins, + @Autowired(required = false) @Nullable List nexusClientInterceptors, + @Autowired(required = false) @Nullable + Map> + nexusClientCustomizerMap) { DataConverter chosenDataConverter = AutoConfigurationUtils.chooseDataConverter(dataConverters, mainDataConverter, properties); List chosenClientInterceptors = @@ -119,6 +126,9 @@ public NamespaceTemplate rootNamespaceTemplate( List> scheduleCustomizer = AutoConfigurationUtils.chooseTemporalCustomizerBeans( beanFactory, scheduleCustomizerMap, ScheduleClientOptions.Builder.class, properties); + List> nexusClientCustomizers = + AutoConfigurationUtils.chooseTemporalCustomizerBeans( + beanFactory, nexusClientCustomizerMap, NexusClientOptions.Builder.class, properties); List> workflowImplementationCustomizer = AutoConfigurationUtils.chooseTemporalCustomizerBeans( @@ -166,7 +176,9 @@ public NamespaceTemplate rootNamespaceTemplate( workflowImplementationCustomizer, filteredClientPlugins, filteredSchedulePlugins, - filteredWorkerPlugins); + filteredWorkerPlugins, + nexusClientInterceptors, + nexusClientCustomizers); } /** Client */ @@ -189,6 +201,12 @@ public ScheduleClient scheduleClient(ClientTemplate clientTemplate) { return clientTemplate.getScheduleClient(); } + @Primary + @Bean(name = "temporalNexusClient") + public NexusClient nexusClient(ClientTemplate clientTemplate) { + return clientTemplate.getNexusClient(); + } + /** Workers */ @Primary @Bean(name = "temporalWorkersTemplate") diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/NonRootNamespaceProperties.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/NonRootNamespaceProperties.java index a43f0c1a4f..16aa0cce45 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/NonRootNamespaceProperties.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/NonRootNamespaceProperties.java @@ -16,6 +16,7 @@ public class NonRootNamespaceProperties extends NamespaceProperties { * - WorkersTemplate
* - WorkflowClient
* - ScheduleClient
+ * - NexusClient
* - WorkerFactory
* You guys can use this alias to get the beans.
* for example if you set spring.temporal.namespace[0].alias=foo
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 c9f68cb620..92b5275aab 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 @@ -2,6 +2,8 @@ import com.google.common.base.Preconditions; import io.opentracing.Tracer; +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientOptions; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowClientPlugin; @@ -9,6 +11,7 @@ import io.temporal.client.schedules.ScheduleClientOptions; import io.temporal.client.schedules.ScheduleClientPlugin; import io.temporal.common.converter.DataConverter; +import io.temporal.common.interceptors.NexusClientInterceptor; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; import io.temporal.serviceclient.WorkflowServiceStubs; @@ -19,12 +22,14 @@ public class ClientTemplate { private final @Nonnull WorkflowClientOptionsTemplate optionsTemplate; + private final @Nonnull NexusClientOptionsTemplate nexusOptionsTemplate; private final @Nullable WorkflowServiceStubs workflowServiceStubs; // if not null, we work with an environment with defined test server private final @Nullable TestWorkflowEnvironmentAdapter testWorkflowEnvironment; private WorkflowClient workflowClient; private ScheduleClient scheduleClient; + private NexusClient nexusClient; public ClientTemplate( @Nonnull String namespace, @@ -37,7 +42,10 @@ public ClientTemplate( @Nullable List> clientCustomizers, @Nullable List> scheduleCustomizers, @Nullable List workflowClientPlugins, - @Nullable List scheduleClientPlugins) { + @Nullable List scheduleClientPlugins, + @Nullable List nexusClientInterceptors, + @Nullable + List> nexusClientCustomizers) { this.optionsTemplate = new WorkflowClientOptionsTemplate( namespace, @@ -49,39 +57,13 @@ public ClientTemplate( scheduleCustomizers, workflowClientPlugins, scheduleClientPlugins); + this.nexusOptionsTemplate = + new NexusClientOptionsTemplate( + namespace, dataConverter, nexusClientInterceptors, nexusClientCustomizers); 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(); @@ -96,6 +78,20 @@ public ScheduleClient getScheduleClient() { return scheduleClient; } + public NexusClient getNexusClient() { + if (nexusClient == null) { + WorkflowServiceStubs serviceStubs = + testWorkflowEnvironment != null + ? testWorkflowEnvironment.getWorkflowClient().getWorkflowServiceStubs() + : workflowServiceStubs; + Preconditions.checkState( + serviceStubs != null, "ClientTemplate was created without workflowServiceStubs"); + this.nexusClient = + NexusClient.newInstance(serviceStubs, nexusOptionsTemplate.createNexusClientOptions()); + } + return nexusClient; + } + private WorkflowClient createWorkflowClient() { if (testWorkflowEnvironment != null) { return testWorkflowEnvironment.getWorkflowClient(); 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 f8966d6ac2..b0ccf1ec1a 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 @@ -1,11 +1,13 @@ package io.temporal.spring.boot.autoconfigure.template; import io.opentracing.Tracer; +import io.temporal.client.NexusClientOptions; 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.NexusClientInterceptor; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -42,6 +44,9 @@ public class NamespaceTemplate { private final @Nullable List workflowClientPlugins; private final @Nullable List scheduleClientPlugins; private final @Nullable List workerPlugins; + private final @Nullable List nexusClientInterceptors; + private final @Nullable List> + nexusClientCustomizers; private ClientTemplate clientTemplate; private WorkersTemplate workersTemplate; @@ -65,7 +70,10 @@ public NamespaceTemplate( workflowImplementationCustomizers, @Nullable List workflowClientPlugins, @Nullable List scheduleClientPlugins, - @Nullable List workerPlugins) { + @Nullable List workerPlugins, + @Nullable List nexusClientInterceptors, + @Nullable + List> nexusClientCustomizers) { this.namespaceProperties = namespaceProperties; this.workflowServiceStubs = workflowServiceStubs; this.dataConverter = dataConverter; @@ -83,6 +91,8 @@ public NamespaceTemplate( this.workflowClientPlugins = workflowClientPlugins; this.scheduleClientPlugins = scheduleClientPlugins; this.workerPlugins = workerPlugins; + this.nexusClientInterceptors = nexusClientInterceptors; + this.nexusClientCustomizers = nexusClientCustomizers; } public ClientTemplate getClientTemplate() { @@ -99,7 +109,9 @@ public ClientTemplate getClientTemplate() { clientCustomizers, scheduleCustomizers, workflowClientPlugins, - scheduleClientPlugins); + scheduleClientPlugins, + nexusClientInterceptors, + nexusClientCustomizers); } return clientTemplate; } diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NexusClientOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NexusClientOptionsTemplate.java new file mode 100644 index 0000000000..3c37ddeeb5 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/NexusClientOptionsTemplate.java @@ -0,0 +1,43 @@ +package io.temporal.spring.boot.autoconfigure.template; + +import io.temporal.client.NexusClientOptions; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.interceptors.NexusClientInterceptor; +import io.temporal.spring.boot.TemporalOptionsCustomizer; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class NexusClientOptionsTemplate { + private final @Nonnull String namespace; + private final @Nullable DataConverter dataConverter; + private final @Nullable List interceptors; + private final @Nullable List> customizers; + + public NexusClientOptionsTemplate( + @Nonnull String namespace, + @Nullable DataConverter dataConverter, + @Nullable List interceptors, + @Nullable List> customizers) { + this.namespace = namespace; + this.dataConverter = dataConverter; + this.interceptors = interceptors; + this.customizers = customizers; + } + + public NexusClientOptions createNexusClientOptions() { + NexusClientOptions.Builder options = NexusClientOptions.newBuilder().setNamespace(namespace); + if (dataConverter != null) { + options.setDataConverter(dataConverter); + } + if (interceptors != null) { + options.setInterceptors(interceptors); + } + if (customizers != null) { + for (TemporalOptionsCustomizer customizer : customizers) { + options = customizer.customize(options); + } + } + return options.build(); + } +} 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 7f62bf97bb..e8ee822b9d 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 @@ -1,11 +1,13 @@ package io.temporal.spring.boot.autoconfigure.template; import io.opentracing.Tracer; +import io.temporal.client.NexusClientOptions; 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.NexusClientInterceptor; import io.temporal.common.interceptors.ScheduleClientInterceptor; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; @@ -46,7 +48,10 @@ public NonRootNamespaceTemplate( workflowImplementationCustomizers, @Nullable List workflowClientPlugins, @Nullable List scheduleClientPlugins, - @Nullable List workerPlugins) { + @Nullable List workerPlugins, + @Nullable List nexusClientInterceptors, + @Nullable + List> nexusClientCustomizers) { super( namespaceProperties, workflowServiceStubs, @@ -63,52 +68,12 @@ public NonRootNamespaceTemplate( workflowImplementationCustomizers, workflowClientPlugins, scheduleClientPlugins, - workerPlugins); + workerPlugins, + nexusClientInterceptors, + nexusClientCustomizers); 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/test/java/io/temporal/spring/boot/autoconfigure/NexusClientAutoConfigurationTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NexusClientAutoConfigurationTest.java new file mode 100644 index 0000000000..0c2b9e77df --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/NexusClientAutoConfigurationTest.java @@ -0,0 +1,249 @@ +package io.temporal.spring.boot.autoconfigure; + +import static org.junit.jupiter.api.Assertions.*; + +import io.temporal.client.NexusClient; +import io.temporal.client.NexusClientOptions; +import io.temporal.client.NexusOperationExecutionCount; +import io.temporal.client.WorkflowClient; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.DefaultDataConverter; +import io.temporal.common.interceptors.NexusClientCallsInterceptor; +import io.temporal.common.interceptors.NexusClientCallsInterceptorBase; +import io.temporal.common.interceptors.NexusClientInterceptor; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.spring.boot.TemporalOptionsCustomizer; +import io.temporal.spring.boot.autoconfigure.template.ClientTemplate; +import io.temporal.testing.TestWorkflowEnvironment; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; + +public class NexusClientAutoConfigurationTest { + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + TestServerAutoConfiguration.class, + ServiceStubsAutoConfiguration.class, + RootNamespaceAutoConfiguration.class, + NonRootNamespaceAutoConfiguration.class)) + .withPropertyValues( + "spring.temporal.connection.target=127.0.0.1:7233", + "spring.temporal.start-workers=false"); + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void clientIsAvailableWithoutWorkersOrCustomizers(boolean testServer) { + contextRunner + .withPropertyValues("spring.temporal.test-server.enabled=" + testServer) + .run( + context -> { + assertNull(context.getStartupFailure()); + NexusClient client = context.getBean(NexusClient.class); + assertSame(client, context.getBean("temporalNexusClient")); + assertSame(client, context.getBean(ClientTemplate.class).getNexusClient()); + assertSame( + context.getBean(WorkflowClient.class).getWorkflowServiceStubs().getRawChannel(), + client.getWorkflowServiceStubs().getRawChannel()); + assertNotNull(client.getHandle("operation-id", null, String.class)); + assertNotNull(client.newUntypedNexusServiceClient("endpoint", "service")); + }); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void clientsUseNamespaceSpecificOptionsAndConnections(boolean testServer) { + contextRunner + .withUserConfiguration(CustomizationConfiguration.class) + .withPropertyValues( + "spring.temporal.test-server.enabled=" + testServer, + "spring.temporal.namespace=root", + "spring.temporal.namespaces[0].namespace=applications", + "spring.temporal.namespaces[0].alias=apps", + "spring.temporal.namespaces[1].namespace=support", + "spring.temporal.namespaces[1].connection.target=127.0.0.1:7234") + .run( + context -> { + assertNull(context.getStartupFailure()); + assertEquals(3, context.getBeansOfType(NexusClient.class).size()); + NexusClient root = context.getBean(NexusClient.class); + NexusClient apps = context.getBean("appsNexusClient", NexusClient.class); + NexusClient support = context.getBean("supportNexusClient", NexusClient.class); + assertSame(root, context.getBean("temporalNexusClient")); + NexusClients injected = context.getBean(NexusClients.class); + assertSame(root, injected.root); + assertSame(apps, injected.apps); + assertSame(support, injected.support); + assertNotSame(root, apps); + assertNotSame(apps, support); + assertSame( + apps, + context.getBean("appsClientTemplate", ClientTemplate.class).getNexusClient()); + assertSame( + support, + context.getBean("supportClientTemplate", ClientTemplate.class).getNexusClient()); + assertSame( + context + .getBean("appsWorkflowServiceStubs", WorkflowServiceStubs.class) + .getRawChannel(), + apps.getWorkflowServiceStubs().getRawChannel()); + assertSame( + context + .getBean("supportWorkflowServiceStubs", WorkflowServiceStubs.class) + .getRawChannel(), + support.getWorkflowServiceStubs().getRawChannel()); + if (testServer) { + WorkflowServiceStubs testStubs = + context + .getBean(TestWorkflowEnvironment.class) + .getWorkflowClient() + .getWorkflowServiceStubs(); + assertSame( + testStubs.getRawChannel(), root.getWorkflowServiceStubs().getRawChannel()); + assertSame( + testStubs.getRawChannel(), apps.getWorkflowServiceStubs().getRawChannel()); + assertSame( + testStubs.getRawChannel(), support.getWorkflowServiceStubs().getRawChannel()); + } else { + assertEquals( + "127.0.0.1:7233", apps.getWorkflowServiceStubs().getOptions().getTarget()); + assertEquals( + "127.0.0.1:7234", support.getWorkflowServiceStubs().getOptions().getTarget()); + } + + OptionsRecorder recorder = context.getBean(OptionsRecorder.class); + assertEquals(3, recorder.options.size()); + assertEquals(4, recorder.customizerCalls); + NexusClientOptions rootOptions = recorder.options.get("root"); + assertEquals("root", rootOptions.getNamespace()); + assertEquals("root-identity", rootOptions.getIdentity()); + assertSame(context.getBean("rootDataConverter"), rootOptions.getDataConverter()); + assertEquals( + Collections.singletonList(context.getBean(NexusClientInterceptor.class)), + rootOptions.getInterceptors()); + NexusClientOptions appsOptions = recorder.options.get("apps"); + assertEquals("applications", appsOptions.getNamespace()); + assertEquals("apps-identity", appsOptions.getIdentity()); + assertSame(context.getBean("appsDataConverter"), appsOptions.getDataConverter()); + assertTrue(appsOptions.getInterceptors().isEmpty()); + NexusClientOptions supportOptions = recorder.options.get("support"); + assertEquals("support", supportOptions.getNamespace()); + assertEquals("support-identity", supportOptions.getIdentity()); + assertSame( + context.getBean("supportDataConverter"), supportOptions.getDataConverter()); + + assertEquals(42, root.countNexusOperationExecutions(null).getCount()); + assertEquals(1, recorder.interceptorCalls.get()); + }); + } + + static class OptionsRecorder { + final Map options = new HashMap<>(); + final AtomicInteger interceptorCalls = new AtomicInteger(); + int customizerCalls; + + NexusClientOptions.Builder record(String name, NexusClientOptions.Builder options) { + customizerCalls++; + assertNull(this.options.put(name, options.build())); + return options; + } + } + + static class CustomizationConfiguration { + @Bean + OptionsRecorder optionsRecorder() { + return new OptionsRecorder(); + } + + @Bean + DataConverter rootDataConverter() { + return DefaultDataConverter.newDefaultInstance(); + } + + @Bean + DataConverter appsDataConverter() { + return DefaultDataConverter.newDefaultInstance(); + } + + @Bean + DataConverter supportDataConverter() { + return DefaultDataConverter.newDefaultInstance(); + } + + @Bean + NexusClientInterceptor nexusClientInterceptor(OptionsRecorder recorder) { + return next -> + new NexusClientCallsInterceptorBase(next) { + @Override + public NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput + countNexusOperationExecutions( + NexusClientCallsInterceptor.CountNexusOperationExecutionsInput input) { + recorder.interceptorCalls.incrementAndGet(); + return new NexusClientCallsInterceptor.CountNexusOperationExecutionsOutput( + new NexusOperationExecutionCount(42, Collections.emptyList())); + } + }; + } + + @Bean + @Order(2) + TemporalOptionsCustomizer lastNexusClientCustomizer( + OptionsRecorder recorder) { + return options -> { + assertEquals("first-identity", options.build().getIdentity()); + return recorder.record("root", options.setIdentity("root-identity")); + }; + } + + @Bean + @Order(1) + TemporalOptionsCustomizer firstNexusClientCustomizer( + OptionsRecorder recorder) { + return options -> { + recorder.customizerCalls++; + return NexusClientOptions.newBuilder(options.build()).setIdentity("first-identity"); + }; + } + + @Bean + TemporalOptionsCustomizer appsNexusClientCustomizer( + OptionsRecorder recorder) { + return options -> recorder.record("apps", options.setIdentity("apps-identity")); + } + + @Bean + TemporalOptionsCustomizer supportNexusClientCustomizer( + OptionsRecorder recorder) { + return options -> recorder.record("support", options.setIdentity("support-identity")); + } + + @Bean + NexusClients nexusClients( + NexusClient root, + @Qualifier("appsNexusClient") NexusClient apps, + @Qualifier("supportNexusClient") NexusClient support) { + return new NexusClients(root, apps, support); + } + } + + static class NexusClients { + final NexusClient root; + final NexusClient apps; + final NexusClient support; + + NexusClients(NexusClient root, NexusClient apps, NexusClient support) { + this.root = root; + this.apps = apps; + this.support = support; + } + } +}