From eff3ca20fcf8579f6fe38d8345d588425c0034b6 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Tue, 15 Oct 2024 12:26:28 -0700 Subject: [PATCH] Merge Nexus into Master (#2270) Nexus Support to the Java SDK --- build.gradle | 1 + docker/github/docker-compose.yaml | 2 + docker/github/dynamicconfig/development.yaml | 10 +- .../workflow/NexusOperationOptionsExt.kt | 41 + .../workflow/NexusServiceOptionsExt.kt | 41 + .../nexus/NexusOperationOptionsExtTest.kt | 42 + .../nexus/NexusServiceOptionsExtTest.kt | 67 ++ .../temporal/workflow/KotlinAsyncNexusTest.kt | 112 +++ temporal-sdk/build.gradle | 1 + .../client/WorkflowClientInternalImpl.java | 13 + .../io/temporal/client/WorkflowException.java | 3 +- .../client/WorkflowInvocationHandler.java | 45 +- .../io/temporal/client/WorkflowOptions.java | 132 ++- .../java/io/temporal/client/WorkflowStub.java | 8 + .../io/temporal/client/WorkflowStubImpl.java | 6 + .../WorkflowOutboundCallsInterceptor.java | 91 +- .../WorkflowOutboundCallsInterceptorBase.java | 6 + .../failure/DefaultFailureConverter.java | 32 +- .../failure/NexusOperationFailure.java | 99 +++ .../client/NexusStartWorkflowRequest.java | 66 ++ .../client/WorkflowClientInternal.java | 4 + .../client/WorkflowClientRequestFactory.java | 12 + .../internal/common/InternalUtils.java | 65 ++ .../internal/common}/LinkConverter.java | 16 +- .../temporal/internal/common/NexusUtil.java | 57 ++ .../internal/common/ProtoEnumNameUtils.java | 13 +- .../common/WorkflowExecutionUtils.java | 6 + .../temporal/internal/logging/LoggerTag.java | 2 + .../nexus/CurrentNexusOperationContext.java | 55 ++ .../internal/nexus/NexusInternal.java | 31 + .../nexus/NexusOperationContextImpl.java | 57 ++ .../internal/nexus/NexusTaskHandlerImpl.java | 332 ++++++++ .../internal/nexus/PayloadSerializer.java | 60 ++ .../replay/ReplayWorkflowContext.java | 6 + .../replay/ReplayWorkflowContextImpl.java | 16 +- .../CancelNexusOperationStateMachine.java | 99 +++ .../NexusOperationStateMachine.java | 270 ++++++ .../statemachines/WorkflowStateMachines.java | 81 +- .../sync/NexusOperationExecutionImpl.java | 38 + .../sync/NexusOperationHandleImpl.java | 46 + .../sync/NexusServiceInvocationHandler.java | 72 ++ .../internal/sync/NexusServiceStubImpl.java | 123 +++ .../internal/sync/StartNexusCallInternal.java | 86 ++ .../internal/sync/SyncWorkflowContext.java | 102 ++- .../internal/sync/WorkflowInternal.java | 43 +- .../internal/worker/NexusPollTask.java | 131 +++ .../temporal/internal/worker/NexusTask.java | 60 ++ .../internal/worker/NexusTaskHandler.java | 65 ++ .../temporal/internal/worker/NexusWorker.java | 368 ++++++++ .../internal/worker/PollTaskExecutor.java | 7 - .../internal/worker/ShutdownManager.java | 19 +- .../internal/worker/SyncNexusWorker.java | 125 +++ .../internal/worker/SyncWorkflowWorker.java | 6 +- .../worker/WorkerThreadsNameHelper.java | 5 + .../internal/worker/WorkflowTask.java | 2 +- .../main/java/io/temporal/nexus/Nexus.java | 51 ++ .../temporal/nexus/NexusOperationContext.java | 42 + ...ronousWorkflowClientOperationFunction.java | 41 + .../WorkflowClientOperationHandlers.java | 73 ++ .../io/temporal/nexus/WorkflowHandle.java | 284 +++++++ .../temporal/nexus/WorkflowHandleFactory.java | 42 + .../temporal/nexus/WorkflowHandleInvoker.java | 28 + .../temporal/nexus/WorkflowMethodFactory.java | 45 + .../nexus/WorkflowMethodMethodInvoker.java | 43 + .../temporal/nexus/WorkflowRunOperation.java | 106 +++ .../nexus/WorkflowStubHandleInvoker.java | 42 + .../payload/context/SerializationContext.java | 3 + .../java/io/temporal/worker/MetricsType.java | 23 + .../main/java/io/temporal/worker/Worker.java | 69 +- .../io/temporal/worker/WorkerMetricsTag.java | 3 +- .../io/temporal/worker/WorkerOptions.java | 67 ++ .../worker/WorkflowImplementationOptions.java | 82 +- .../worker/tuning/CompositeTuner.java | 12 +- .../temporal/worker/tuning/NexusSlotInfo.java | 105 +++ .../tuning/ResourceBasedSlotSupplier.java | 30 +- .../worker/tuning/ResourceBasedTuner.java | 34 +- .../temporal/worker/tuning/WorkerTuner.java | 6 + .../workflow/NexusOperationExecution.java | 32 + .../workflow/NexusOperationHandle.java | 46 + .../workflow/NexusOperationOptions.java | 125 +++ .../workflow/NexusServiceOptions.java | 194 +++++ .../temporal/workflow/NexusServiceStub.java | 112 +++ .../java/io/temporal/workflow/Workflow.java | 62 ++ .../temporal/client/functional/StartTest.java | 4 +- .../internal/common}/LinkConverterTest.java | 6 +- .../internal/common/NexusUtilTest.java | 38 + .../nexus/NexusTaskHandlerImplTest.java | 230 +++++ .../internal/nexus/PayloadSerializerTest.java | 45 + .../CancelNexusOperationStateMachineTest.java | 157 ++++ .../NexusOperationStateMachineTest.java | 802 ++++++++++++++++++ .../statemachines/TestHistoryBuilder.java | 30 +- .../WorkflowSlotGrpcInterceptedTests.java | 11 +- .../internal/worker/WorkflowSlotTests.java | 121 ++- .../worker/WorkflowSlotsSmallSizeTests.java | 11 +- .../worker/WorkerIsNotGettingStartedTest.java | 25 +- .../io/temporal/worker/WorkerOptionsTest.java | 16 +- .../worker/WorkerPollerThreadCountTest.java | 39 +- .../worker/WorkerRegistrationTest.java | 81 ++ ...a => CleanActivityWorkerShutdownTest.java} | 2 +- .../CleanNexusWorkerShutdownTest.java | 165 ++++ .../EagerWorkflowTaskDispatchTest.java | 5 +- .../io/temporal/workflow/ExecuteTest.java | 5 +- .../workflow/WorkflowIdReusePolicyTest.java | 6 +- .../EagerActivityDispatchingTest.java | 11 +- .../ChildAsyncWorkflowTest.java | 2 +- .../nexus/AsyncWorkflowOperationTest.java | 140 +++ .../workflow/nexus/BaseNexusTest.java | 86 ++ .../nexus/CancelAsyncOperationTest.java | 112 +++ .../nexus/OperationFailureConversionTest.java | 127 +++ .../nexus/ParallelWorkflowOperationTest.java | 121 +++ .../nexus/SyncClientOperationTest.java | 160 ++++ .../nexus/SyncOperationCancelledTest.java | 108 +++ .../workflow/nexus/SyncOperationFailTest.java | 127 +++ .../workflow/nexus/SyncOperationStubTest.java | 90 ++ .../nexus/SyncOperationTimeoutTest.java | 86 ++ .../TerminateWorkflowAsyncOperationTest.java | 117 +++ .../nexus/UntypedSyncOperationStubTest.java | 95 +++ .../nexus/WorkflowHandleFuncTest.java | 173 ++++ .../nexus/WorkflowHandleProcTest.java | 174 ++++ .../nexus/WorkflowOperationLinkingTest.java | 147 ++++ .../shared/TestMultiArgWorkflowFunctions.java | 4 +- .../workflow/shared/TestNexusServices.java | 57 ++ .../updateTest/UpdateWithStartTest.java | 2 +- ...testAsyncWorkflowOperationTestHistory.json | 555 ++++++++++++ ...tParallelWorkflowOperationTestHistory.json | 742 ++++++++++++++++ .../temporal/serviceclient/LongPollUtil.java | 1 + .../io/temporal/serviceclient/MetricsTag.java | 2 + temporal-shaded/build.gradle | 3 + .../spring/boot/NexusServiceImpl.java | 48 ++ .../properties/WorkerProperties.java | 30 +- .../template/WorkerOptionsTemplate.java | 4 + .../template/WorkersTemplate.java | 187 ++++ .../AutoDiscoveryByTaskQueueResolverTest.java | 21 +- .../AutoDiscoveryByTaskQueueTest.java | 16 +- .../AutoDiscoveryByWorkerNameTest.java | 17 +- .../OptionalWorkerOptionsTest.java | 8 + .../autoconfigure/RegisteredInfoTest.java | 14 + .../bytaskqueue/TestNexusService.java | 30 + .../bytaskqueue/TestNexusServiceImpl.java | 38 + .../bytaskqueue/TestWorkflowImpl.java | 15 + .../byworkername/TestNexusService.java | 30 + .../byworkername/TestNexusServiceImpl.java | 39 + .../byworkername/TestWorkflowImpl.java | 14 + .../src/test/resources/application.yml | 2 + .../internal/testservice/StateMachines.java | 6 +- .../testservice/TestWorkflowService.java | 3 +- .../functional/NexusWorkflowTest.java | 2 +- .../sync/DummySyncWorkflowContext.java | 9 + .../TestActivityEnvironmentInternal.java | 6 + .../testing/TestWorkflowEnvironment.java | 20 + .../TestWorkflowEnvironmentInternal.java | 38 + .../testing/TestWorkflowExtension.java | 73 +- .../io/temporal/testing/TestWorkflowRule.java | 63 ++ .../testing/TimeLockingInterceptor.java | 5 + .../testing/internal/SDKTestWorkflowRule.java | 10 + .../testing/internal/TestServiceUtils.java | 41 + .../internal/TracingWorkerInterceptor.java | 9 + .../junit5/TestWorkflowExtensionTest.java | 35 +- 158 files changed, 10784 insertions(+), 190 deletions(-) create mode 100644 temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt create mode 100644 temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt create mode 100644 temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt create mode 100644 temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt create mode 100644 temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt create mode 100644 temporal-sdk/src/main/java/io/temporal/failure/NexusOperationFailure.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/client/NexusStartWorkflowRequest.java rename {temporal-test-server/src/main/java/io/temporal/internal/testservice => temporal-sdk/src/main/java/io/temporal/internal/common}/LinkConverter.java (86%) create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/common/NexusUtil.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/nexus/CurrentNexusOperationContext.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusInternal.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusOperationContextImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/nexus/PayloadSerializer.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachine.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/statemachines/NexusOperationStateMachine.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationExecutionImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationHandleImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceInvocationHandler.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceStubImpl.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/sync/StartNexusCallInternal.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/worker/NexusPollTask.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTask.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTaskHandler.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java create mode 100644 temporal-sdk/src/main/java/io/temporal/internal/worker/SyncNexusWorker.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/Nexus.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/NexusOperationContext.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/SynchronousWorkflowClientOperationFunction.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowClientOperationHandlers.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandle.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleFactory.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleInvoker.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodFactory.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodMethodInvoker.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowRunOperation.java create mode 100644 temporal-sdk/src/main/java/io/temporal/nexus/WorkflowStubHandleInvoker.java create mode 100644 temporal-sdk/src/main/java/io/temporal/worker/tuning/NexusSlotInfo.java create mode 100644 temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationExecution.java create mode 100644 temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationHandle.java create mode 100644 temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java create mode 100644 temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java create mode 100644 temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceStub.java rename {temporal-test-server/src/test/java/io/temporal/internal/testservice => temporal-sdk/src/test/java/io/temporal/internal/common}/LinkConverterTest.java (98%) create mode 100644 temporal-sdk/src/test/java/io/temporal/internal/common/NexusUtilTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/internal/nexus/PayloadSerializerTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachineTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/internal/statemachines/NexusOperationStateMachineTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/worker/WorkerRegistrationTest.java rename temporal-sdk/src/test/java/io/temporal/worker/shutdown/{CleanWorkerShutdownTest.java => CleanActivityWorkerShutdownTest.java} (99%) create mode 100644 temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanNexusWorkerShutdownTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/AsyncWorkflowOperationTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/BaseNexusTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/CancelAsyncOperationTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/ParallelWorkflowOperationTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncClientOperationTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationStubTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationTimeoutTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/TerminateWorkflowAsyncOperationTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/UntypedSyncOperationStubTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleFuncTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleProcTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowOperationLinkingTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/shared/TestNexusServices.java create mode 100644 temporal-sdk/src/test/resources/testAsyncWorkflowOperationTestHistory.json create mode 100644 temporal-sdk/src/test/resources/testParallelWorkflowOperationTestHistory.json create mode 100644 temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusService.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusServiceImpl.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusService.java create mode 100644 temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java diff --git a/build.gradle b/build.gradle index 46f471eb8..aaa5c9ae5 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ ext { // Platforms grpcVersion = '1.54.1' // [1.38.0,) Needed for io.grpc.protobuf.services.HealthStatusManager jacksonVersion = '2.14.2' // [2.9.0,) + nexusVersion = '0.2.0-alpha' // we don't upgrade to 1.10.x because it requires kotlin 1.6. Users may use 1.10.x in their environments though. micrometerVersion = project.hasProperty("edgeDepsTest") ? '1.10.5' : '1.9.9' // [1.0.0,) diff --git a/docker/github/docker-compose.yaml b/docker/github/docker-compose.yaml index 650007bf9..a2d270b00 100644 --- a/docker/github/docker-compose.yaml +++ b/docker/github/docker-compose.yaml @@ -34,11 +34,13 @@ services: - "6934:6934" - "6935:6935" - "6939:6939" + - "7243:7243" environment: - "CASSANDRA_SEEDS=cassandra" - "ENABLE_ES=true" - "ES_SEEDS=elasticsearch" - "DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml" + - "FRONTEND_HTTP_PORT=7243" depends_on: - cassandra - elasticsearch diff --git a/docker/github/dynamicconfig/development.yaml b/docker/github/dynamicconfig/development.yaml index 39a54ab0f..7a0b9db0e 100644 --- a/docker/github/dynamicconfig/development.yaml +++ b/docker/github/dynamicconfig/development.yaml @@ -19,4 +19,12 @@ history.MaxBufferedQueryCount: worker.buildIdScavengerEnabled: - value: true worker.removableBuildIdDurationSinceDefault: - - value: 1 \ No newline at end of file + - value: 1 +system.enableNexus: + - value: true +component.nexusoperations.callback.endpoint.template: + - value: http://localhost:7243/namespaces/{{.NamespaceName}}/nexus/callback +component.callbacks.allowedAddresses: + - value: + - Pattern: "localhost:7243" + AllowInsecure: true \ No newline at end of file diff --git a/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt new file mode 100644 index 000000000..f7fb8369c --- /dev/null +++ b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow + +import io.temporal.kotlin.TemporalDsl + +/** + * @see NexusOperationOptions + */ +inline fun NexusOperationOptions( + options: @TemporalDsl NexusOperationOptions.Builder.() -> Unit +): NexusOperationOptions { + return NexusOperationOptions.newBuilder().apply(options).build() +} + +/** + * Create a new instance of [NexusOperationOptions], optionally overriding some of its properties. + */ +inline fun NexusOperationOptions.copy( + overrides: @TemporalDsl NexusOperationOptions.Builder.() -> Unit +): NexusOperationOptions { + return toBuilder().apply(overrides).build() +} diff --git a/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt new file mode 100644 index 000000000..0d3c76649 --- /dev/null +++ b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow + +import io.temporal.kotlin.TemporalDsl + +/** + * @see NexusServiceOptions + */ +inline fun NexusServiceOptions( + options: @TemporalDsl NexusServiceOptions.Builder.() -> Unit +): NexusServiceOptions { + return NexusServiceOptions.newBuilder().apply(options).build() +} + +/** + * Create a new instance of [NexusServiceOptions], optionally overriding some of its properties. + */ +inline fun NexusServiceOptions.copy( + overrides: @TemporalDsl NexusServiceOptions.Builder.() -> Unit +): NexusServiceOptions { + return toBuilder().apply(overrides).build() +} diff --git a/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt new file mode 100644 index 000000000..6dd2af4b5 --- /dev/null +++ b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus + +import io.temporal.workflow.NexusOperationOptions +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Duration + +class NexusOperationOptionsExtTest { + + @Test + fun `OperationOptions DSL should be equivalent to builder`() { + val dslOperationOptions = NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofMinutes(1)) + } + + val builderOperationOptions = NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofMinutes(1)) + .build() + + assertEquals(builderOperationOptions, dslOperationOptions) + } +} diff --git a/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt new file mode 100644 index 000000000..fe89d0d81 --- /dev/null +++ b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus + +import io.temporal.workflow.NexusOperationOptions +import io.temporal.workflow.NexusServiceOptions +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Duration + +class NexusServiceOptionsExtTest { + + @Test + fun `ServiceOptions DSL should be equivalent to builder`() { + val dslServiceOptions = NexusServiceOptions { + setEndpoint("TestEndpoint") + setOperationOptions( + NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofMinutes(1)) + } + ) + setOperationMethodOptions( + mapOf( + "test" to NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofMinutes(2)) + } + ) + ) + } + + val builderServiceOptions = NexusServiceOptions.newBuilder() + .setEndpoint("TestEndpoint") + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofMinutes(1)) + .build() + ) + .setOperationMethodOptions( + mapOf( + "test" to NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofMinutes(2)) + .build() + ) + ) + .build() + + assertEquals(builderServiceOptions, dslServiceOptions) + } +} diff --git a/temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt b/temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt new file mode 100644 index 000000000..53ec4c109 --- /dev/null +++ b/temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow + +import io.nexusrpc.Operation +import io.nexusrpc.Service +import io.nexusrpc.handler.OperationContext +import io.nexusrpc.handler.OperationHandler +import io.nexusrpc.handler.OperationImpl +import io.nexusrpc.handler.OperationStartDetails +import io.nexusrpc.handler.ServiceImpl +import io.nexusrpc.handler.SynchronousOperationFunction +import io.temporal.client.WorkflowClientOptions +import io.temporal.client.WorkflowOptions +import io.temporal.common.converter.DefaultDataConverter +import io.temporal.common.converter.JacksonJsonPayloadConverter +import io.temporal.common.converter.KotlinObjectMapperFactory +import io.temporal.internal.async.FunctionWrappingUtil +import io.temporal.internal.sync.AsyncInternal +import io.temporal.testing.internal.SDKTestWorkflowRule +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import java.time.Duration + +class KotlinAsyncNexusTest { + + @Rule + @JvmField + var testWorkflowRule: SDKTestWorkflowRule = SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(WorkflowImpl::class.java) + .setNexusServiceImplementation(TestNexusServiceImpl()) + .setWorkflowClientOptions( + WorkflowClientOptions.newBuilder() + .setDataConverter(DefaultDataConverter(JacksonJsonPayloadConverter(KotlinObjectMapperFactory.new()))) + .build() + ) + .build() + + @Service + interface TestNexusService { + @Operation + fun operation(): String? + } + + @ServiceImpl(service = TestNexusService::class) + class TestNexusServiceImpl { + @OperationImpl + fun operation(): OperationHandler { + // Implemented inline + return OperationHandler.sync( + SynchronousOperationFunction { ctx: OperationContext, details: OperationStartDetails, _: Void? -> "Hello Kotlin" } + ) + } + } + + @WorkflowInterface + interface TestWorkflow { + @WorkflowMethod + fun execute() + } + + class WorkflowImpl : TestWorkflow { + override fun execute() { + val nexusService = Workflow.newNexusServiceStub( + TestNexusService::class.java, + NexusServiceOptions { + setOperationOptions( + NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofSeconds(10)) + } + ) + } + ) + assertTrue( + "This has to be true to make Async.function(nexusService::operation) work correctly as expected", + AsyncInternal.isAsync(nexusService::operation) + ) + assertTrue( + "This has to be true to make Async.function(nexusService::operation) work correctly as expected", + AsyncInternal.isAsync(FunctionWrappingUtil.temporalJavaFunctionalWrapper(nexusService::operation)) + ) + Async.function(nexusService::operation).get() + } + } + + @Test + fun asyncNexusWorkflowTest() { + val client = testWorkflowRule.workflowClient + val options = WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.taskQueue).build() + val workflowStub = client.newWorkflowStub(TestWorkflow::class.java, options) + workflowStub.execute() + } +} diff --git a/temporal-sdk/build.gradle b/temporal-sdk/build.gradle index 4bd3fdddc..050612a25 100644 --- a/temporal-sdk/build.gradle +++ b/temporal-sdk/build.gradle @@ -8,6 +8,7 @@ dependencies { api project(':temporal-serviceclient') api "com.google.code.gson:gson:$gsonVersion" api "io.micrometer:micrometer-core" + api "io.nexusrpc:nexus-sdk:$nexusVersion" implementation "com.google.guava:guava:$guavaVersion" api "com.fasterxml.jackson.core:jackson-databind" diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java index 0e02603f0..f53daeb2f 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowClientInternalImpl.java @@ -37,6 +37,7 @@ import io.temporal.common.interceptors.WorkflowClientCallsInterceptor; import io.temporal.common.interceptors.WorkflowClientInterceptor; import io.temporal.internal.WorkflowThreadMarker; +import io.temporal.internal.client.NexusStartWorkflowRequest; import io.temporal.internal.client.RootWorkflowClientInvoker; import io.temporal.internal.client.WorkerFactoryRegistry; import io.temporal.internal.client.WorkflowClientInternal; @@ -567,4 +568,16 @@ public void registerWorkerFactory(WorkerFactory workerFactory) { public void deregisterWorkerFactory(WorkerFactory workerFactory) { workerFactoryRegistry.deregister(workerFactory); } + + @Override + public WorkflowExecution startNexus(NexusStartWorkflowRequest request, Functions.Proc workflow) { + enforceNonWorkflowThread(); + WorkflowInvocationHandler.initAsyncInvocation(InvocationType.START_NEXUS, request); + try { + workflow.apply(); + return WorkflowInvocationHandler.getAsyncInvocationResult(WorkflowExecution.class); + } finally { + WorkflowInvocationHandler.closeAsyncInvocation(); + } + } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowException.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowException.java index 7afd311c5..df089ce2e 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowException.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowException.java @@ -59,7 +59,6 @@ private static String getMessage(WorkflowExecution execution, String workflowTyp + execution.getWorkflowId() + "', runId='" + execution.getRunId() - + (workflowType == null ? "" : "', workflowType='" + workflowType + '\'') - + '}'; + + (workflowType == null ? "" : "', workflowType='" + workflowType + '\''); } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowInvocationHandler.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowInvocationHandler.java index 0bfa46048..1497ed91a 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowInvocationHandler.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowInvocationHandler.java @@ -20,6 +20,8 @@ package io.temporal.client; +import static io.temporal.internal.common.InternalUtils.createNexusBoundStub; + import com.google.common.base.Defaults; import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.api.enums.v1.WorkflowIdReusePolicy; @@ -30,6 +32,7 @@ import io.temporal.common.metadata.POJOWorkflowInterfaceMetadata; import io.temporal.common.metadata.POJOWorkflowMethodMetadata; import io.temporal.common.metadata.WorkflowMethodType; +import io.temporal.internal.client.NexusStartWorkflowRequest; import io.temporal.internal.sync.StubMarker; import io.temporal.workflow.QueryMethod; import io.temporal.workflow.SignalMethod; @@ -37,8 +40,7 @@ import io.temporal.workflow.WorkflowMethod; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.util.Objects; -import java.util.Optional; +import java.util.*; /** * Dynamic implementation of a strongly typed workflow interface that can be used to start, signal @@ -51,6 +53,7 @@ public enum InvocationType { START, EXECUTE, SIGNAL_WITH_START, + START_NEXUS, UPDATE_WITH_START } @@ -87,6 +90,9 @@ static void initAsyncInvocation(InvocationType type, T value) { } else if (type == InvocationType.SIGNAL_WITH_START) { SignalWithStartBatchRequest batch = (SignalWithStartBatchRequest) value; invocationContext.set(new SignalWithStartWorkflowInvocationHandler(batch)); + } else if (type == InvocationType.START_NEXUS) { + NexusStartWorkflowRequest request = (NexusStartWorkflowRequest) value; + invocationContext.set(new StartNexusOperationInvocationHandler(request)); } else if (type == InvocationType.UPDATE_WITH_START) { UpdateWithStartWorkflowOperation operation = (UpdateWithStartWorkflowOperation) value; invocationContext.set(new UpdateWithStartInvocationHandler(operation)); @@ -400,6 +406,41 @@ public R getResult(Class resultClass) { } } + private static class StartNexusOperationInvocationHandler implements SpecificInvocationHandler { + private final NexusStartWorkflowRequest request; + private Object result; + + public StartNexusOperationInvocationHandler(NexusStartWorkflowRequest request) { + this.request = request; + } + + @Override + public InvocationType getInvocationType() { + return InvocationType.START_NEXUS; + } + + @Override + public void invoke( + POJOWorkflowInterfaceMetadata workflowMetadata, + WorkflowStub untyped, + Method method, + Object[] args) { + WorkflowMethod workflowMethod = method.getAnnotation(WorkflowMethod.class); + if (workflowMethod == null) { + throw new IllegalArgumentException( + "Only on a method annotated with @WorkflowMethod can be used to start a Nexus operation."); + } + + result = createNexusBoundStub(untyped, request).start(args); + } + + @Override + @SuppressWarnings("unchecked") + public R getResult(Class resultClass) { + return (R) result; + } + } + private static class UpdateWithStartInvocationHandler implements SpecificInvocationHandler { enum State { diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowOptions.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowOptions.java index fe7954c76..022a8f088 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowOptions.java @@ -21,6 +21,8 @@ package io.temporal.client; import com.google.common.base.Objects; +import io.temporal.api.common.v1.Callback; +import io.temporal.api.common.v1.Link; import io.temporal.api.enums.v1.WorkflowIdConflictPolicy; import io.temporal.api.enums.v1.WorkflowIdReusePolicy; import io.temporal.common.*; @@ -79,6 +81,9 @@ public static WorkflowOptions merge( .setWorkflowIdConflictPolicy(o.getWorkflowIdConflictPolicy()) .setStaticSummary(o.getStaticSummary()) .setStaticDetails(o.getStaticDetails()) + .setRequestId(o.getRequestId()) + .setCompletionCallbacks(o.getCompletionCallbacks()) + .setLinks(o.getLinks()) .validateBuildWithDefaults(); } @@ -112,12 +117,18 @@ public static final class Builder { private Duration startDelay; - private WorkflowIdConflictPolicy workflowIdConflictpolicy; + private WorkflowIdConflictPolicy workflowIdConflictPolicy; private String staticSummary; private String staticDetails; + private String requestId; + + private List completionCallbacks; + + private List links; + private Builder() {} private Builder(WorkflowOptions options) { @@ -138,9 +149,12 @@ private Builder(WorkflowOptions options) { this.contextPropagators = options.contextPropagators; this.disableEagerExecution = options.disableEagerExecution; this.startDelay = options.startDelay; - this.workflowIdConflictpolicy = options.workflowIdConflictpolicy; + this.workflowIdConflictPolicy = options.workflowIdConflictPolicy; this.staticSummary = options.staticSummary; this.staticDetails = options.staticDetails; + this.requestId = options.requestId; + this.completionCallbacks = options.completionCallbacks; + this.links = options.links; } /** @@ -191,8 +205,8 @@ public Builder setWorkflowIdReusePolicy(WorkflowIdReusePolicy workflowIdReusePol *
  • TerminateExisting Terminate the running workflow before starting a new one. * */ - public Builder setWorkflowIdConflictPolicy(WorkflowIdConflictPolicy workflowIdConflictpolicy) { - this.workflowIdConflictpolicy = workflowIdConflictpolicy; + public Builder setWorkflowIdConflictPolicy(WorkflowIdConflictPolicy workflowIdConflictPolicy) { + this.workflowIdConflictPolicy = workflowIdConflictPolicy; return this; } @@ -413,6 +427,39 @@ public Builder setStaticDetails(String staticDetails) { return this; } + /** + * A unique identifier for this start request. + * + *

    WARNING: Not intended for User Code. + */ + @Experimental + public Builder setRequestId(String requestId) { + this.requestId = requestId; + return this; + } + + /** + * Callbacks to be called by the server when this workflow reaches a terminal state. + * + *

    WARNING: Not intended for User Code. + */ + @Experimental + public Builder setCompletionCallbacks(List completionCallbacks) { + this.completionCallbacks = completionCallbacks; + return this; + } + + /** + * Links to be associated with the workflow. + * + *

    WARNING: Not intended for User Code. + */ + @Experimental + public Builder setLinks(List links) { + this.links = links; + return this; + } + public WorkflowOptions build() { return new WorkflowOptions( workflowId, @@ -429,9 +476,12 @@ public WorkflowOptions build() { contextPropagators, disableEagerExecution, startDelay, - workflowIdConflictpolicy, + workflowIdConflictPolicy, staticSummary, - staticDetails); + staticDetails, + requestId, + completionCallbacks, + links); } /** @@ -453,9 +503,12 @@ public WorkflowOptions validateBuildWithDefaults() { contextPropagators, disableEagerExecution, startDelay, - workflowIdConflictpolicy, + workflowIdConflictPolicy, staticSummary, - staticDetails); + staticDetails, + requestId, + completionCallbacks, + links); } } @@ -487,12 +540,18 @@ public WorkflowOptions validateBuildWithDefaults() { private final Duration startDelay; - private final WorkflowIdConflictPolicy workflowIdConflictpolicy; + private final WorkflowIdConflictPolicy workflowIdConflictPolicy; private final String staticSummary; private final String staticDetails; + private final String requestId; + + private final List completionCallbacks; + + private final List links; + private WorkflowOptions( String workflowId, WorkflowIdReusePolicy workflowIdReusePolicy, @@ -508,9 +567,12 @@ private WorkflowOptions( List contextPropagators, boolean disableEagerExecution, Duration startDelay, - WorkflowIdConflictPolicy workflowIdConflictpolicy, + WorkflowIdConflictPolicy workflowIdConflictPolicy, String staticSummary, - String staticDetails) { + String staticDetails, + String requestId, + List completionCallbacks, + List links) { this.workflowId = workflowId; this.workflowIdReusePolicy = workflowIdReusePolicy; this.workflowRunTimeout = workflowRunTimeout; @@ -525,9 +587,12 @@ private WorkflowOptions( this.contextPropagators = contextPropagators; this.disableEagerExecution = disableEagerExecution; this.startDelay = startDelay; - this.workflowIdConflictpolicy = workflowIdConflictpolicy; + this.workflowIdConflictPolicy = workflowIdConflictPolicy; this.staticSummary = staticSummary; this.staticDetails = staticDetails; + this.requestId = requestId; + this.completionCallbacks = completionCallbacks; + this.links = links; } public String getWorkflowId() { @@ -596,13 +661,30 @@ public boolean isDisableEagerExecution() { } public WorkflowIdConflictPolicy getWorkflowIdConflictPolicy() { - return workflowIdConflictpolicy; + return workflowIdConflictPolicy; + } + + @Experimental + public String getRequestId() { + return requestId; + } + + @Experimental + public @Nullable List getCompletionCallbacks() { + return completionCallbacks; + } + + @Experimental + public @Nullable List getLinks() { + return links; } + @Experimental public String getStaticSummary() { return staticSummary; } + @Experimental public String getStaticDetails() { return staticDetails; } @@ -630,9 +712,12 @@ public boolean equals(Object o) { && Objects.equal(contextPropagators, that.contextPropagators) && Objects.equal(disableEagerExecution, that.disableEagerExecution) && Objects.equal(startDelay, that.startDelay) - && Objects.equal(workflowIdConflictpolicy, that.workflowIdConflictpolicy) + && Objects.equal(workflowIdConflictPolicy, that.workflowIdConflictPolicy) && Objects.equal(staticSummary, that.staticSummary) - && Objects.equal(staticDetails, that.staticDetails); + && Objects.equal(staticDetails, that.staticDetails) + && Objects.equal(requestId, that.requestId) + && Objects.equal(completionCallbacks, that.completionCallbacks) + && Objects.equal(links, that.links); } @Override @@ -652,9 +737,12 @@ public int hashCode() { contextPropagators, disableEagerExecution, startDelay, - workflowIdConflictpolicy, + workflowIdConflictPolicy, staticSummary, - staticDetails); + staticDetails, + requestId, + completionCallbacks, + links); } @Override @@ -691,12 +779,18 @@ public String toString() { + disableEagerExecution + ", startDelay=" + startDelay - + ", workflowIdConflictpolicy=" - + workflowIdConflictpolicy + + ", workflowIdConflictPolicy=" + + workflowIdConflictPolicy + ", staticSummary=" + staticSummary + ", staticDetails=" + staticDetails + + ", requestId=" + + requestId + + ", completionCallbacks=" + + completionCallbacks + + ", links=" + + links + '}'; } } diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowStub.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowStub.java index a8cf4a6ae..b99da3e45 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowStub.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowStub.java @@ -383,4 +383,12 @@ CompletableFuture getResultAsync( void terminate(@Nullable String reason, Object... details); Optional getOptions(); + + /** + * Creates a new stub that can be used to start a new run of the same workflow type with the same + * options. + * + * @param options new options to use for the stub + */ + WorkflowStub newInstance(WorkflowOptions options); } diff --git a/temporal-sdk/src/main/java/io/temporal/client/WorkflowStubImpl.java b/temporal-sdk/src/main/java/io/temporal/client/WorkflowStubImpl.java index 91dde73de..d3b1a245f 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/WorkflowStubImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/WorkflowStubImpl.java @@ -447,6 +447,12 @@ public Optional getOptions() { return Optional.ofNullable(options); } + @Override + public WorkflowStub newInstance(WorkflowOptions options) { + return new WorkflowStubImpl( + clientOptions, workflowClientInvoker, workflowType.orElse(null), options); + } + private void checkStarted() { if (execution.get() == null || execution.get().getWorkflowId() == null) { throw new IllegalStateException("Null workflowId. Was workflow started?"); diff --git a/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptor.java b/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptor.java index 7cb961644..22e339bcc 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptor.java +++ b/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptor.java @@ -30,10 +30,7 @@ import io.temporal.workflow.Functions.Func; import java.lang.reflect.Type; import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.UUID; +import java.util.*; import java.util.function.BiPredicate; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -261,6 +258,89 @@ public Promise getWorkflowExecution() { } } + @Experimental + final class ExecuteNexusOperationInput { + private final String endpoint; + private final String service; + private final String operation; + private final Class resultClass; + private final Type resultType; + private final Object arg; + private final NexusOperationOptions options; + private final Map headers; + + public ExecuteNexusOperationInput( + String endpoint, + String service, + String operation, + Class resultClass, + Type resultType, + Object arg, + NexusOperationOptions options, + Map headers) { + this.endpoint = endpoint; + this.service = service; + this.operation = operation; + this.resultClass = resultClass; + this.resultType = resultType; + this.arg = arg; + this.options = options; + this.headers = headers; + } + + public String getService() { + return service; + } + + public String getOperation() { + return operation; + } + + public Object getArg() { + return arg; + } + + public Class getResultClass() { + return resultClass; + } + + public Type getResultType() { + return resultType; + } + + public String getEndpoint() { + return endpoint; + } + + public NexusOperationOptions getOptions() { + return options; + } + + public Map getHeaders() { + return headers; + } + } + + @Experimental + final class ExecuteNexusOperationOutput { + private final Promise result; + private final Promise operationExecution; + + public ExecuteNexusOperationOutput( + Promise result, Promise operationExecution) { + this.result = result; + this.operationExecution = operationExecution; + } + + public Promise getResult() { + return result; + } + + public Promise getOperationExecution() { + return operationExecution; + } + } + final class SignalExternalInput { private final WorkflowExecution execution; private final String signalName; @@ -575,6 +655,9 @@ public DynamicUpdateHandler getHandler() { ChildWorkflowOutput executeChildWorkflow(ChildWorkflowInput input); + @Experimental + ExecuteNexusOperationOutput executeNexusOperation(ExecuteNexusOperationInput input); + Random newRandom(); SignalExternalOutput signalExternalWorkflow(SignalExternalInput input); diff --git a/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptorBase.java b/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptorBase.java index e2d422355..e6d6e9db6 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptorBase.java +++ b/temporal-sdk/src/main/java/io/temporal/common/interceptors/WorkflowOutboundCallsInterceptorBase.java @@ -57,6 +57,12 @@ public ChildWorkflowOutput executeChildWorkflow(ChildWorkflowInput inp return next.executeChildWorkflow(input); } + @Override + public ExecuteNexusOperationOutput executeNexusOperation( + ExecuteNexusOperationInput input) { + return next.executeNexusOperation(input); + } + @Override public Random newRandom() { return next.newRandom(); diff --git a/temporal-sdk/src/main/java/io/temporal/failure/DefaultFailureConverter.java b/temporal-sdk/src/main/java/io/temporal/failure/DefaultFailureConverter.java index 35b8e3654..87b54e761 100644 --- a/temporal-sdk/src/main/java/io/temporal/failure/DefaultFailureConverter.java +++ b/temporal-sdk/src/main/java/io/temporal/failure/DefaultFailureConverter.java @@ -26,15 +26,7 @@ import io.temporal.api.common.v1.ActivityType; import io.temporal.api.common.v1.Payloads; import io.temporal.api.common.v1.WorkflowType; -import io.temporal.api.failure.v1.ActivityFailureInfo; -import io.temporal.api.failure.v1.ApplicationFailureInfo; -import io.temporal.api.failure.v1.CanceledFailureInfo; -import io.temporal.api.failure.v1.ChildWorkflowExecutionFailureInfo; -import io.temporal.api.failure.v1.Failure; -import io.temporal.api.failure.v1.ResetWorkflowFailureInfo; -import io.temporal.api.failure.v1.ServerFailureInfo; -import io.temporal.api.failure.v1.TerminatedFailureInfo; -import io.temporal.api.failure.v1.TimeoutFailureInfo; +import io.temporal.api.failure.v1.*; import io.temporal.client.ActivityCanceledException; import io.temporal.common.converter.DataConverter; import io.temporal.common.converter.EncodedValues; @@ -183,6 +175,18 @@ private TemporalFailure failureToExceptionImpl(Failure failure, DataConverter da info.getRetryState(), cause); } + case NEXUS_OPERATION_EXECUTION_FAILURE_INFO: + { + NexusOperationFailureInfo info = failure.getNexusOperationExecutionFailureInfo(); + return new NexusOperationFailure( + failure.getMessage(), + info.getScheduledEventId(), + info.getEndpoint(), + info.getService(), + info.getOperation(), + info.getOperationId(), + cause); + } case FAILUREINFO_NOT_SET: default: throw new IllegalArgumentException("Failure info not set"); @@ -289,6 +293,16 @@ private Failure exceptionToFailure(Throwable throwable) { } else if (throwable instanceof ActivityCanceledException) { CanceledFailureInfo.Builder info = CanceledFailureInfo.newBuilder(); failure.setCanceledFailureInfo(info); + } else if (throwable instanceof NexusOperationFailure) { + NexusOperationFailure no = (NexusOperationFailure) throwable; + NexusOperationFailureInfo.Builder info = + NexusOperationFailureInfo.newBuilder() + .setScheduledEventId(no.getScheduledEventId()) + .setEndpoint(no.getEndpoint()) + .setService(no.getService()) + .setOperation(no.getOperation()) + .setOperationId(no.getOperationId()); + failure.setNexusOperationExecutionFailureInfo(info); } else { ApplicationFailureInfo.Builder info = ApplicationFailureInfo.newBuilder() diff --git a/temporal-sdk/src/main/java/io/temporal/failure/NexusOperationFailure.java b/temporal-sdk/src/main/java/io/temporal/failure/NexusOperationFailure.java new file mode 100644 index 000000000..634435bb4 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/failure/NexusOperationFailure.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.failure; + +import io.temporal.common.Experimental; + +/** + * Contains information about a Nexus operation failure. Always contains the original reason for the + * failure as its cause. For example if a Nexus operation timed out the cause is {@link + * TimeoutFailure}. + * + *

    This exception is only expected to be thrown by the Temporal SDK. + */ +@Experimental +public final class NexusOperationFailure extends TemporalFailure { + private final long scheduledEventId; + private final String endpoint; + private final String service; + private final String operation; + private final String operationId; + + public NexusOperationFailure( + String message, + long scheduledEventId, + String endpoint, + String service, + String operation, + String operationId, + Throwable cause) { + super( + getMessage(message, scheduledEventId, endpoint, service, operation, operationId), + message, + cause); + this.scheduledEventId = scheduledEventId; + this.endpoint = endpoint; + this.service = service; + this.operation = operation; + this.operationId = operationId; + } + + public static String getMessage( + String originalMessage, + long scheduledEventId, + String endpoint, + String service, + String operation, + String operationId) { + return "Nexus Operation with operation='" + + operation + + "service='" + + service + + "' endpoint='" + + endpoint + + "' failed: '" + + originalMessage + + "'. " + + "scheduledEventId=" + + scheduledEventId + + (operationId == null ? "" : ", operationId=" + operationId); + } + + public long getScheduledEventId() { + return scheduledEventId; + } + + public String getEndpoint() { + return endpoint; + } + + public String getService() { + return service; + } + + public String getOperation() { + return operation; + } + + public String getOperationId() { + return operationId; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/NexusStartWorkflowRequest.java b/temporal-sdk/src/main/java/io/temporal/internal/client/NexusStartWorkflowRequest.java new file mode 100644 index 000000000..382004803 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/NexusStartWorkflowRequest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.client; + +import io.nexusrpc.Link; +import java.util.List; +import java.util.Map; + +public final class NexusStartWorkflowRequest { + private final String requestId; + private final String callbackUrl; + private final Map callbackHeaders; + private final String taskQueue; + private final List links; + + public NexusStartWorkflowRequest( + String requestId, + String callbackUrl, + Map callbackHeaders, + String taskQueue, + List links) { + this.requestId = requestId; + this.callbackUrl = callbackUrl; + this.callbackHeaders = callbackHeaders; + this.taskQueue = taskQueue; + this.links = links; + } + + public String getRequestId() { + return requestId; + } + + public String getCallbackUrl() { + return callbackUrl; + } + + public Map getCallbackHeaders() { + return callbackHeaders; + } + + public String getTaskQueue() { + return taskQueue; + } + + public List getLinks() { + return links; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientInternal.java b/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientInternal.java index 83f27b8bb..cafba9f24 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientInternal.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientInternal.java @@ -20,8 +20,10 @@ package io.temporal.internal.client; +import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.client.WorkflowClient; import io.temporal.worker.WorkerFactory; +import io.temporal.workflow.Functions; /** * From OOP point of view, there is no reason for this interface not to extend {@link @@ -35,4 +37,6 @@ public interface WorkflowClientInternal { void registerWorkerFactory(WorkerFactory workerFactory); void deregisterWorkerFactory(WorkerFactory workerFactory); + + WorkflowExecution startNexus(NexusStartWorkflowRequest request, Functions.Proc workflow); } diff --git a/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientRequestFactory.java b/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientRequestFactory.java index e9382a8c1..dc8dadd61 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientRequestFactory.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/client/WorkflowClientRequestFactory.java @@ -88,6 +88,18 @@ StartWorkflowExecutionRequest.Builder newStartWorkflowExecutionRequest( request.setWorkflowIdConflictPolicy(options.getWorkflowIdConflictPolicy()); } + if (options.getRequestId() != null) { + request.setRequestId(options.getRequestId()); + } + + if (options.getCompletionCallbacks() != null) { + options.getCompletionCallbacks().forEach(request::addCompletionCallbacks); + } + + if (options.getLinks() != null) { + options.getLinks().forEach(request::addLinks); + } + String taskQueue = options.getTaskQueue(); if (taskQueue != null && !taskQueue.isEmpty()) { request.setTaskQueue(TaskQueue.newBuilder().setName(taskQueue).build()); diff --git a/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java b/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java index babc0b0eb..80bb28af0 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java @@ -21,11 +21,21 @@ package io.temporal.internal.common; import com.google.common.base.Defaults; +import io.temporal.api.common.v1.Callback; import io.temporal.api.enums.v1.TaskQueueKind; import io.temporal.api.taskqueue.v1.TaskQueue; +import io.temporal.client.WorkflowOptions; +import io.temporal.client.WorkflowStub; +import io.temporal.internal.client.NexusStartWorkflowRequest; +import java.util.Arrays; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Utility functions shared by the implementation code. */ public final class InternalUtils { + private static final Logger log = LoggerFactory.getLogger(InternalUtils.class); + public static TaskQueue createStickyTaskQueue( String stickyTaskQueueName, String normalTaskQueueName) { return TaskQueue.newBuilder() @@ -49,6 +59,61 @@ public static Object getValueOrDefault(Object value, Class valueClass) { return Defaults.defaultValue(valueClass); } + /** + * Creates a new stub that is bound to the same workflow as the given stub, but with the Nexus + * callback URL and headers set. + * + * @param stub the stub to create a new stub from + * @param request the request containing the Nexus callback URL and headers + * @return a new stub bound to the same workflow as the given stub, but with the Nexus callback + * URL and headers set + */ + public static WorkflowStub createNexusBoundStub( + WorkflowStub stub, NexusStartWorkflowRequest request) { + if (!stub.getOptions().isPresent()) { + throw new IllegalArgumentException("Options are expected to be set on the stub"); + } + WorkflowOptions options = stub.getOptions().get(); + WorkflowOptions.Builder nexusWorkflowOptions = + WorkflowOptions.newBuilder() + .setRequestId(request.getRequestId()) + .setCompletionCallbacks( + Arrays.asList( + Callback.newBuilder() + .setNexus( + Callback.Nexus.newBuilder() + .setUrl(request.getCallbackUrl()) + .putAllHeader(request.getCallbackHeaders()) + .build()) + .build())); + if (options.getTaskQueue() == null) { + nexusWorkflowOptions.setTaskQueue(request.getTaskQueue()); + } + if (request.getLinks() != null) { + nexusWorkflowOptions.setLinks( + request.getLinks().stream() + .map( + (link) -> { + if (io.temporal.api.common.v1.Link.WorkflowEvent.getDescriptor() + .getFullName() + .equals(link.getType())) { + io.temporal.api.nexus.v1.Link nexusLink = + io.temporal.api.nexus.v1.Link.newBuilder() + .setType(link.getType()) + .setUrl(link.getUri().toString()) + .build(); + return LinkConverter.nexusLinkToWorkflowEvent(nexusLink); + } else { + log.warn("ignoring unsupported link data type: {}", link.getType()); + return null; + } + }) + .filter(link -> link != null) + .collect(Collectors.toList())); + } + return stub.newInstance(nexusWorkflowOptions.build()); + } + /** Prohibit instantiation */ private InternalUtils() {} } diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/LinkConverter.java b/temporal-sdk/src/main/java/io/temporal/internal/common/LinkConverter.java similarity index 86% rename from temporal-test-server/src/main/java/io/temporal/internal/testservice/LinkConverter.java rename to temporal-sdk/src/main/java/io/temporal/internal/common/LinkConverter.java index fac2fe174..c6c2367c9 100644 --- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/LinkConverter.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/common/LinkConverter.java @@ -18,7 +18,10 @@ * limitations under the License. */ -package io.temporal.internal.testservice; +package io.temporal.internal.common; + +import static io.temporal.internal.common.ProtoEnumNameUtils.EVENT_TYPE_PREFIX; +import static io.temporal.internal.common.ProtoEnumNameUtils.simplifiedToUniqueName; import io.temporal.api.common.v1.Link; import io.temporal.api.enums.v1.EventType; @@ -32,7 +35,7 @@ public class LinkConverter { - private static final Logger log = LoggerFactory.getLogger(StateMachines.class); + private static final Logger log = LoggerFactory.getLogger(LinkConverter.class); private static final String linkPathFormat = "temporal:///namespaces/%s/workflows/%s/%s/history"; @@ -113,7 +116,14 @@ public static Link nexusLinkToWorkflowEvent(io.temporal.api.nexus.v1.Link nexusL eventRef.setEventId(Long.parseLong(param[1])); continue; case "eventType": - eventRef.setEventType(EventType.valueOf(param[1])); + // Have to handle the SCREAMING_CASE enum or the traditional temporal PascalCase enum + // to EventType + if (param[1].startsWith(EVENT_TYPE_PREFIX)) { + eventRef.setEventType(EventType.valueOf(param[1])); + } else { + eventRef.setEventType( + EventType.valueOf(simplifiedToUniqueName(param[1], EVENT_TYPE_PREFIX))); + } } } we.setEventRef(eventRef); diff --git a/temporal-sdk/src/main/java/io/temporal/internal/common/NexusUtil.java b/temporal-sdk/src/main/java/io/temporal/internal/common/NexusUtil.java new file mode 100644 index 000000000..dc6ee53f5 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/common/NexusUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.common; + +import io.nexusrpc.Link; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; + +public class NexusUtil { + public static Duration parseRequestTimeout(String timeout) { + try { + if (timeout.endsWith("m")) { + return Duration.ofMillis( + Math.round(1e3 * 60 * Double.parseDouble(timeout.substring(0, timeout.length() - 1)))); + } else if (timeout.endsWith("ms")) { + return Duration.ofMillis( + Math.round(Double.parseDouble(timeout.substring(0, timeout.length() - 2)))); + } else if (timeout.endsWith("s")) { + return Duration.ofMillis( + Math.round(1e3 * Double.parseDouble(timeout.substring(0, timeout.length() - 1)))); + } else { + throw new IllegalArgumentException("Invalid timeout format: " + timeout); + } + } catch (NumberFormatException | NullPointerException e) { + throw new IllegalArgumentException("Invalid timeout format: " + timeout); + } + } + + public static Link nexusProtoLinkToLink(io.temporal.api.nexus.v1.Link nexusLink) + throws URISyntaxException { + return Link.newBuilder() + .setType(nexusLink.getType()) + .setUri(new URI(nexusLink.getUrl())) + .build(); + } + + private NexusUtil() {} +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/common/ProtoEnumNameUtils.java b/temporal-sdk/src/main/java/io/temporal/internal/common/ProtoEnumNameUtils.java index e11c1c18c..eb3a9e43a 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/common/ProtoEnumNameUtils.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/common/ProtoEnumNameUtils.java @@ -23,6 +23,7 @@ import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableMap; import io.temporal.api.enums.v1.CommandType; +import io.temporal.api.enums.v1.EventType; import io.temporal.api.enums.v1.IndexedValueType; import io.temporal.api.enums.v1.WorkflowTaskFailedCause; import java.util.Map; @@ -51,12 +52,18 @@ public class ProtoEnumNameUtils { public static final String WORKFLOW_TASK_FAILED_CAUSE_PREFIX = "WORKFLOW_TASK_FAILED_CAUSE_"; public static final String INDEXED_VALUE_TYPE_PREFIX = "INDEXED_VALUE_TYPE_"; public static final String COMMAND_TYPE_PREFIX = "COMMAND_TYPE_"; + public static final String EVENT_TYPE_PREFIX = "EVENT_TYPE_"; public static final Map, String> ENUM_CLASS_TO_PREFIX = ImmutableMap.of( - WorkflowTaskFailedCause.class, WORKFLOW_TASK_FAILED_CAUSE_PREFIX, - IndexedValueType.class, INDEXED_VALUE_TYPE_PREFIX, - CommandType.class, COMMAND_TYPE_PREFIX); + WorkflowTaskFailedCause.class, + WORKFLOW_TASK_FAILED_CAUSE_PREFIX, + IndexedValueType.class, + INDEXED_VALUE_TYPE_PREFIX, + CommandType.class, + COMMAND_TYPE_PREFIX, + EventType.class, + EVENT_TYPE_PREFIX); @Nonnull public static String uniqueToSimplifiedName(@Nonnull Enum enumm) { diff --git a/temporal-sdk/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java b/temporal-sdk/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java index 94d568930..7041750d6 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java @@ -316,6 +316,8 @@ public static boolean isCommandEvent(HistoryEvent event) { case EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_REJECTED: case EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED: case EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED: + case EVENT_TYPE_NEXUS_OPERATION_SCHEDULED: + case EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED: return true; default: return false; @@ -353,6 +355,10 @@ public static EventType getEventTypeForCommand(CommandType commandType) { return EventType.EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES; case COMMAND_TYPE_MODIFY_WORKFLOW_PROPERTIES: return EventType.EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED; + case COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION: + return EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED; + case COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION: + return EventType.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED; } throw new IllegalArgumentException("Unknown commandType"); } diff --git a/temporal-sdk/src/main/java/io/temporal/internal/logging/LoggerTag.java b/temporal-sdk/src/main/java/io/temporal/internal/logging/LoggerTag.java index fc8212a1c..8b42c8895 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/logging/LoggerTag.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/logging/LoggerTag.java @@ -40,4 +40,6 @@ private LoggerTag() {} public static final String ATTEMPT = "Attempt"; public static final String UPDATE_ID = "UpdateId"; public static final String UPDATE_NAME = "UpdateName"; + public static final String NEXUS_SERVICE = "NexusService"; + public static final String NEXUS_OPERATION = "NexusOperation"; } diff --git a/temporal-sdk/src/main/java/io/temporal/internal/nexus/CurrentNexusOperationContext.java b/temporal-sdk/src/main/java/io/temporal/internal/nexus/CurrentNexusOperationContext.java new file mode 100644 index 000000000..99ca97b3e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/nexus/CurrentNexusOperationContext.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.nexus; + +/** + * Thread local store of the context object passed to a nexus operation implementation. Not to be + * used directly. + */ +public final class CurrentNexusOperationContext { + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + public static NexusOperationContextImpl get() { + NexusOperationContextImpl result = CURRENT.get(); + if (result == null) { + throw new IllegalStateException( + "NexusOperationContext can be used only inside of nexus operation handler " + + "implementation methods and in the same thread that invoked the operation."); + } + return CURRENT.get(); + } + + public static void set(NexusOperationContextImpl context) { + if (context == null) { + throw new IllegalArgumentException("null context"); + } + if (CURRENT.get() != null) { + throw new IllegalStateException("current already set"); + } + CURRENT.set(context); + } + + public static void unset() { + CURRENT.set(null); + } + + private CurrentNexusOperationContext() {} +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusInternal.java b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusInternal.java new file mode 100644 index 000000000..56565f4ff --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusInternal.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.nexus; + +import io.temporal.nexus.NexusOperationContext; + +public final class NexusInternal { + private NexusInternal() {} + + public static NexusOperationContext getOperationContext() { + return CurrentNexusOperationContext.get(); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusOperationContextImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusOperationContextImpl.java new file mode 100644 index 000000000..b54c0867d --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusOperationContextImpl.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.nexus; + +import com.uber.m3.tally.Scope; +import io.temporal.client.WorkflowClient; +import io.temporal.nexus.NexusOperationContext; + +public class NexusOperationContextImpl implements NexusOperationContext { + private final String namespace; + private final String taskQueue; + private final WorkflowClient client; + private final Scope metricsScope; + + public NexusOperationContextImpl( + String namespace, String taskQueue, WorkflowClient client, Scope metricsScope) { + this.namespace = namespace; + this.taskQueue = taskQueue; + this.client = client; + this.metricsScope = metricsScope; + } + + @Override + public Scope getMetricsScope() { + return metricsScope; + } + + public WorkflowClient getWorkflowClient() { + return client; + } + + public String getTaskQueue() { + return taskQueue; + } + + public String getNamespace() { + return namespace; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java new file mode 100644 index 000000000..0714d472e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.nexus; + +import static io.temporal.internal.common.NexusUtil.nexusProtoLinkToLink; + +import com.google.protobuf.ByteString; +import com.uber.m3.tally.Scope; +import io.nexusrpc.FailureInfo; +import io.nexusrpc.Header; +import io.nexusrpc.OperationUnsuccessfulException; +import io.nexusrpc.handler.*; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.nexus.v1.*; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowException; +import io.temporal.common.converter.DataConverter; +import io.temporal.failure.ApplicationFailure; +import io.temporal.internal.common.NexusUtil; +import io.temporal.internal.worker.NexusTask; +import io.temporal.internal.worker.NexusTaskHandler; +import io.temporal.internal.worker.ShutdownManager; +import io.temporal.serviceclient.CheckedExceptionWrapper; +import io.temporal.worker.TypeAlreadyRegisteredException; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NexusTaskHandlerImpl implements NexusTaskHandler { + private static final Logger log = LoggerFactory.getLogger(NexusTaskHandlerImpl.class); + private final DataConverter dataConverter; + private final String namespace; + private final String taskQueue; + private final WorkflowClient client; + private ServiceHandler serviceHandler; + private final Map serviceImplInstances = + Collections.synchronizedMap(new HashMap<>()); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + public NexusTaskHandlerImpl( + WorkflowClient client, String namespace, String taskQueue, DataConverter dataConverter) { + this.client = client; + this.namespace = namespace; + this.taskQueue = taskQueue; + this.dataConverter = dataConverter; + } + + @Override + public boolean start() { + if (serviceImplInstances.isEmpty()) { + return false; + } + ServiceHandler.Builder serviceHandlerBuilder = + ServiceHandler.newBuilder().setSerializer(new PayloadSerializer(dataConverter)); + serviceImplInstances.forEach((name, instance) -> serviceHandlerBuilder.addInstance(instance)); + serviceHandler = serviceHandlerBuilder.build(); + return true; + } + + @Override + public Result handle(NexusTask task, Scope metricsScope) throws TimeoutException { + Request request = task.getResponse().getRequest(); + Map headers = request.getHeaderMap(); + if (headers == null) { + headers = Collections.emptyMap(); + } + + OperationContext.Builder ctx = OperationContext.newBuilder(); + headers.forEach(ctx::putHeader); + OperationMethodCanceller canceller = new OperationMethodCanceller(); + ctx.setMethodCanceller(canceller); + + ScheduledFuture timeoutTask = null; + AtomicBoolean timedOut = new AtomicBoolean(false); + try { + String timeoutString = headers.get(Header.REQUEST_TIMEOUT); + if (timeoutString != null) { + try { + Duration timeout = NexusUtil.parseRequestTimeout(timeoutString); + timeoutTask = + scheduler.schedule( + () -> { + timedOut.set(true); + canceller.cancel("timeout"); + }, + timeout.toMillis(), + java.util.concurrent.TimeUnit.MILLISECONDS); + } catch (IllegalArgumentException e) { + return new Result( + HandlerError.newBuilder() + .setErrorType(OperationHandlerException.ErrorType.BAD_REQUEST.toString()) + .setFailure( + Failure.newBuilder().setMessage("cannot parse request timeout").build()) + .build()); + } + } + + CurrentNexusOperationContext.set( + new NexusOperationContextImpl(namespace, taskQueue, client, metricsScope)); + + switch (request.getVariantCase()) { + case START_OPERATION: + StartOperationResponse startResponse = + handleStartOperation(ctx, request.getStartOperation()); + return new Result(Response.newBuilder().setStartOperation(startResponse).build()); + case CANCEL_OPERATION: + CancelOperationResponse cancelResponse = + handleCancelledOperation(ctx, request.getCancelOperation()); + return new Result(Response.newBuilder().setCancelOperation(cancelResponse).build()); + default: + return new Result( + HandlerError.newBuilder() + .setErrorType(OperationHandlerException.ErrorType.NOT_IMPLEMENTED.toString()) + .setFailure(Failure.newBuilder().setMessage("unknown request type").build()) + .build()); + } + } catch (OperationHandlerException e) { + return new Result( + HandlerError.newBuilder() + .setErrorType(e.getErrorType().toString()) + .setFailure(createFailure(e.getFailureInfo())) + .build()); + } catch (Throwable e) { + return new Result( + HandlerError.newBuilder() + .setErrorType(OperationHandlerException.ErrorType.INTERNAL.toString()) + .setFailure(Failure.newBuilder().setMessage(e.toString()).build()) + .build()); + } finally { + // If the task timed out, we should not send a response back to the server + if (timedOut.get()) { + throw new TimeoutException("Nexus task completed after timeout."); + } + canceller.cancel(""); + if (timeoutTask != null) { + timeoutTask.cancel(false); + } + CurrentNexusOperationContext.unset(); + } + } + + private Failure createFailure(FailureInfo failInfo) { + Failure.Builder failure = Failure.newBuilder(); + if (failInfo.getMessage() != null) { + failure.setMessage(failInfo.getMessage()); + } + if (failInfo.getDetailsJson() != null) { + failure.setDetails(ByteString.copyFromUtf8(failInfo.getDetailsJson())); + } + if (!failInfo.getMetadata().isEmpty()) { + failure.putAllMetadata(failInfo.getMetadata()); + } + return failure.build(); + } + + private void cancelOperation(OperationContext context, OperationCancelDetails details) { + try { + serviceHandler.cancelOperation(context, details); + } catch (Throwable e) { + Throwable failure = CheckedExceptionWrapper.unwrap(e); + log.warn( + "Nexus cancel operation failure. Service={}, Operation={}", + context.getService(), + context.getOperation(), + failure); + // Re-throw the original exception to handle it in the caller + throw e; + } + } + + private CancelOperationResponse handleCancelledOperation( + OperationContext.Builder ctx, CancelOperationRequest task) { + ctx.setService(task.getService()).setOperation(task.getOperation()); + + OperationCancelDetails operationCancelDetails = + OperationCancelDetails.newBuilder().setOperationId(task.getOperationId()).build(); + try { + cancelOperation(ctx.build(), operationCancelDetails); + } catch (Throwable failure) { + convertKnownFailures(failure); + } + + return CancelOperationResponse.newBuilder().build(); + } + + private void convertKnownFailures(Throwable e) { + Throwable failure = CheckedExceptionWrapper.unwrap(e); + if (failure instanceof ApplicationFailure) { + if (((ApplicationFailure) failure).isNonRetryable()) { + throw new OperationHandlerException( + OperationHandlerException.ErrorType.BAD_REQUEST, failure.getMessage()); + } + throw new OperationHandlerException( + OperationHandlerException.ErrorType.INTERNAL, failure.getMessage()); + } + if (failure instanceof WorkflowException) { + throw new OperationHandlerException( + OperationHandlerException.ErrorType.BAD_REQUEST, failure.getMessage()); + } + if (failure instanceof Error) { + throw (Error) failure; + } + throw new RuntimeException(failure); + } + + private OperationStartResult startOperation( + OperationContext context, OperationStartDetails details, HandlerInputContent input) + throws OperationUnsuccessfulException { + try { + return serviceHandler.startOperation(context, details, input); + } catch (Throwable e) { + Throwable ex = CheckedExceptionWrapper.unwrap(e); + log.warn( + "Nexus start operation failure. Service={}, Operation={}", + context.getService(), + context.getOperation(), + ex); + // Re-throw the original exception to handle it in the caller + throw e; + } + } + + private StartOperationResponse handleStartOperation( + OperationContext.Builder ctx, StartOperationRequest task) { + ctx.setService(task.getService()).setOperation(task.getOperation()); + + OperationStartDetails.Builder operationStartDetails = + OperationStartDetails.newBuilder() + .setCallbackUrl(task.getCallback()) + .setRequestId(task.getRequestId()); + task.getCallbackHeaderMap().forEach(operationStartDetails::putCallbackHeader); + task.getLinksList() + .forEach( + link -> { + try { + operationStartDetails.addLink(nexusProtoLinkToLink(link)); + } catch (URISyntaxException e) { + log.error("failed to parse link url: " + link.getUrl(), e); + throw new OperationHandlerException( + OperationHandlerException.ErrorType.BAD_REQUEST, + "Invalid link URL: " + link.getUrl(), + e); + } + }); + + HandlerInputContent.Builder input = + HandlerInputContent.newBuilder().setDataStream(task.getPayload().toByteString().newInput()); + + StartOperationResponse.Builder startResponseBuilder = StartOperationResponse.newBuilder(); + try { + OperationStartResult result = + startOperation(ctx.build(), operationStartDetails.build(), input.build()); + if (result.isSync()) { + startResponseBuilder.setSyncSuccess( + StartOperationResponse.Sync.newBuilder() + .setPayload(Payload.parseFrom(result.getSyncResult().getDataBytes())) + .build()); + } else { + startResponseBuilder.setAsyncSuccess( + StartOperationResponse.Async.newBuilder() + .setOperationId(result.getAsyncOperationId()) + .addAllLinks( + result.getLinks().stream() + .map( + link -> + io.temporal.api.nexus.v1.Link.newBuilder() + .setType(link.getType()) + .setUrl(link.getUri().toString()) + .build()) + .collect(Collectors.toList())) + .build()); + } + } catch (OperationUnsuccessfulException e) { + startResponseBuilder.setOperationError( + UnsuccessfulOperationError.newBuilder() + .setOperationState(e.getState().toString().toLowerCase()) + .setFailure(createFailure(e.getFailureInfo())) + .build()); + } catch (Throwable failure) { + convertKnownFailures(failure); + } + return startResponseBuilder.build(); + } + + public void registerNexusServiceImplementations(Object[] nexusServiceImplementation) { + for (Object nexusService : nexusServiceImplementation) { + registerNexusService(nexusService); + } + } + + private void registerNexusService(Object nexusService) { + if (nexusService instanceof Class) { + throw new IllegalArgumentException("Nexus service object instance expected, not the class"); + } + ServiceImplInstance instance = ServiceImplInstance.fromInstance(nexusService); + if (serviceImplInstances.put(instance.getDefinition().getName(), instance) != null) { + throw new TypeAlreadyRegisteredException( + instance.getDefinition().getName(), + "\"" + + instance.getDefinition().getName() + + "\" service type is already registered with the worker"); + } + } + + public CompletionStage shutdown(ShutdownManager shutdownManager, boolean unused) { + return shutdownManager.shutdownExecutorNow( + scheduler, "NexusTaskHandlerImpl#scheduler", Duration.ofSeconds(5)); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/nexus/PayloadSerializer.java b/temporal-sdk/src/main/java/io/temporal/internal/nexus/PayloadSerializer.java new file mode 100644 index 000000000..edb123bb7 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/nexus/PayloadSerializer.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.nexus; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.nexusrpc.Serializer; +import io.temporal.api.common.v1.Payload; +import io.temporal.common.converter.DataConverter; +import java.lang.reflect.Type; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * PayloadSerializer is a serializer that converts objects to and from {@link + * io.nexusrpc.Serializer.Content} objects by using the {@link DataConverter} to convert objects to + * and from {@link Payload} objects. + */ +class PayloadSerializer implements Serializer { + DataConverter dataConverter; + + PayloadSerializer(DataConverter dataConverter) { + this.dataConverter = dataConverter; + } + + @Override + public Content serialize(@Nullable Object o) { + Optional payload = dataConverter.toPayload(o); + Content.Builder content = Content.newBuilder(); + content.setData(payload.get().toByteArray()); + return content.build(); + } + + @Override + public @Nullable Object deserialize(Content content, Type type) { + try { + Payload payload = Payload.parseFrom(content.getData()); + return dataConverter.fromPayload(payload, type.getClass(), type); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContext.java b/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContext.java index 35b8b75f9..377b6b7c7 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContext.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContext.java @@ -22,6 +22,7 @@ import com.uber.m3.tally.Scope; import io.temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributes; +import io.temporal.api.command.v1.ScheduleNexusOperationCommandAttributes; import io.temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes; import io.temporal.api.common.v1.*; import io.temporal.api.failure.v1.Failure; @@ -176,6 +177,11 @@ Functions.Proc1 startChildWorkflow( Functions.Proc2 startCallback, Functions.Proc2, Exception> completionCallback); + Functions.Proc1 startNexusOperation( + ScheduleNexusOperationCommandAttributes attributes, + Functions.Proc2, Failure> startedCallback, + Functions.Proc2, Failure> completionCallback); + /** * Signal a workflow execution by WorkflowId and optionally RunId. * diff --git a/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContextImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContextImpl.java index 78330da95..4a3e81b23 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContextImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContextImpl.java @@ -21,11 +21,7 @@ package io.temporal.internal.replay; import com.uber.m3.tally.Scope; -import io.temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributes; -import io.temporal.api.command.v1.RequestCancelExternalWorkflowExecutionCommandAttributes; -import io.temporal.api.command.v1.ScheduleActivityTaskCommandAttributes; -import io.temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes; -import io.temporal.api.command.v1.StartTimerCommandAttributes; +import io.temporal.api.command.v1.*; import io.temporal.api.common.v1.*; import io.temporal.api.failure.v1.Failure; import io.temporal.api.history.v1.HistoryEvent; @@ -224,6 +220,16 @@ public Functions.Proc1 startChildWorkflow( return (exception) -> cancellationHandler.apply(); } + @Override + public Functions.Proc1 startNexusOperation( + ScheduleNexusOperationCommandAttributes attributes, + Functions.Proc2, Failure> startedCallback, + Functions.Proc2, Failure> completionCallback) { + Functions.Proc cancellationHandler = + workflowStateMachines.startNexusOperation(attributes, startedCallback, completionCallback); + return (exception) -> cancellationHandler.apply(); + } + @Override public Functions.Proc1 signalExternalWorkflowExecution( SignalExternalWorkflowExecutionCommandAttributes.Builder attributes, diff --git a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachine.java b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachine.java new file mode 100644 index 000000000..dc1c7118e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachine.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.statemachines; + +import io.temporal.api.command.v1.Command; +import io.temporal.api.command.v1.RequestCancelNexusOperationCommandAttributes; +import io.temporal.api.enums.v1.CommandType; +import io.temporal.api.enums.v1.EventType; +import io.temporal.workflow.Functions; + +/** CancelNexusOperationStateMachine manges a request to cancel a nexus operation. */ +final class CancelNexusOperationStateMachine + extends EntityStateMachineInitialCommand< + CancelNexusOperationStateMachine.State, + CancelNexusOperationStateMachine.ExplicitEvent, + CancelNexusOperationStateMachine> { + + private final RequestCancelNexusOperationCommandAttributes requestCancelNexusAttributes; + + /** + * @param attributes attributes to use to cancel a nexus operation + * @param commandSink sink to send commands + */ + public static void newInstance( + RequestCancelNexusOperationCommandAttributes attributes, + Functions.Proc1 commandSink, + Functions.Proc1 stateMachineSink) { + new CancelNexusOperationStateMachine(attributes, commandSink, stateMachineSink); + } + + private CancelNexusOperationStateMachine( + RequestCancelNexusOperationCommandAttributes attributes, + Functions.Proc1 commandSink, + Functions.Proc1 stateMachineSink) { + super(STATE_MACHINE_DEFINITION, commandSink, stateMachineSink); + this.requestCancelNexusAttributes = attributes; + explicitEvent(ExplicitEvent.SCHEDULE); + } + + enum ExplicitEvent { + SCHEDULE + } + + enum State { + CREATED, + REQUEST_CANCEL_NEXUS_OPERATION_COMMAND_CREATED, + CANCEL_REQUESTED, + } + + public static final StateMachineDefinition + STATE_MACHINE_DEFINITION = + StateMachineDefinition + .newInstance( + "CancelNexusOperation", State.CREATED, State.CANCEL_REQUESTED) + .add( + State.CREATED, + ExplicitEvent.SCHEDULE, + State.REQUEST_CANCEL_NEXUS_OPERATION_COMMAND_CREATED, + CancelNexusOperationStateMachine::createCancelNexusCommand) + .add( + State.REQUEST_CANCEL_NEXUS_OPERATION_COMMAND_CREATED, + CommandType.COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION, + State.REQUEST_CANCEL_NEXUS_OPERATION_COMMAND_CREATED) + .add( + State.REQUEST_CANCEL_NEXUS_OPERATION_COMMAND_CREATED, + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED, + State.CANCEL_REQUESTED, + CancelNexusOperationStateMachine::notifyCompleted); + + private void createCancelNexusCommand() { + addCommand( + Command.newBuilder() + .setCommandType(CommandType.COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION) + .setRequestCancelNexusOperationCommandAttributes(requestCancelNexusAttributes) + .build()); + } + + private void notifyCompleted() { + setInitialCommandEventId(); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/NexusOperationStateMachine.java b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/NexusOperationStateMachine.java new file mode 100644 index 000000000..6d2555c59 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/NexusOperationStateMachine.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.statemachines; + +import io.temporal.api.command.v1.Command; +import io.temporal.api.command.v1.ScheduleNexusOperationCommandAttributes; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.enums.v1.CommandType; +import io.temporal.api.enums.v1.EventType; +import io.temporal.api.failure.v1.CanceledFailureInfo; +import io.temporal.api.failure.v1.Failure; +import io.temporal.api.failure.v1.NexusOperationFailureInfo; +import io.temporal.api.history.v1.*; +import io.temporal.workflow.Functions; +import java.util.Optional; + +/** + * NexusOperationStateMachine manages a nexus operation. + * + *

    Note: A cancellation request is managed by {@link CancelNexusOperationStateMachine} + */ +final class NexusOperationStateMachine + extends EntityStateMachineInitialCommand< + NexusOperationStateMachine.State, + NexusOperationStateMachine.ExplicitEvent, + NexusOperationStateMachine> { + private static final String JAVA_SDK = "JavaSDK"; + private static final String NEXUS_OPERATION_CANCELED_MESSAGE = "Nexus operation canceled"; + + private ScheduleNexusOperationCommandAttributes scheduleAttributes; + private final Functions.Proc2, Failure> startedCallback; + private boolean async = false; + + private final Functions.Proc2, Failure> completionCallback; + private final String endpoint; + private final String service; + private final String operation; + + public boolean isCancellable() { + return State.SCHEDULE_COMMAND_CREATED == getState(); + } + + public void cancel() { + if (!isFinalState()) { + explicitEvent(ExplicitEvent.CANCEL); + } + } + + enum ExplicitEvent { + SCHEDULE, + CANCEL + } + + enum State { + CREATED, + SCHEDULE_COMMAND_CREATED, + SCHEDULED_EVENT_RECORDED, + STARTED, + COMPLETED, + FAILED, + TIMED_OUT, + CANCELED, + } + + public static final StateMachineDefinition< + NexusOperationStateMachine.State, + NexusOperationStateMachine.ExplicitEvent, + NexusOperationStateMachine> + STATE_MACHINE_DEFINITION = + StateMachineDefinition + . + newInstance( + "NexusOperation", + State.CREATED, + State.COMPLETED, + State.FAILED, + State.TIMED_OUT, + State.CANCELED) + .add( + State.CREATED, + ExplicitEvent.SCHEDULE, + State.SCHEDULE_COMMAND_CREATED, + NexusOperationStateMachine::createScheduleNexusTaskCommand) + .add( + State.SCHEDULE_COMMAND_CREATED, + CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, + State.SCHEDULE_COMMAND_CREATED) + .add( + State.SCHEDULE_COMMAND_CREATED, + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + State.SCHEDULED_EVENT_RECORDED, + NexusOperationStateMachine::setInitialCommandEventId) + .add( + State.SCHEDULE_COMMAND_CREATED, + ExplicitEvent.CANCEL, + State.CANCELED, + NexusOperationStateMachine::cancelNexusOperationCommand) + .add( + State.SCHEDULED_EVENT_RECORDED, + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + State.COMPLETED, + NexusOperationStateMachine::notifyCompleted) + .add( + State.SCHEDULED_EVENT_RECORDED, + EventType.EVENT_TYPE_NEXUS_OPERATION_FAILED, + State.FAILED, + NexusOperationStateMachine::notifyFailed) + .add( + State.SCHEDULED_EVENT_RECORDED, + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCELED, + State.CANCELED, + NexusOperationStateMachine::notifyCanceled) + .add( + State.SCHEDULED_EVENT_RECORDED, + EventType.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT, + State.TIMED_OUT, + NexusOperationStateMachine::notifyTimedOut) + .add( + State.SCHEDULED_EVENT_RECORDED, + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + State.STARTED, + NexusOperationStateMachine::notifyStarted) + .add( + State.STARTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + State.COMPLETED, + NexusOperationStateMachine::notifyCompleted) + .add( + State.STARTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_FAILED, + State.FAILED, + NexusOperationStateMachine::notifyFailed) + .add( + State.STARTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCELED, + State.CANCELED, + NexusOperationStateMachine::notifyCanceled) + .add( + State.STARTED, + EventType.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT, + State.TIMED_OUT, + NexusOperationStateMachine::notifyTimedOut); + + private void cancelNexusOperationCommand() { + cancelCommand(); + Failure canceledFailure = + Failure.newBuilder() + .setSource(JAVA_SDK) + .setMessage("operation canceled before it was started") + .setCanceledFailureInfo(CanceledFailureInfo.getDefaultInstance()) + .build(); + NexusOperationFailureInfo nexusFailureInfo = + NexusOperationFailureInfo.newBuilder() + .setEndpoint(endpoint) + .setService(service) + .setOperation(operation) + .setScheduledEventId(getInitialCommandEventId()) + .build(); + Failure failure = + Failure.newBuilder() + .setNexusOperationExecutionFailureInfo(nexusFailureInfo) + .setCause(canceledFailure) + .setMessage(NEXUS_OPERATION_CANCELED_MESSAGE) + .build(); + startedCallback.apply(Optional.empty(), failure); + completionCallback.apply(Optional.empty(), failure); + } + + private void notifyStarted() { + if (!async) { + if (currentEvent.getEventType() != EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED) { + startedCallback.apply(Optional.empty(), null); + } else { + async = true; + startedCallback.apply( + Optional.of(currentEvent.getNexusOperationStartedEventAttributes().getOperationId()), + null); + } + } + } + + private void notifyCompleted() { + notifyStarted(); + NexusOperationCompletedEventAttributes attributes = + currentEvent.getNexusOperationCompletedEventAttributes(); + completionCallback.apply(Optional.of(attributes.getResult()), null); + } + + private void notifyFailed() { + notifyStarted(); + NexusOperationFailedEventAttributes attributes = + currentEvent.getNexusOperationFailedEventAttributes(); + completionCallback.apply(Optional.empty(), attributes.getFailure()); + } + + private void notifyCanceled() { + notifyStarted(); + NexusOperationCanceledEventAttributes attributes = + currentEvent.getNexusOperationCanceledEventAttributes(); + completionCallback.apply(Optional.empty(), attributes.getFailure()); + } + + private void notifyTimedOut() { + notifyStarted(); + NexusOperationTimedOutEventAttributes attributes = + currentEvent.getNexusOperationTimedOutEventAttributes(); + completionCallback.apply(Optional.empty(), attributes.getFailure()); + } + + /** + * @param attributes attributes used to schedule the nexus operation + * @param startedCallback invoked when the Nexus operation start + * @param completionCallback invoked when Nexus operation completes + * @param commandSink sink to send commands + * @return an instance of NexusOperationStateMachine + */ + public static NexusOperationStateMachine newInstance( + ScheduleNexusOperationCommandAttributes attributes, + Functions.Proc2, Failure> startedCallback, + Functions.Proc2, Failure> completionCallback, + Functions.Proc1 commandSink, + Functions.Proc1 stateMachineSink) { + return new NexusOperationStateMachine( + attributes, startedCallback, completionCallback, commandSink, stateMachineSink); + } + + private NexusOperationStateMachine( + ScheduleNexusOperationCommandAttributes attributes, + Functions.Proc2, Failure> startedCallback, + Functions.Proc2, Failure> completionCallback, + Functions.Proc1 commandSink, + Functions.Proc1 stateMachineSink) { + super(STATE_MACHINE_DEFINITION, commandSink, stateMachineSink); + this.scheduleAttributes = attributes; + this.operation = attributes.getOperation(); + this.service = attributes.getService(); + this.endpoint = attributes.getEndpoint(); + this.startedCallback = startedCallback; + this.completionCallback = completionCallback; + explicitEvent(ExplicitEvent.SCHEDULE); + } + + public void createScheduleNexusTaskCommand() { + addCommand( + Command.newBuilder() + .setCommandType(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION) + .setScheduleNexusOperationCommandAttributes(scheduleAttributes) + .build()); + scheduleAttributes = null; // avoiding retaining large input for the duration of the operation + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java index a5460cc6d..b66e948c4 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java @@ -892,6 +892,27 @@ public Functions.Proc startChildWorkflow( }; } + public Functions.Proc startNexusOperation( + ScheduleNexusOperationCommandAttributes attributes, + Functions.Proc2, Failure> startedCallback, + Functions.Proc2, Failure> completionCallback) { + checkEventLoopExecuting(); + NexusOperationStateMachine operation = + NexusOperationStateMachine.newInstance( + attributes, startedCallback, completionCallback, commandSink, stateMachineSink); + return () -> { + if (operation.isCancellable()) { + operation.cancel(); + } + if (!operation.isFinalState()) { + requestCancelNexusOperation( + RequestCancelNexusOperationCommandAttributes.newBuilder() + .setScheduledEventId(operation.getInitialCommandEventId()) + .build()); + } + }; + } + private void notifyChildCanceled( Functions.Proc2, Exception> completionCallback) { CanceledFailure failure = new CanceledFailure("Child canceled"); @@ -923,6 +944,14 @@ public void requestCancelExternalWorkflowExecution( attributes, completionCallback, commandSink, stateMachineSink); } + /** + * @param attributes attributes to use to cancel a nexus operation + */ + public void requestCancelNexusOperation(RequestCancelNexusOperationCommandAttributes attributes) { + checkEventLoopExecuting(); + CancelNexusOperationStateMachine.newInstance(attributes, commandSink, stateMachineSink); + } + public void upsertSearchAttributes(SearchAttributes attributes) { checkEventLoopExecuting(); UpsertSearchAttributesStateMachine.newInstance(attributes, commandSink, stateMachineSink); @@ -1196,6 +1225,40 @@ private void validateCommand(Command command, HistoryEvent event) { eventAttributes.getTimerId()); } break; + case COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION: + { + ScheduleNexusOperationCommandAttributes commandAttributes = + command.getScheduleNexusOperationCommandAttributes(); + NexusOperationScheduledEventAttributes eventAttributes = + event.getNexusOperationScheduledEventAttributes(); + assertMatch( + command, + event, + "operation", + commandAttributes.getOperation(), + eventAttributes.getOperation()); + assertMatch( + command, + event, + "service", + commandAttributes.getService(), + eventAttributes.getService()); + } + break; + case COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION: + { + RequestCancelNexusOperationCommandAttributes commandAttributes = + command.getRequestCancelNexusOperationCommandAttributes(); + NexusOperationCancelRequestedEventAttributes eventAttributes = + event.getNexusOperationCancelRequestedEventAttributes(); + assertMatch( + command, + event, + "scheduleEventId", + commandAttributes.getScheduledEventId(), + eventAttributes.getScheduledEventId()); + } + break; case COMMAND_TYPE_CANCEL_TIMER: case COMMAND_TYPE_CANCEL_WORKFLOW_EXECUTION: case COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION: @@ -1343,7 +1406,21 @@ private OptionalLong getInitialCommandEventId(HistoryEvent event) { event.getWorkflowTaskTimedOutEventAttributes().getScheduledEventId()); case EVENT_TYPE_WORKFLOW_TASK_FAILED: return OptionalLong.of(event.getWorkflowTaskFailedEventAttributes().getScheduledEventId()); - + case EVENT_TYPE_NEXUS_OPERATION_STARTED: + return OptionalLong.of( + event.getNexusOperationStartedEventAttributes().getScheduledEventId()); + case EVENT_TYPE_NEXUS_OPERATION_COMPLETED: + return OptionalLong.of( + event.getNexusOperationCompletedEventAttributes().getScheduledEventId()); + case EVENT_TYPE_NEXUS_OPERATION_FAILED: + return OptionalLong.of( + event.getNexusOperationFailedEventAttributes().getScheduledEventId()); + case EVENT_TYPE_NEXUS_OPERATION_CANCELED: + return OptionalLong.of( + event.getNexusOperationCanceledEventAttributes().getScheduledEventId()); + case EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT: + return OptionalLong.of( + event.getNexusOperationTimedOutEventAttributes().getScheduledEventId()); case EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: case EVENT_TYPE_TIMER_STARTED: case EVENT_TYPE_MARKER_RECORDED: @@ -1363,6 +1440,8 @@ private OptionalLong getInitialCommandEventId(HistoryEvent event) { case EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED: case EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ADMITTED: case EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED: + case EVENT_TYPE_NEXUS_OPERATION_SCHEDULED: + case EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED: return OptionalLong.of(event.getEventId()); default: diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationExecutionImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationExecutionImpl.java new file mode 100644 index 000000000..cf373abfc --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationExecutionImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.sync; + +import io.temporal.workflow.NexusOperationExecution; +import java.util.Optional; + +public class NexusOperationExecutionImpl implements NexusOperationExecution { + + private final Optional operationId; + + public NexusOperationExecutionImpl(Optional operationId) { + this.operationId = operationId; + } + + @Override + public Optional getOperationId() { + return operationId; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationHandleImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationHandleImpl.java new file mode 100644 index 000000000..58e0fde5f --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusOperationHandleImpl.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.sync; + +import io.temporal.workflow.NexusOperationExecution; +import io.temporal.workflow.NexusOperationHandle; +import io.temporal.workflow.Promise; + +public class NexusOperationHandleImpl implements NexusOperationHandle { + Promise operationExecution; + Promise result; + + public NexusOperationHandleImpl( + Promise operationExecution, Promise result) { + this.operationExecution = operationExecution; + this.result = result; + } + + @Override + public Promise getExecution() { + return operationExecution; + } + + @Override + public Promise getResult() { + return result; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceInvocationHandler.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceInvocationHandler.java new file mode 100644 index 000000000..e0110fa80 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceInvocationHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.sync; + +import static io.temporal.internal.common.InternalUtils.getValueOrDefault; + +import com.google.common.base.Defaults; +import io.nexusrpc.Operation; +import io.nexusrpc.ServiceDefinition; +import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor; +import io.temporal.workflow.*; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +public class NexusServiceInvocationHandler implements InvocationHandler { + private final NexusServiceStub stub; + + private final ServiceDefinition serviceDef; + + NexusServiceInvocationHandler( + ServiceDefinition serviceDef, + NexusServiceOptions options, + WorkflowOutboundCallsInterceptor outboundCallsInterceptor, + Functions.Proc1 assertReadOnly) { + this.serviceDef = serviceDef; + this.stub = + new NexusServiceStubImpl( + serviceDef.getName(), options, outboundCallsInterceptor, assertReadOnly) {}; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals(StubMarker.GET_UNTYPED_STUB_METHOD)) { + return stub; + } + Object arg = args != null ? args[0] : null; + + Operation opAnnotation = method.getAnnotation(Operation.class); + if (opAnnotation == null) { + throw new IllegalArgumentException("Unknown method: " + method); + } + String opName = !opAnnotation.name().equals("") ? opAnnotation.name() : method.getName(); + // If the method was invoked as part of a start call then we need to return a handle back + // to the caller. The result of this method will be ignored. + if (StartNexusCallInternal.isAsync()) { + StartNexusCallInternal.setAsyncResult( + this.stub.start(opName, method.getReturnType(), method.getGenericReturnType(), arg)); + return Defaults.defaultValue(method.getReturnType()); + } + return getValueOrDefault( + this.stub.execute(opName, method.getReturnType(), method.getGenericReturnType(), arg), + method.getReturnType()); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceStubImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceStubImpl.java new file mode 100644 index 000000000..2ec5a86f4 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/NexusServiceStubImpl.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.sync; + +import com.google.common.base.Defaults; +import io.temporal.common.interceptors.WorkflowOutboundCallsInterceptor; +import io.temporal.failure.TemporalFailure; +import io.temporal.workflow.*; +import java.lang.reflect.Type; +import java.util.Collections; + +public class NexusServiceStubImpl implements NexusServiceStub { + final String name; + final NexusServiceOptions options; + final WorkflowOutboundCallsInterceptor outboundCallsInterceptor; + final Functions.Proc1 assertReadOnly; + + public NexusServiceStubImpl( + String name, + NexusServiceOptions options, + WorkflowOutboundCallsInterceptor outboundCallsInterceptor, + Functions.Proc1 assertReadOnly) { + this.name = name; + this.options = options; + this.outboundCallsInterceptor = outboundCallsInterceptor; + this.assertReadOnly = assertReadOnly; + } + + @Override + public R execute(String operationName, Class resultClass, Object arg) { + return execute(operationName, resultClass, resultClass, arg); + } + + @Override + public R execute(String operationName, Class resultClass, Type resultType, Object arg) { + assertReadOnly.apply("execute nexus operation"); + Promise result = executeAsync(operationName, resultClass, resultType, arg); + if (AsyncInternal.isAsync()) { + AsyncInternal.setAsyncResult(result); + return Defaults.defaultValue(resultClass); + } + try { + return result.get(); + } catch (TemporalFailure e) { + // Reset stack to the current one. Otherwise, it is very confusing to see a stack of + // an event handling method. + e.setStackTrace(Thread.currentThread().getStackTrace()); + throw e; + } + } + + @Override + public Promise executeAsync(String operationName, Class resultClass, Object arg) { + return executeAsync(operationName, resultClass, resultClass, arg); + } + + @Override + public Promise executeAsync( + String operationName, Class resultClass, Type resultType, Object arg) { + assertReadOnly.apply("execute nexus operation"); + NexusOperationOptions mergedOptions = + NexusOperationOptions.newBuilder(options.getOperationOptions()) + .mergeNexusOperationOptions(options.getOperationMethodOptions().get(operationName)) + .build(); + WorkflowOutboundCallsInterceptor.ExecuteNexusOperationOutput result = + outboundCallsInterceptor.executeNexusOperation( + new WorkflowOutboundCallsInterceptor.ExecuteNexusOperationInput<>( + options.getEndpoint(), + name, + operationName, + resultClass, + resultType, + arg, + mergedOptions, + Collections.emptyMap())); + return result.getResult(); + } + + @Override + public NexusOperationHandle start(String operationName, Class resultClass, Object arg) { + return start(operationName, resultClass, resultClass, arg); + } + + @Override + public NexusOperationHandle start( + String operationName, Class resultClass, Type resultType, Object arg) { + assertReadOnly.apply("schedule nexus operation"); + NexusOperationOptions mergedOptions = + NexusOperationOptions.newBuilder(options.getOperationOptions()) + .mergeNexusOperationOptions(options.getOperationMethodOptions().get(operationName)) + .build(); + WorkflowOutboundCallsInterceptor.ExecuteNexusOperationOutput result = + outboundCallsInterceptor.executeNexusOperation( + new WorkflowOutboundCallsInterceptor.ExecuteNexusOperationInput<>( + options.getEndpoint(), + name, + operationName, + resultClass, + resultType, + arg, + mergedOptions, + Collections.emptyMap())); + return new NexusOperationHandleImpl(result.getOperationExecution(), result.getResult()); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/StartNexusCallInternal.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/StartNexusCallInternal.java new file mode 100644 index 000000000..bc0111729 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/StartNexusCallInternal.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.sync; + +import io.temporal.workflow.Functions; +import io.temporal.workflow.NexusOperationHandle; +import java.util.concurrent.atomic.AtomicReference; + +public class StartNexusCallInternal { + + private static final ThreadLocal>> asyncResult = + new ThreadLocal<>(); + + public static boolean isAsync() { + return asyncResult.get() != null; + } + + public static void setAsyncResult(NexusOperationHandle handle) { + AtomicReference> placeholder = asyncResult.get(); + if (placeholder == null) { + throw new IllegalStateException("not in invoke invocation"); + } + placeholder.set(handle); + } + + /** + * Indicate to the dynamic interface implementation that call was done through + * + * @link Async#invoke}. Must be closed through {@link #closeAsyncInvocation()} + */ + public static void initAsyncInvocation() { + if (asyncResult.get() != null) { + throw new IllegalStateException("already in start invocation"); + } + asyncResult.set(new AtomicReference<>()); + } + + /** + * @return asynchronous result of an invocation. + */ + private static NexusOperationHandle getAsyncInvocationResult() { + AtomicReference> reference = asyncResult.get(); + if (reference == null) { + throw new IllegalStateException("initAsyncInvocation wasn't called"); + } + @SuppressWarnings("unchecked") + NexusOperationHandle result = (NexusOperationHandle) reference.get(); + if (result == null) { + throw new IllegalStateException("start result wasn't set"); + } + return result; + } + + /** Closes async invocation created through {@link #initAsyncInvocation()} */ + public static void closeAsyncInvocation() { + asyncResult.remove(); + } + + public static NexusOperationHandle startNexusOperation(Functions.Proc operation) { + initAsyncInvocation(); + try { + operation.apply(); + return getAsyncInvocationResult(); + } finally { + closeAsyncInvocation(); + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java index 7541ab5f4..efcfe1978 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java @@ -31,10 +31,7 @@ import com.uber.m3.tally.Scope; import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; -import io.temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributes; -import io.temporal.api.command.v1.ScheduleActivityTaskCommandAttributes; -import io.temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes; -import io.temporal.api.command.v1.StartChildWorkflowExecutionCommandAttributes; +import io.temporal.api.command.v1.*; import io.temporal.api.common.v1.ActivityType; import io.temporal.api.common.v1.Memo; import io.temporal.api.common.v1.Payload; @@ -119,6 +116,8 @@ final class SyncWorkflowContext implements WorkflowContext, WorkflowOutboundCall private Map activityOptionsMap; private LocalActivityOptions defaultLocalActivityOptions = null; private Map localActivityOptionsMap; + private NexusServiceOptions defaultNexusServiceOptions = null; + private Map nexusServiceOptionsMap; private boolean readOnly = false; private final WorkflowThreadLocal currentUpdateInfo = new WorkflowThreadLocal<>(); // Map of all running update handlers. Key is the update Id of the update request. @@ -152,6 +151,10 @@ public SyncWorkflowContext( workflowImplementationOptions.getDefaultLocalActivityOptions(); this.localActivityOptionsMap = new HashMap<>(workflowImplementationOptions.getLocalActivityOptions()); + this.defaultNexusServiceOptions = + workflowImplementationOptions.getDefaultNexusServiceOptions(); + this.nexusServiceOptionsMap = + new HashMap<>(workflowImplementationOptions.getNexusServiceOptions()); } this.workflowImplementationOptions = workflowImplementationOptions == null @@ -221,6 +224,16 @@ public LocalActivityOptions getDefaultLocalActivityOptions() { : Collections.emptyMap(); } + public NexusServiceOptions getDefaultNexusServiceOptions() { + return defaultNexusServiceOptions; + } + + public @Nonnull Map getNexusServiceOptions() { + return nexusServiceOptionsMap != null + ? Collections.unmodifiableMap(nexusServiceOptionsMap) + : Collections.emptyMap(); + } + public void setDefaultActivityOptions(ActivityOptions defaultActivityOptions) { this.defaultActivityOptions = (this.defaultActivityOptions == null) @@ -726,6 +739,87 @@ public ChildWorkflowOutput executeChildWorkflow(ChildWorkflowInput inp return new ChildWorkflowOutput<>(result, executionPromise); } + @Override + public ExecuteNexusOperationOutput executeNexusOperation( + ExecuteNexusOperationInput input) { + Preconditions.checkArgument( + input.getEndpoint() != null && !input.getEndpoint().isEmpty(), "endpoint must be set"); + Preconditions.checkArgument( + input.getService() != null && !input.getService().isEmpty(), "service must be set"); + + if (CancellationScope.current().isCancelRequested()) { + CanceledFailure canceledFailure = + new CanceledFailure("execute nexus operation called from a canceled scope"); + return new ExecuteNexusOperationOutput<>( + Workflow.newFailedPromise(canceledFailure), Workflow.newFailedPromise(canceledFailure)); + } + + CompletablePromise operationPromise = Workflow.newPromise(); + CompletablePromise> resultPromise = Workflow.newPromise(); + + // Not using the context aware data converter because the context will not be available on the + // worker side + Optional payload = dataConverter.toPayload(input.getArg()); + + ScheduleNexusOperationCommandAttributes.Builder attributes = + ScheduleNexusOperationCommandAttributes.newBuilder(); + payload.ifPresent(attributes::setInput); + attributes.setOperation(input.getOperation()); + attributes.setService(input.getService()); + attributes.setEndpoint(input.getEndpoint()); + attributes.putAllNexusHeader(input.getHeaders()); + attributes.setScheduleToCloseTimeout( + ProtobufTimeUtils.toProtoDuration(input.getOptions().getScheduleToCloseTimeout())); + + Functions.Proc1 cancellationCallback = + replayContext.startNexusOperation( + attributes.build(), + (operationExec, failure) -> { + if (failure != null) { + runner.executeInWorkflowThread( + "nexus operation start failed callback", + () -> + operationPromise.completeExceptionally( + dataConverter.failureToException(failure))); + } else { + runner.executeInWorkflowThread( + "nexus operation started callback", + () -> + operationPromise.complete(new NexusOperationExecutionImpl(operationExec))); + } + }, + (Optional result, Failure failure) -> { + if (failure != null) { + runner.executeInWorkflowThread( + "nexus operation failure callback", + () -> + resultPromise.completeExceptionally( + dataConverter.failureToException(failure))); + } else { + runner.executeInWorkflowThread( + "nexus operation completion callback", () -> resultPromise.complete(result)); + } + }); + AtomicBoolean callbackCalled = new AtomicBoolean(); + CancellationScope.current() + .getCancellationRequest() + .thenApply( + (reason) -> { + if (!callbackCalled.getAndSet(true)) { + cancellationCallback.apply(new CanceledFailure(reason)); + } + return null; + }); + Promise result = + resultPromise.thenApply( + (b) -> + input.getResultClass() != Void.class + ? dataConverter.fromPayload( + b.get(), input.getResultClass(), input.getResultType()) + : null); + return new ExecuteNexusOperationOutput<>(result, operationPromise); + } + @SuppressWarnings("deprecation") private StartChildWorkflowExecutionParameters createChildWorkflowParameters( String workflowId, diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java index 6c4d55d29..7a9d3b953 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java @@ -27,6 +27,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.uber.m3.tally.Scope; +import io.nexusrpc.ServiceDefinition; import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; import io.temporal.api.common.v1.Payload; @@ -784,7 +785,47 @@ public static boolean isEveryHandlerFinished() { return getRootWorkflowContext().isEveryHandlerFinished(); } - private static WorkflowOutboundCallsInterceptor getWorkflowOutboundInterceptor() { + public static T newNexusServiceStub(Class serviceInterface, NexusServiceOptions options) { + SyncWorkflowContext context = getRootWorkflowContext(); + NexusServiceOptions baseOptions = + (options == null) ? context.getDefaultNexusServiceOptions() : options; + + @Nonnull + Map predefinedNexusServiceOptions = + context.getNexusServiceOptions(); + + ServiceDefinition serviceDef = ServiceDefinition.fromClass(serviceInterface); + NexusServiceOptions mergedOptions = + NexusServiceOptions.newBuilder(predefinedNexusServiceOptions.get(serviceDef.getName())) + .mergeNexusServiceOptions(baseOptions) + .build(); + return (T) + Proxy.newProxyInstance( + serviceInterface.getClassLoader(), + new Class[] {serviceInterface, StubMarker.class, AsyncInternal.AsyncMarker.class}, + new NexusServiceInvocationHandler( + serviceDef, + mergedOptions, + getWorkflowOutboundInterceptor(), + WorkflowInternal::assertNotReadOnly)); + } + + public static NexusServiceStub newUntypedNexusServiceStub( + String service, NexusServiceOptions options) { + return new NexusServiceStubImpl( + service, options, getWorkflowOutboundInterceptor(), WorkflowInternal::assertNotReadOnly); + } + + public static NexusOperationHandle startNexusOperation( + Functions.Func1 operation, T arg) { + return StartNexusCallInternal.startNexusOperation(() -> operation.apply(arg)); + } + + public static NexusOperationHandle startNexusOperation(Functions.Func operation) { + return StartNexusCallInternal.startNexusOperation(() -> operation.apply()); + } + + static WorkflowOutboundCallsInterceptor getWorkflowOutboundInterceptor() { return getRootWorkflowContext().getWorkflowOutboundInterceptor(); } diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusPollTask.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusPollTask.java new file mode 100644 index 000000000..313296583 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusPollTask.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.worker; + +import static io.temporal.serviceclient.MetricsTag.METRICS_TAGS_CALL_OPTIONS_KEY; + +import com.google.protobuf.Timestamp; +import com.uber.m3.tally.Scope; +import io.temporal.api.common.v1.WorkerVersionCapabilities; +import io.temporal.api.taskqueue.v1.TaskQueue; +import io.temporal.api.workflowservice.v1.*; +import io.temporal.internal.common.ProtobufTimeUtils; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.MetricsType; +import io.temporal.worker.tuning.*; +import java.util.Objects; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class NexusPollTask implements Poller.PollTask { + private static final Logger log = LoggerFactory.getLogger(NexusPollTask.class); + + private final WorkflowServiceStubs service; + private final TrackingSlotSupplier slotSupplier; + private final Scope metricsScope; + private final PollNexusTaskQueueRequest pollRequest; + + public NexusPollTask( + @Nonnull WorkflowServiceStubs service, + @Nonnull String namespace, + @Nonnull String taskQueue, + @Nonnull String identity, + @Nullable String buildId, + boolean useBuildIdForVersioning, + @Nonnull TrackingSlotSupplier slotSupplier, + @Nonnull Scope metricsScope, + @Nonnull Supplier serverCapabilities) { + this.service = Objects.requireNonNull(service); + this.slotSupplier = slotSupplier; + this.metricsScope = Objects.requireNonNull(metricsScope); + + PollNexusTaskQueueRequest.Builder pollRequest = + PollNexusTaskQueueRequest.newBuilder() + .setNamespace(namespace) + .setIdentity(identity) + .setTaskQueue(TaskQueue.newBuilder().setName(taskQueue)); + + if (serverCapabilities.get().getBuildIdBasedVersioning()) { + pollRequest.setWorkerVersionCapabilities( + WorkerVersionCapabilities.newBuilder() + .setBuildId(buildId) + .setUseVersioning(useBuildIdForVersioning) + .build()); + } + this.pollRequest = pollRequest.build(); + } + + @Override + public NexusTask poll() { + if (log.isTraceEnabled()) { + log.trace("poll request begin: " + pollRequest); + } + PollNexusTaskQueueResponse response; + SlotPermit permit; + boolean isSuccessful = false; + + try { + permit = + slotSupplier.reserveSlot( + new SlotReservationData( + pollRequest.getTaskQueue().getName(), + pollRequest.getIdentity(), + pollRequest.getWorkerVersionCapabilities().getBuildId())); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } catch (Exception e) { + log.warn("Error while trying to reserve a slot for a nexus task", e.getCause()); + return null; + } + + try { + response = + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .pollNexusTaskQueue(pollRequest); + + if (response == null || response.getTaskToken().isEmpty()) { + metricsScope.counter(MetricsType.NEXUS_POLL_NO_TASK_COUNTER).inc(1); + return null; + } + + Timestamp startedTime = ProtobufTimeUtils.getCurrentProtoTime(); + metricsScope + .timer(MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY) + .record( + ProtobufTimeUtils.toM3Duration( + startedTime, response.getRequest().getScheduledTime())); + + isSuccessful = true; + return new NexusTask( + response, + permit, + () -> slotSupplier.releaseSlot(SlotReleaseReason.taskComplete(), permit)); + } finally { + if (!isSuccessful) slotSupplier.releaseSlot(SlotReleaseReason.neverUsed(), permit); + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTask.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTask.java new file mode 100644 index 000000000..933772740 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTask.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.worker; + +import io.temporal.api.workflowservice.v1.PollNexusTaskQueueResponseOrBuilder; +import io.temporal.worker.tuning.SlotPermit; +import io.temporal.workflow.Functions; +import javax.annotation.Nonnull; + +public final class NexusTask { + private final @Nonnull PollNexusTaskQueueResponseOrBuilder response; + private final @Nonnull SlotPermit permit; + private final @Nonnull Functions.Proc completionCallback; + + public NexusTask( + @Nonnull PollNexusTaskQueueResponseOrBuilder response, + @Nonnull SlotPermit permit, + @Nonnull Functions.Proc completionCallback) { + this.response = response; + this.permit = permit; + this.completionCallback = completionCallback; + } + + @Nonnull + public PollNexusTaskQueueResponseOrBuilder getResponse() { + return response; + } + + /** + * Completion handle function that must be called by the handler whenever the nexus task + * processing is completed. + */ + @Nonnull + public Functions.Proc getCompletionCallback() { + return completionCallback; + } + + @Nonnull + public SlotPermit getPermit() { + return permit; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTaskHandler.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTaskHandler.java new file mode 100644 index 000000000..07817f9ce --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusTaskHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.worker; + +import com.uber.m3.tally.Scope; +import io.temporal.api.nexus.v1.HandlerError; +import io.temporal.api.nexus.v1.Response; +import java.util.concurrent.TimeoutException; +import javax.annotation.Nullable; + +public interface NexusTaskHandler { + + /** + * Start the handler if the handler has any registered services. It is an error to start a handler + * more than once. + * + * @return True if this handler can handle at least one nexus service. + */ + boolean start(); + + NexusTaskHandler.Result handle(NexusTask task, Scope metricsScope) throws TimeoutException; + + class Result { + @Nullable private final Response response; + @Nullable private final HandlerError handlerError; + + public Result(Response response) { + this.response = response; + handlerError = null; + } + + public Result(HandlerError handlerError) { + this.handlerError = handlerError; + response = null; + } + + @Nullable + public Response getResponse() { + return response; + } + + @Nullable + public HandlerError getHandlerError() { + return handlerError; + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java new file mode 100644 index 000000000..13d0e7b0a --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.worker; + +import static io.temporal.serviceclient.MetricsTag.METRICS_TAGS_CALL_OPTIONS_KEY; + +import com.google.protobuf.ByteString; +import com.uber.m3.tally.Scope; +import com.uber.m3.tally.Stopwatch; +import com.uber.m3.util.Duration; +import com.uber.m3.util.ImmutableMap; +import io.temporal.api.nexus.v1.HandlerError; +import io.temporal.api.nexus.v1.Request; +import io.temporal.api.nexus.v1.Response; +import io.temporal.api.workflowservice.v1.*; +import io.temporal.internal.common.ProtobufTimeUtils; +import io.temporal.internal.logging.LoggerTag; +import io.temporal.internal.retryer.GrpcRetryer; +import io.temporal.serviceclient.MetricsTag; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.rpcretry.DefaultStubServiceOperationRpcRetryOptions; +import io.temporal.worker.MetricsType; +import io.temporal.worker.WorkerMetricsTag; +import io.temporal.worker.tuning.*; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +final class NexusWorker implements SuspendableWorker { + private static final Logger log = LoggerFactory.getLogger(NexusWorker.class); + + private SuspendableWorker poller = new NoopWorker(); + private PollTaskExecutor pollTaskExecutor; + + private final NexusTaskHandler handler; + private final WorkflowServiceStubs service; + private final String namespace; + private final String taskQueue; + private final SingleWorkerOptions options; + private final PollerOptions pollerOptions; + private final Scope workerMetricsScope; + private final GrpcRetryer grpcRetryer; + private final GrpcRetryer.GrpcRetryerOptions replyGrpcRetryerOptions; + private final TrackingSlotSupplier slotSupplier; + + public NexusWorker( + @Nonnull WorkflowServiceStubs service, + @Nonnull String namespace, + @Nonnull String taskQueue, + @Nonnull SingleWorkerOptions options, + @Nonnull NexusTaskHandler handler, + @Nonnull SlotSupplier slotSupplier) { + this.service = Objects.requireNonNull(service); + this.namespace = Objects.requireNonNull(namespace); + this.taskQueue = Objects.requireNonNull(taskQueue); + this.handler = Objects.requireNonNull(handler); + this.options = Objects.requireNonNull(options); + this.pollerOptions = getPollerOptions(options); + this.workerMetricsScope = + MetricsTag.tagged(options.getMetricsScope(), WorkerMetricsTag.WorkerType.NEXUS_WORKER); + this.grpcRetryer = new GrpcRetryer(service.getServerCapabilities()); + this.replyGrpcRetryerOptions = + new GrpcRetryer.GrpcRetryerOptions( + DefaultStubServiceOperationRpcRetryOptions.INSTANCE, null); + + this.slotSupplier = new TrackingSlotSupplier<>(slotSupplier, this.workerMetricsScope); + } + + @Override + public boolean start() { + if (handler.start()) { + this.pollTaskExecutor = + new PollTaskExecutor<>( + namespace, + taskQueue, + options.getIdentity(), + new TaskHandlerImpl(handler), + pollerOptions, + slotSupplier.maximumSlots().orElse(Integer.MAX_VALUE), + true); + poller = + new Poller<>( + options.getIdentity(), + new NexusPollTask( + service, + namespace, + taskQueue, + options.getIdentity(), + options.getBuildId(), + options.isUsingBuildIdForVersioning(), + this.slotSupplier, + workerMetricsScope, + service.getServerCapabilities()), + this.pollTaskExecutor, + pollerOptions, + workerMetricsScope); + poller.start(); + workerMetricsScope.counter(MetricsType.WORKER_START_COUNTER).inc(1); + return true; + } else { + return false; + } + } + + @Override + public CompletableFuture shutdown(ShutdownManager shutdownManager, boolean interruptTasks) { + String supplierName = this + "#executorSlots"; + return poller + .shutdown(shutdownManager, interruptTasks) + .thenCompose( + ignore -> + !interruptTasks + ? shutdownManager.waitForSupplierPermitsReleasedUnlimited( + slotSupplier, supplierName) + : CompletableFuture.completedFuture(null)) + .thenCompose( + ignore -> + pollTaskExecutor != null + ? pollTaskExecutor.shutdown(shutdownManager, interruptTasks) + : CompletableFuture.completedFuture(null)) + .exceptionally( + e -> { + log.error("Unexpected exception during shutdown", e); + return null; + }); + } + + @Override + public void awaitTermination(long timeout, TimeUnit unit) { + long timeoutMillis = ShutdownManager.awaitTermination(poller, unit.toMillis(timeout)); + ShutdownManager.awaitTermination(pollTaskExecutor, timeoutMillis); + } + + @Override + public void suspendPolling() { + poller.suspendPolling(); + } + + @Override + public void resumePolling() { + poller.resumePolling(); + } + + @Override + public boolean isShutdown() { + return poller.isShutdown(); + } + + @Override + public boolean isTerminated() { + return poller.isTerminated() && (pollTaskExecutor == null || pollTaskExecutor.isTerminated()); + } + + @Override + public boolean isSuspended() { + return poller.isSuspended(); + } + + @Override + public WorkerLifecycleState getLifecycleState() { + return poller.getLifecycleState(); + } + + private PollerOptions getPollerOptions(SingleWorkerOptions options) { + PollerOptions pollerOptions = options.getPollerOptions(); + if (pollerOptions.getPollThreadNamePrefix() == null) { + pollerOptions = + PollerOptions.newBuilder(pollerOptions) + .setPollThreadNamePrefix( + WorkerThreadsNameHelper.getNexusPollerThreadPrefix(namespace, taskQueue)) + .build(); + } + return pollerOptions; + } + + @Override + public String toString() { + return String.format( + "NexusWorker{identity=%s, namespace=%s, taskQueue=%s}", + options.getIdentity(), namespace, taskQueue); + } + + private class TaskHandlerImpl implements PollTaskExecutor.TaskHandler { + + final NexusTaskHandler handler; + + private TaskHandlerImpl(NexusTaskHandler handler) { + this.handler = handler; + } + + private String getNexusTaskService(PollNexusTaskQueueResponseOrBuilder pollResponse) { + Request request = pollResponse.getRequest(); + if (request.hasStartOperation()) { + return request.getStartOperation().getService(); + } else if (request.hasCancelOperation()) { + return request.getCancelOperation().getService(); + } + return ""; + } + + private String getNexusTaskOperation(PollNexusTaskQueueResponseOrBuilder pollResponse) { + Request request = pollResponse.getRequest(); + if (request.hasStartOperation()) { + return request.getStartOperation().getOperation(); + } else if (request.hasCancelOperation()) { + return request.getCancelOperation().getOperation(); + } + return ""; + } + + @Override + public void handle(NexusTask task) { + PollNexusTaskQueueResponseOrBuilder pollResponse = task.getResponse(); + // Extract service and operation from the request and set them as MDC and metrics + // scope tags. If the request does not have a service or operation, do not set the tags. + // If we don't know how to handle the task, we will fail the task further down the line. + Scope metricsScope = workerMetricsScope; + String service = getNexusTaskService(pollResponse); + if (!service.isEmpty()) { + MDC.put(LoggerTag.NEXUS_SERVICE, service); + metricsScope = metricsScope.tagged(ImmutableMap.of(MetricsTag.NEXUS_SERVICE, service)); + } + String operation = getNexusTaskOperation(pollResponse); + if (!operation.isEmpty()) { + MDC.put(LoggerTag.NEXUS_OPERATION, operation); + metricsScope = metricsScope.tagged(ImmutableMap.of(MetricsTag.NEXUS_OPERATION, operation)); + } + slotSupplier.markSlotUsed( + new NexusSlotInfo( + service, operation, taskQueue, options.getIdentity(), options.getBuildId()), + task.getPermit()); + + try { + handleNexusTask(task, metricsScope); + } finally { + task.getCompletionCallback().apply(); + MDC.remove(LoggerTag.NEXUS_SERVICE); + MDC.remove(LoggerTag.NEXUS_OPERATION); + } + } + + @Override + public Throwable wrapFailure(NexusTask task, Throwable failure) { + PollNexusTaskQueueResponseOrBuilder response = task.getResponse(); + return new RuntimeException( + "Failure processing nexus response: " + response.getRequest().toString(), failure); + } + + private void handleNexusTask(NexusTask task, Scope metricsScope) { + PollNexusTaskQueueResponseOrBuilder pollResponse = task.getResponse(); + ByteString taskToken = pollResponse.getTaskToken(); + + NexusTaskHandler.Result result; + + Stopwatch sw = metricsScope.timer(MetricsType.NEXUS_EXEC_LATENCY).start(); + try { + result = handler.handle(task, metricsScope); + if (result.getHandlerError() != null + || (result.getResponse().hasStartOperation() + && result.getResponse().getStartOperation().hasOperationError())) { + metricsScope.counter(MetricsType.NEXUS_EXEC_FAILED_COUNTER).inc(1); + } + } catch (TimeoutException e) { + log.warn("Nexus task timed out while processing", e); + metricsScope.counter(MetricsType.NEXUS_EXEC_FAILED_COUNTER).inc(1); + return; + } catch (Throwable e) { + // handler.handle if expected to never throw an exception and return result + // that can be used for a workflow callback if this method throws, it's a bug. + log.error("[BUG] Code that expected to never throw an exception threw an exception", e); + throw e; + } finally { + sw.stop(); + } + + try { + sendReply(taskToken, result, metricsScope); + } catch (Exception e) { + logExceptionDuringResultReporting(e, pollResponse, result); + throw e; + } + + Duration e2eDuration = + ProtobufTimeUtils.toM3DurationSinceNow(pollResponse.getRequest().getScheduledTime()); + metricsScope.timer(MetricsType.NEXUS_TASK_E2E_LATENCY).record(e2eDuration); + } + + private void logExceptionDuringResultReporting( + Exception e, + PollNexusTaskQueueResponseOrBuilder pollResponse, + NexusTaskHandler.Result result) { + if (log.isDebugEnabled()) { + log.debug( + "Failure during reporting of nexus task result to the server. TaskResult={}", + result, + e); + } else { + log.warn("Failure during reporting of nexus task result to the server.", e); + } + } + + private void sendReply( + ByteString taskToken, NexusTaskHandler.Result response, Scope metricsScope) { + Response taskResponse = response.getResponse(); + if (taskResponse != null) { + RespondNexusTaskCompletedRequest request = + RespondNexusTaskCompletedRequest.newBuilder() + .setTaskToken(taskToken) + .setIdentity(options.getIdentity()) + .setNamespace(namespace) + .setResponse(taskResponse) + .build(); + + grpcRetryer.retry( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .respondNexusTaskCompleted(request), + replyGrpcRetryerOptions); + } else { + HandlerError taskFailed = response.getHandlerError(); + if (taskFailed != null) { + RespondNexusTaskFailedRequest request = + RespondNexusTaskFailedRequest.newBuilder() + .setTaskToken(taskToken) + .setIdentity(options.getIdentity()) + .setNamespace(namespace) + .setError(taskFailed) + .build(); + + grpcRetryer.retry( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .respondNexusTaskFailed(request), + replyGrpcRetryerOptions); + } else { + throw new IllegalArgumentException("[BUG] Either response or failure must be set"); + } + } + } + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/PollTaskExecutor.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/PollTaskExecutor.java index 38cdfcd21..3db39f49a 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/PollTaskExecutor.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/PollTaskExecutor.java @@ -97,13 +97,6 @@ public void process(@Nonnull T task) { .getUncaughtExceptionHandler() .uncaughtException(Thread.currentThread(), handler.wrapFailure(task, e)); } - // TODO we should stop swallowing errors with the uncaught exception handler and - // let them go to the top. Errors are not recoverable. This should be done as a separate - // PR and carefully to make sure our own Temporal Errors thrown in the workflow code - // are not killing threads in the thread pool. - // if (e instanceof Error) { - // throw (Error)e; - // } } finally { MDC.remove(LoggerTag.NAMESPACE); MDC.remove(LoggerTag.TASK_QUEUE); diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/ShutdownManager.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/ShutdownManager.java index a98eae1d9..db60a25bd 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/ShutdownManager.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/ShutdownManager.java @@ -20,6 +20,8 @@ package io.temporal.internal.worker; +import static io.temporal.internal.common.GrpcUtils.isChannelShutdownException; + import com.google.common.util.concurrent.ListenableFuture; import io.grpc.Status; import io.grpc.StatusRuntimeException; @@ -134,13 +136,18 @@ public CompletableFuture waitOnWorkerShutdownRequest( () -> { try { shutdownRequest.get(); - } catch (StatusRuntimeException e) { - // If the server does not support shutdown, ignore the exception - if (Status.Code.UNIMPLEMENTED.equals(e.getStatus().getCode())) { - return; - } - log.warn("failed to call shutdown worker", e); } catch (Exception e) { + if (e instanceof ExecutionException) { + e = (Exception) e.getCause(); + } + if (e instanceof StatusRuntimeException) { + // If the server does not support shutdown, ignore the exception + if (Status.Code.UNIMPLEMENTED.equals( + ((StatusRuntimeException) e).getStatus().getCode()) + || isChannelShutdownException((StatusRuntimeException) e)) { + return; + } + } log.warn("failed to call shutdown worker", e); } finally { future.complete(null); diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/SyncNexusWorker.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/SyncNexusWorker.java new file mode 100644 index 000000000..c213dafc2 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/SyncNexusWorker.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.worker; + +import io.temporal.client.WorkflowClient; +import io.temporal.internal.nexus.NexusTaskHandlerImpl; +import io.temporal.worker.tuning.NexusSlotInfo; +import io.temporal.worker.tuning.SlotSupplier; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SyncNexusWorker implements SuspendableWorker { + private static final Logger log = LoggerFactory.getLogger(SyncNexusWorker.class); + + private final String identity; + private final String namespace; + private final String taskQueue; + private final NexusTaskHandlerImpl taskHandler; + private final NexusWorker worker; + + public SyncNexusWorker( + WorkflowClient client, + String namespace, + String taskQueue, + SingleWorkerOptions options, + SlotSupplier slotSupplier) { + this.identity = options.getIdentity(); + this.namespace = namespace; + this.taskQueue = taskQueue; + + this.taskHandler = + new NexusTaskHandlerImpl(client, namespace, taskQueue, options.getDataConverter()); + this.worker = + new NexusWorker( + client.getWorkflowServiceStubs(), + namespace, + taskQueue, + options, + taskHandler, + slotSupplier); + } + + @Override + public CompletableFuture shutdown(ShutdownManager shutdownManager, boolean interruptTasks) { + return worker + .shutdown(shutdownManager, interruptTasks) + .thenCompose(r -> taskHandler.shutdown(shutdownManager, interruptTasks)) + .exceptionally( + e -> { + log.error("[BUG] Unexpected exception during shutdown", e); + return null; + }); + } + + @Override + public void awaitTermination(long timeout, TimeUnit unit) { + long timeoutMillis = unit.toMillis(timeout); + ShutdownManager.awaitTermination(worker, timeoutMillis); + } + + @Override + public boolean start() { + return worker.start(); + } + + @Override + public void suspendPolling() { + worker.suspendPolling(); + } + + @Override + public void resumePolling() { + worker.resumePolling(); + } + + @Override + public boolean isSuspended() { + return worker.isSuspended(); + } + + @Override + public boolean isShutdown() { + return worker.isShutdown(); + } + + @Override + public boolean isTerminated() { + return worker.isTerminated(); + } + + @Override + public WorkerLifecycleState getLifecycleState() { + return worker.getLifecycleState(); + } + + @Override + public String toString() { + return String.format( + "SyncNexusWorker{namespace=%s, taskQueue=%s, identity=%s}", namespace, taskQueue, identity); + } + + public void registerNexusServiceImplementation(Object... nexusServiceImplementations) { + taskHandler.registerNexusServiceImplementations(nexusServiceImplementations); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/SyncWorkflowWorker.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/SyncWorkflowWorker.java index 5d83efbb8..feb1aa1ab 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/SyncWorkflowWorker.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/SyncWorkflowWorker.java @@ -72,6 +72,7 @@ public class SyncWorkflowWorker implements SuspendableWorker { private final POJOWorkflowImplementationFactory factory; private final DataConverter dataConverter; private final ActivityTaskHandlerImpl laTaskHandler; + private boolean runningLocalActivityWorker; public SyncWorkflowWorker( @Nonnull WorkflowServiceStubs service, @@ -176,7 +177,7 @@ public boolean start() { boolean started = workflowWorker.start(); // It doesn't start if no types are registered with it. if (started) { - laWorker.start(); + runningLocalActivityWorker = laWorker.start(); } return started; } @@ -235,7 +236,8 @@ public boolean isShutdown() { @Override public boolean isTerminated() { - return workflowWorker.isTerminated() && laWorker.isTerminated(); + return workflowWorker.isTerminated() + && (!runningLocalActivityWorker || laWorker.isTerminated()); } @Override diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkerThreadsNameHelper.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkerThreadsNameHelper.java index dbf453c34..59c933046 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkerThreadsNameHelper.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkerThreadsNameHelper.java @@ -25,6 +25,7 @@ class WorkerThreadsNameHelper { private static final String LOCAL_ACTIVITY_POLL_THREAD_NAME_PREFIX = "Local Activity Poller taskQueue="; private static final String ACTIVITY_POLL_THREAD_NAME_PREFIX = "Activity Poller taskQueue="; + private static final String NEXUS_POLL_THREAD_NAME_PREFIX = "Nexus Poller taskQueue="; public static final String SHUTDOWN_MANAGER_THREAD_NAME_PREFIX = "TemporalShutdownManager"; public static final String ACTIVITY_HEARTBEAT_THREAD_NAME_PREFIX = "TemporalActivityHeartbeat-"; @@ -65,4 +66,8 @@ public static String getActivityHeartbeatThreadPrefix(String namespace, String t public static String getLocalActivitySchedulerThreadPrefix(String namespace, String taskQueue) { return LOCAL_ACTIVITY_SCHEDULER_THREAD_NAME_PREFIX + namespace + "-" + taskQueue; } + + public static String getNexusPollerThreadPrefix(String namespace, String taskQueue) { + return NEXUS_POLL_THREAD_NAME_PREFIX + "\"" + taskQueue + "\", namespace=\"" + namespace + "\""; + } } diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkflowTask.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkflowTask.java index 9946b2e36..8d550e860 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkflowTask.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/WorkflowTask.java @@ -42,7 +42,7 @@ public PollWorkflowTaskQueueResponse getResponse() { } /** - * Completion handle function that must be called by the handler whenever activity processing is + * Completion handle function that must be called by the handler whenever workflow processing is * completed. */ @Nonnull diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/Nexus.java b/temporal-sdk/src/main/java/io/temporal/nexus/Nexus.java new file mode 100644 index 000000000..eb06ac057 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/Nexus.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.temporal.common.Experimental; +import io.temporal.internal.nexus.NexusInternal; +import io.temporal.internal.sync.WorkflowInternal; + +/** This class contains methods exposing Temporal APIs for Nexus Operations */ +@Experimental +public final class Nexus { + /** + * Can be used to get information about a Nexus Operation. This static method relies on a + * thread-local variable and works only in the original Nexus thread. + */ + public static NexusOperationContext getOperationContext() { + return NexusInternal.getOperationContext(); + } + + /** + * Use this to rethrow a checked exception from a Nexus Operation instead of adding the exception + * to a method signature. + * + * @return Never returns; always throws. Throws original exception if e is {@link + * RuntimeException} or {@link Error}. + */ + public static RuntimeException wrap(Throwable e) { + return WorkflowInternal.wrap(e); + } + + /** Prohibits instantiation. */ + private Nexus() {} +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/NexusOperationContext.java b/temporal-sdk/src/main/java/io/temporal/nexus/NexusOperationContext.java new file mode 100644 index 000000000..6642c3548 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/NexusOperationContext.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import com.uber.m3.tally.Scope; +import io.temporal.common.Experimental; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; + +/** + * Context object passed to a Nexus operation implementation. Use {@link + * Nexus#getOperationContext()} from a Nexus Operation implementation to access. + */ +@Experimental +public interface NexusOperationContext { + + /** + * Get scope for reporting business metrics in a nexus handler. This scope is tagged with the + * service and operation. + * + *

    The original metrics scope is set through {@link + * WorkflowServiceStubsOptions.Builder#setMetricsScope(Scope)} when a worker starts up. + */ + Scope getMetricsScope(); +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/SynchronousWorkflowClientOperationFunction.java b/temporal-sdk/src/main/java/io/temporal/nexus/SynchronousWorkflowClientOperationFunction.java new file mode 100644 index 000000000..d3f5c9610 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/SynchronousWorkflowClientOperationFunction.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.nexusrpc.OperationUnsuccessfulException; +import io.nexusrpc.handler.OperationContext; +import io.nexusrpc.handler.OperationStartDetails; +import io.temporal.client.WorkflowClient; +import io.temporal.common.Experimental; +import javax.annotation.Nullable; + +/** + * Function interface for {@link WorkflowClientOperationHandlers#sync} representing a call made for + * every operation call that takes a {@link WorkflowClient}. + */ +@FunctionalInterface +@Experimental +public interface SynchronousWorkflowClientOperationFunction { + @Nullable + R apply( + OperationContext ctx, OperationStartDetails details, WorkflowClient client, @Nullable T input) + throws OperationUnsuccessfulException; +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowClientOperationHandlers.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowClientOperationHandlers.java new file mode 100644 index 000000000..c41a9f0e7 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowClientOperationHandlers.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.nexusrpc.handler.*; +import io.nexusrpc.handler.OperationHandler; +import io.temporal.client.WorkflowClient; +import io.temporal.common.Experimental; +import io.temporal.internal.nexus.CurrentNexusOperationContext; +import io.temporal.internal.nexus.NexusOperationContextImpl; + +/** WorkflowClientOperationHandlers can be used to create Temporal specific OperationHandlers */ +@Experimental +public final class WorkflowClientOperationHandlers { + /** + * Helper to create {@link io.nexusrpc.handler.OperationHandler} instances that take a {@link + * io.temporal.client.WorkflowClient}. + */ + public static OperationHandler sync( + SynchronousWorkflowClientOperationFunction func) { + return io.nexusrpc.handler.OperationHandler.sync( + (OperationContext ctx, OperationStartDetails details, T input) -> { + NexusOperationContextImpl nexusCtx = CurrentNexusOperationContext.get(); + return func.apply(ctx, details, nexusCtx.getWorkflowClient(), input); + }); + } + + /** + * Maps a workflow method to an {@link io.nexusrpc.handler.OperationHandler}. + * + * @param startMethod returns the workflow method reference to call + * @return Operation handler to be used as an {@link OperationImpl} + */ + public static OperationHandler fromWorkflowMethod( + WorkflowMethodFactory startMethod) { + return new RunWorkflowOperation<>( + (OperationContext context, OperationStartDetails details, WorkflowClient client, T input) -> + WorkflowHandle.fromWorkflowMethod( + startMethod.apply(context, details, client, input), input)); + } + + /** + * Maps a workflow handle to an {@link io.nexusrpc.handler.OperationHandler}. + * + * @param handleFactory returns the workflow handle that will be mapped to the call + * @return Operation handler to be used as an {@link OperationImpl} + */ + public static OperationHandler fromWorkflowHandle( + WorkflowHandleFactory handleFactory) { + return new RunWorkflowOperation<>(handleFactory); + } + + /** Prohibits instantiation. */ + private WorkflowClientOperationHandlers() {} +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandle.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandle.java new file mode 100644 index 000000000..fd299f809 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandle.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.temporal.client.*; +import io.temporal.workflow.Functions; + +/** WorkflowHandle is a readonly representation of a workflow run backing a Nexus operation. */ +public final class WorkflowHandle { + /** + * Create a handle to a zero argument workflow with void return type + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod(Functions.Proc workflow) { + return new WorkflowHandle(new WorkflowMethodMethodInvoker(workflow)); + } + + /** + * Create a handle to a one argument workflow with void return type + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow argument + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Proc1 workflow, A1 arg1) { + return new WorkflowHandle(new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1))); + } + + /** + * Create a handle to a two argument workflow with void return type + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow argument + * @param arg2 second workflow argument + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Proc2 workflow, A1 arg1, A2 arg2) { + return new WorkflowHandle(new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2))); + } + + /** + * Create a handle to a three argument workflow with void return type + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow argument + * @param arg2 second workflow argument + * @param arg3 third workflow argument + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Proc3 workflow, A1 arg1, A2 arg2, A3 arg3) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3))); + } + + /** + * Create a handle to a four argument workflow with void return type + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow argument + * @param arg2 second workflow argument + * @param arg3 third workflow argument + * @param arg4 third workflow argument + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Proc4 workflow, A1 arg1, A2 arg2, A3 arg3, A4 arg4) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3, arg4))); + } + + /** + * Create a handle to a five argument workflow with void return type + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow argument + * @param arg2 second workflow argument + * @param arg3 third workflow argument + * @param arg4 fourth workflow argument + * @param arg5 fifth workflow argument + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Proc5 workflow, A1 arg1, A2 arg2, A3 arg3, A4 arg4, A5 arg5) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3, arg4, arg5))); + } + + /** + * Create a handle to a five argument workflow with void return type + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow argument + * @param arg2 second workflow argument + * @param arg3 third workflow argument + * @param arg4 fourth workflow argument + * @param arg5 fifth workflow argument + * @param arg6 fifth workflow argument + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Proc6 workflow, + A1 arg1, + A2 arg2, + A3 arg3, + A4 arg4, + A5 arg5, + A6 arg6) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3, arg4, arg5, arg6))); + } + + /** + * Create a handle to a zero argument workflow + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod(Functions.Func workflow) { + return new WorkflowHandle(new WorkflowMethodMethodInvoker(() -> workflow.apply())); + } + + /** + * Create a handle to a one argument workflow. + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow argument + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Func1 workflow, A1 arg1) { + return new WorkflowHandle(new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1))); + } + + /** + * Create a handle to a two argument workflow. + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow function parameter + * @param arg2 second workflow function parameter + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Func2 workflow, A1 arg1, A2 arg2) { + return new WorkflowHandle(new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2))); + } + + /** + * Create a handle to a three argument workflow. + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow function parameter + * @param arg2 second workflow function parameter + * @param arg3 third workflow function parameter + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Func3 workflow, A1 arg1, A2 arg2, A3 arg3) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3))); + } + + /** + * Create a handle to a four argument workflow. + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow function parameter + * @param arg2 second workflow function parameter + * @param arg3 third workflow function parameter + * @param arg4 fourth workflow function parameter + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Func4 workflow, A1 arg1, A2 arg2, A3 arg3, A4 arg4) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3, arg4))); + } + + /** + * Create a handle to a five argument workflow. + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow function parameter + * @param arg2 second workflow function parameter + * @param arg3 third workflow function parameter + * @param arg4 fourth workflow function parameter + * @param arg5 fifth workflow function parameter + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Func5 workflow, + A1 arg1, + A2 arg2, + A3 arg3, + A4 arg4, + A5 arg5) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3, arg4, arg5))); + } + + /** + * Create a handle to a six argument workflow. + * + * @param workflow The only supported value is method reference to a proxy created through {@link + * WorkflowClient#newWorkflowStub(Class, WorkflowOptions)}. + * @param arg1 first workflow function parameter + * @param arg2 second workflow function parameter + * @param arg3 third workflow function parameter + * @param arg4 fourth workflow function parameter + * @param arg5 fifth workflow function parameter + * @param arg6 sixth workflow function parameter + * @return WorkflowHandle + */ + public static WorkflowHandle fromWorkflowMethod( + Functions.Func6 workflow, + A1 arg1, + A2 arg2, + A3 arg3, + A4 arg4, + A5 arg5, + A6 arg6) { + return new WorkflowHandle( + new WorkflowMethodMethodInvoker(() -> workflow.apply(arg1, arg2, arg3, arg4, arg5, arg6))); + } + + /** + * Create a WorkflowHandle from an untyped workflow stub. + * + * @param stub The workflow stub to use + * @param resultClass class of the workflow return value + * @param args arguments to start the workflow + * @return WorkflowHandle + */ + static WorkflowHandle fromWorkflowStub( + WorkflowStub stub, Class resultClass, Object... args) { + return new WorkflowHandle(new WorkflowStubHandleInvoker(stub, args)); + } + + final WorkflowHandleInvoker invoker; + + WorkflowHandleInvoker getInvoker() { + return invoker; + } + + /** Prohibits outside instantiation. */ + private WorkflowHandle(WorkflowHandleInvoker invoker) { + this.invoker = invoker; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleFactory.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleFactory.java new file mode 100644 index 000000000..a5b0e14be --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.nexusrpc.handler.OperationContext; +import io.nexusrpc.handler.OperationStartDetails; +import io.temporal.client.WorkflowClient; +import javax.annotation.Nullable; + +/** + * Function interface for {@link + * WorkflowClientOperationHandlers#fromWorkflowHandle(WorkflowHandleFactory)} representing the + * workflow to associate with each operation call. + */ +@FunctionalInterface +public interface WorkflowHandleFactory { + /** + * Invoked every operation start call and expected to return a workflow handle to a workflow stub + * through the provided {@link WorkflowClient}. + */ + @Nullable + WorkflowHandle apply( + OperationContext context, OperationStartDetails details, WorkflowClient client, T input); +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleInvoker.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleInvoker.java new file mode 100644 index 000000000..7a74e0b9b --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowHandleInvoker.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.internal.client.NexusStartWorkflowRequest; + +interface WorkflowHandleInvoker { + WorkflowExecution invoke(NexusStartWorkflowRequest request); +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodFactory.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodFactory.java new file mode 100644 index 000000000..f59ff75db --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.nexusrpc.handler.OperationContext; +import io.nexusrpc.handler.OperationStartDetails; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.workflow.Functions; +import javax.annotation.Nullable; + +/** + * Function interface for {@link + * WorkflowClientOperationHandlers#fromWorkflowMethod(WorkflowMethodFactory)} representing the + * workflow method to invoke for every operation call. + */ +@FunctionalInterface +public interface WorkflowMethodFactory { + /** + * Invoked every operation start call and expected to return a workflow method reference to a + * proxy created through {@link WorkflowClient#newWorkflowStub(Class, WorkflowOptions)} using the + * provided {@link WorkflowClient}. + */ + @Nullable + Functions.Func1 apply( + OperationContext context, OperationStartDetails details, WorkflowClient client, T input); +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodMethodInvoker.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodMethodInvoker.java new file mode 100644 index 000000000..8512646eb --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowMethodMethodInvoker.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.internal.client.NexusStartWorkflowRequest; +import io.temporal.internal.client.WorkflowClientInternal; +import io.temporal.internal.nexus.CurrentNexusOperationContext; +import io.temporal.internal.nexus.NexusOperationContextImpl; +import io.temporal.workflow.Functions; + +class WorkflowMethodMethodInvoker implements WorkflowHandleInvoker { + private Functions.Proc workflow; + + public WorkflowMethodMethodInvoker(Functions.Proc workflow) { + this.workflow = workflow; + } + + @Override + public WorkflowExecution invoke(NexusStartWorkflowRequest request) { + NexusOperationContextImpl nexusCtx = CurrentNexusOperationContext.get(); + return ((WorkflowClientInternal) nexusCtx.getWorkflowClient().getInternal()) + .startNexus(request, workflow); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowRunOperation.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowRunOperation.java new file mode 100644 index 000000000..c6db1597d --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowRunOperation.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import static io.temporal.internal.common.LinkConverter.workflowEventToNexusLink; +import static io.temporal.internal.common.NexusUtil.nexusProtoLinkToLink; + +import io.nexusrpc.OperationInfo; +import io.nexusrpc.handler.*; +import io.nexusrpc.handler.OperationHandler; +import io.temporal.api.common.v1.Link; +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.api.enums.v1.EventType; +import io.temporal.client.WorkflowClient; +import io.temporal.internal.client.NexusStartWorkflowRequest; +import io.temporal.internal.nexus.CurrentNexusOperationContext; +import io.temporal.internal.nexus.NexusOperationContextImpl; +import java.net.URISyntaxException; + +class RunWorkflowOperation implements OperationHandler { + private final WorkflowHandleFactory handleFactory; + + RunWorkflowOperation(WorkflowHandleFactory handleFactory) { + this.handleFactory = handleFactory; + } + + @Override + public OperationStartResult start( + OperationContext ctx, OperationStartDetails operationStartDetails, T input) { + NexusOperationContextImpl nexusCtx = CurrentNexusOperationContext.get(); + + WorkflowHandle handle = + handleFactory.apply(ctx, operationStartDetails, nexusCtx.getWorkflowClient(), input); + + NexusStartWorkflowRequest nexusRequest = + new NexusStartWorkflowRequest( + operationStartDetails.getRequestId(), + operationStartDetails.getCallbackUrl(), + operationStartDetails.getCallbackHeaders(), + nexusCtx.getTaskQueue(), + operationStartDetails.getLinks()); + + WorkflowExecution workflowExec = handle.getInvoker().invoke(nexusRequest); + + // Create the link information about the new workflow and return to the caller. + Link.WorkflowEvent workflowEventLink = + Link.WorkflowEvent.newBuilder() + .setNamespace(nexusCtx.getNamespace()) + .setWorkflowId(workflowExec.getWorkflowId()) + .setRunId(workflowExec.getRunId()) + .setEventRef( + Link.WorkflowEvent.EventReference.newBuilder() + .setEventType(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED)) + .build(); + io.temporal.api.nexus.v1.Link nexusLink = workflowEventToNexusLink(workflowEventLink); + try { + OperationStartResult.Builder result = + OperationStartResult.newBuilder().setAsyncOperationId(workflowExec.getWorkflowId()); + if (nexusLink != null) { + result.addLink(nexusProtoLinkToLink(nexusLink)); + } + return result.build(); + } catch (URISyntaxException e) { + // Not expected as the link is constructed by the SDK. + throw new OperationHandlerException( + OperationHandlerException.ErrorType.INTERNAL, "failed to construct result URL", e); + } + } + + @Override + public R fetchResult( + OperationContext operationContext, OperationFetchResultDetails operationFetchResultDetails) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public OperationInfo fetchInfo( + OperationContext operationContext, OperationFetchInfoDetails operationFetchInfoDetails) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void cancel( + OperationContext operationContext, OperationCancelDetails operationCancelDetails) { + WorkflowClient client = CurrentNexusOperationContext.get().getWorkflowClient(); + client.newUntypedWorkflowStub(operationCancelDetails.getOperationId()).cancel(); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowStubHandleInvoker.java b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowStubHandleInvoker.java new file mode 100644 index 000000000..d2acf151a --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/nexus/WorkflowStubHandleInvoker.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus; + +import static io.temporal.internal.common.InternalUtils.createNexusBoundStub; + +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.client.WorkflowStub; +import io.temporal.internal.client.NexusStartWorkflowRequest; + +class WorkflowStubHandleInvoker implements WorkflowHandleInvoker { + final Object[] args; + final WorkflowStub stub; + + WorkflowStubHandleInvoker(WorkflowStub stub, Object[] args) { + this.args = args; + this.stub = stub; + } + + @Override + public WorkflowExecution invoke(NexusStartWorkflowRequest request) { + return createNexusBoundStub(stub, request).start(args); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/payload/context/SerializationContext.java b/temporal-sdk/src/main/java/io/temporal/payload/context/SerializationContext.java index 41d94e4ab..d10e72611 100644 --- a/temporal-sdk/src/main/java/io/temporal/payload/context/SerializationContext.java +++ b/temporal-sdk/src/main/java/io/temporal/payload/context/SerializationContext.java @@ -56,6 +56,9 @@ * PayloadConverter#withContext(SerializationContext)} and using the modified instance when * applicable. * + *

    Nexus operations inside a workflow do NOT have a {@link WorkflowSerializationContext} because + * it is not available in the operation handler. + * *

    Note: Serialization Context is experimental feature, the class and field structure of {@link * SerializationContext} objects may change in the future. There may be also situation where the * context is expected, but is not currently provided. diff --git a/temporal-sdk/src/main/java/io/temporal/worker/MetricsType.java b/temporal-sdk/src/main/java/io/temporal/worker/MetricsType.java index e8752ec4f..fad91fac5 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/MetricsType.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/MetricsType.java @@ -142,6 +142,29 @@ private MetricsType() {} public static final String WORKER_TASK_SLOTS_USED = TEMPORAL_METRICS_PREFIX + "worker_task_slots_used"; + // + // Nexus Worker + // + @Experimental + public static final String NEXUS_POLL_NO_TASK_COUNTER = + TEMPORAL_METRICS_PREFIX + "nexus_poll_no_task"; + + @Experimental + public static final String NEXUS_SCHEDULE_TO_START_LATENCY = + TEMPORAL_METRICS_PREFIX + "nexus_task_schedule_to_start_latency"; + + @Experimental + public static final String NEXUS_EXEC_LATENCY = + TEMPORAL_METRICS_PREFIX + "nexus_task_execution_latency"; + + @Experimental + public static final String NEXUS_EXEC_FAILED_COUNTER = + TEMPORAL_METRICS_PREFIX + "nexus_task_execution_failed"; + + @Experimental + public static final String NEXUS_TASK_E2E_LATENCY = + TEMPORAL_METRICS_PREFIX + "nexus_task_endtoend_latency"; + // // Worker Factory // diff --git a/temporal-sdk/src/main/java/io/temporal/worker/Worker.java b/temporal-sdk/src/main/java/io/temporal/worker/Worker.java index 619b9fe49..45253cd66 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/Worker.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/Worker.java @@ -27,6 +27,7 @@ import com.uber.m3.util.ImmutableMap; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; +import io.temporal.common.Experimental; import io.temporal.common.WorkflowExecutionHistory; import io.temporal.common.context.ContextPropagator; import io.temporal.common.converter.DataConverter; @@ -34,9 +35,6 @@ import io.temporal.internal.sync.WorkflowInternal; import io.temporal.internal.sync.WorkflowThreadExecutor; import io.temporal.internal.worker.*; -import io.temporal.internal.worker.SyncActivityWorker; -import io.temporal.internal.worker.SyncWorkflowWorker; -import io.temporal.internal.worker.WorkflowExecutorCache; import io.temporal.serviceclient.MetricsTag; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.worker.tuning.*; @@ -56,8 +54,8 @@ import org.slf4j.LoggerFactory; /** - * Hosts activity and workflow implementations. Uses long poll to receive activity and workflow - * tasks and processes them in a correspondent thread pool. + * Hosts activity, nexus and workflow implementations. Uses long poll to receive workflow, activity + * and nexus tasks and processes them in a correspondent thread pool. */ public final class Worker { private static final Logger log = LoggerFactory.getLogger(Worker.class); @@ -65,6 +63,7 @@ public final class Worker { private final String taskQueue; final SyncWorkflowWorker workflowWorker; final SyncActivityWorker activityWorker; + final SyncNexusWorker nexusWorker; private final AtomicBoolean started = new AtomicBoolean(); /** @@ -128,6 +127,18 @@ public final class Worker { ? activityWorker.getEagerActivityDispatcher() : new EagerActivityDispatcher.NoopEagerActivityDispatcher(); + SingleWorkerOptions nexusOptions = + toNexusOptions( + factoryOptions, this.options, clientOptions, contextPropagators, taggedScope); + SlotSupplier nexusSlotSupplier = + this.options.getWorkerTuner() == null + ? new FixedSizeSlotSupplier<>(this.options.getMaxConcurrentNexusExecutionSize()) + : this.options.getWorkerTuner().getNexusSlotSupplier(); + attachMetricsToResourceController(taggedScope, nexusSlotSupplier); + + nexusWorker = + new SyncNexusWorker(client, namespace, taskQueue, nexusOptions, nexusSlotSupplier); + SingleWorkerOptions singleWorkerOptions = toWorkflowWorkerOptions( factoryOptions, @@ -386,11 +397,27 @@ public void registerActivitiesImplementations(Object... activityImplementations) workflowWorker.registerLocalActivityImplementations(activityImplementations); } + /** + * Register Nexus service implementation objects with a worker. + * + *

    A Nexus service object must be annotated with {@link io.nexusrpc.handler.ServiceImpl}. + * + * @throws TypeAlreadyRegisteredException if one of the services is already registered + */ + @Experimental + public void registerNexusServiceImplementation(Object... nexusServiceImplementations) { + Preconditions.checkState( + !started.get(), + "registerNexusServiceImplementation is not allowed after worker has started"); + nexusWorker.registerNexusServiceImplementation(nexusServiceImplementations); + } + void start() { if (!started.compareAndSet(false, true)) { return; } workflowWorker.start(); + nexusWorker.start(); if (activityWorker != null) { activityWorker.start(); } @@ -399,19 +426,23 @@ void start() { CompletableFuture shutdown(ShutdownManager shutdownManager, boolean interruptUserTasks) { CompletableFuture workflowWorkerShutdownFuture = workflowWorker.shutdown(shutdownManager, interruptUserTasks); + CompletableFuture nexusWorkerShutdownFuture = + nexusWorker.shutdown(shutdownManager, interruptUserTasks); if (activityWorker != null) { return CompletableFuture.allOf( activityWorker.shutdown(shutdownManager, interruptUserTasks), - workflowWorkerShutdownFuture); + workflowWorkerShutdownFuture, + nexusWorkerShutdownFuture); } else { - return workflowWorkerShutdownFuture; + return CompletableFuture.allOf(workflowWorkerShutdownFuture, nexusWorkerShutdownFuture); } } boolean isTerminated() { boolean isTerminated = workflowWorker.isTerminated(); + isTerminated &= nexusWorker.isTerminated(); if (activityWorker != null) { - isTerminated = activityWorker.isTerminated(); + isTerminated &= activityWorker.isTerminated(); } return isTerminated; } @@ -421,6 +452,7 @@ void awaitTermination(long timeout, TimeUnit unit) { if (activityWorker != null) { timeoutMillis = ShutdownManager.awaitTermination(activityWorker, timeoutMillis); } + timeoutMillis = ShutdownManager.awaitTermination(nexusWorker, timeoutMillis); ShutdownManager.awaitTermination(workflowWorker, timeoutMillis); } @@ -476,6 +508,7 @@ public String getTaskQueue() { public void suspendPolling() { workflowWorker.suspendPolling(); + nexusWorker.suspendPolling(); if (activityWorker != null) { activityWorker.suspendPolling(); } @@ -483,13 +516,16 @@ public void suspendPolling() { public void resumePolling() { workflowWorker.resumePolling(); + nexusWorker.resumePolling(); if (activityWorker != null) { activityWorker.resumePolling(); } } public boolean isSuspended() { - return workflowWorker.isSuspended() && (activityWorker == null || activityWorker.isSuspended()); + return workflowWorker.isSuspended() + && nexusWorker.isSuspended() + && (activityWorker == null || activityWorker.isSuspended()); } @Nullable @@ -532,6 +568,21 @@ private static SingleWorkerOptions toActivityOptions( .build(); } + private static SingleWorkerOptions toNexusOptions( + WorkerFactoryOptions factoryOptions, + WorkerOptions options, + WorkflowClientOptions clientOptions, + List contextPropagators, + Scope metricsScope) { + return toSingleWorkerOptions(factoryOptions, options, clientOptions, contextPropagators) + .setPollerOptions( + PollerOptions.newBuilder() + .setPollThreadCount(options.getMaxConcurrentNexusTaskPollers()) + .build()) + .setMetricsScope(metricsScope) + .build(); + } + private static SingleWorkerOptions toWorkflowWorkerOptions( WorkerFactoryOptions factoryOptions, WorkerOptions options, diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerMetricsTag.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerMetricsTag.java index b571fe802..256447096 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerMetricsTag.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerMetricsTag.java @@ -26,7 +26,8 @@ public class WorkerMetricsTag { public enum WorkerType implements MetricsTag.TagValue { WORKFLOW_WORKER("WorkflowWorker"), ACTIVITY_WORKER("ActivityWorker"), - LOCAL_ACTIVITY_WORKER("LocalActivityWorker"); + LOCAL_ACTIVITY_WORKER("LocalActivityWorker"), + NEXUS_WORKER("NexusWorker"); WorkerType(String value) { this.value = value; diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkerOptions.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkerOptions.java index 12306d2c6..515712e4b 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkerOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkerOptions.java @@ -59,9 +59,11 @@ public static final class Builder { private static final int DEFAULT_MAX_CONCURRENT_WORKFLOW_TASK_POLLERS = 5; private static final int DEFAULT_MAX_CONCURRENT_ACTIVITY_TASK_POLLERS = 5; + private static final int DEFAULT_MAX_CONCURRENT_NEXUS_TASK_POLLERS = 5; private static final int DEFAULT_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTION_SIZE = 200; private static final int DEFAULT_MAX_CONCURRENT_ACTIVITY_EXECUTION_SIZE = 200; private static final int DEFAULT_MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE = 200; + private static final int DEFAULT_MAX_CONCURRENT_NEXUS_EXECUTION_SIZE = 200; private static final long DEFAULT_DEADLOCK_DETECTION_TIMEOUT = 1000; private static final Duration DEFAULT_MAX_HEARTBEAT_THROTTLE_INTERVAL = Duration.ofSeconds(60); private static final Duration DEFAULT_DEFAULT_HEARTBEAT_THROTTLE_INTERVAL = @@ -71,9 +73,11 @@ public static final class Builder { private int maxConcurrentActivityExecutionSize; private int maxConcurrentWorkflowTaskExecutionSize; private int maxConcurrentLocalActivityExecutionSize; + private int maxConcurrentNexusExecutionSize; private double maxTaskQueueActivitiesPerSecond; private int maxConcurrentWorkflowTaskPollers; private int maxConcurrentActivityTaskPollers; + private int maxConcurrentNexusTaskPollers; private boolean localActivityWorkerOnly; private long defaultDeadlockDetectionTimeout; private Duration maxHeartbeatThrottleInterval; @@ -96,9 +100,11 @@ private Builder(WorkerOptions o) { this.maxConcurrentActivityExecutionSize = o.maxConcurrentActivityExecutionSize; this.maxConcurrentWorkflowTaskExecutionSize = o.maxConcurrentWorkflowTaskExecutionSize; this.maxConcurrentLocalActivityExecutionSize = o.maxConcurrentLocalActivityExecutionSize; + this.maxConcurrentNexusExecutionSize = o.maxConcurrentNexusExecutionSize; this.workerTuner = o.workerTuner; this.maxTaskQueueActivitiesPerSecond = o.maxTaskQueueActivitiesPerSecond; this.maxConcurrentWorkflowTaskPollers = o.maxConcurrentWorkflowTaskPollers; + this.maxConcurrentNexusTaskPollers = o.maxConcurrentNexusTaskPollers; this.maxConcurrentActivityTaskPollers = o.maxConcurrentActivityTaskPollers; this.localActivityWorkerOnly = o.localActivityWorkerOnly; this.defaultDeadlockDetectionTimeout = o.defaultDeadlockDetectionTimeout; @@ -183,6 +189,22 @@ public Builder setMaxConcurrentLocalActivityExecutionSize( return this; } + /** + * @param maxConcurrentNexusExecutionSize Maximum number of nexus tasks executed in parallel. + * Default is 200, which is chosen if set to zero. + * @return {@code this} + *

    Note setting is mutually exclusive with {@link #setWorkerTuner(WorkerTuner)} + */ + @Experimental + public Builder setMaxConcurrentNexusExecutionSize(int maxConcurrentNexusExecutionSize) { + if (maxConcurrentNexusExecutionSize < 0) { + throw new IllegalArgumentException( + "Negative maxConcurrentNexusExecutionSize value: " + maxConcurrentNexusExecutionSize); + } + this.maxConcurrentNexusExecutionSize = maxConcurrentNexusExecutionSize; + return this; + } + /** * Optional: Sets the rate limiting on number of activities that can be executed per second. * This is managed by the server and controls activities per second for the entire task queue @@ -211,6 +233,19 @@ public Builder setMaxConcurrentWorkflowTaskPollers(int maxConcurrentWorkflowTask return this; } + /** + * Sets the maximum number of simultaneous long poll requests to the Temporal Server to retrieve + * nexus tasks. Changing this value will affect the rate at which the worker is able to consume + * tasks from a task queue. + * + *

    Default is 5, which is chosen if set to zero. + */ + @Experimental + public Builder setMaxConcurrentNexusTaskPollers(int maxConcurrentNexusTaskPollers) { + this.maxConcurrentNexusTaskPollers = maxConcurrentNexusTaskPollers; + return this; + } + /** * Number of simultaneous poll requests on workflow task queue. Note that the majority of the * workflow tasks will be using host local task queue due to caching. So try incrementing {@link @@ -400,10 +435,12 @@ public WorkerOptions build() { maxConcurrentActivityExecutionSize, maxConcurrentWorkflowTaskExecutionSize, maxConcurrentLocalActivityExecutionSize, + maxConcurrentNexusExecutionSize, workerTuner, maxTaskQueueActivitiesPerSecond, maxConcurrentWorkflowTaskPollers, maxConcurrentActivityTaskPollers, + maxConcurrentNexusTaskPollers, localActivityWorkerOnly, defaultDeadlockDetectionTimeout, maxHeartbeatThrottleInterval, @@ -462,6 +499,8 @@ public WorkerOptions validateAndBuildWithDefaults() { Preconditions.checkState( stickyTaskQueueDrainTimeout == null || !stickyTaskQueueDrainTimeout.isNegative(), "negative stickyTaskQueueDrainTimeout"); + Preconditions.checkState( + maxConcurrentNexusTaskPollers >= 0, "negative maxConcurrentNexusTaskPollers"); return new WorkerOptions( maxWorkerActivitiesPerSecond, @@ -474,6 +513,9 @@ public WorkerOptions validateAndBuildWithDefaults() { maxConcurrentLocalActivityExecutionSize == 0 ? DEFAULT_MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE : maxConcurrentLocalActivityExecutionSize, + maxConcurrentNexusExecutionSize == 0 + ? DEFAULT_MAX_CONCURRENT_NEXUS_EXECUTION_SIZE + : maxConcurrentNexusExecutionSize, workerTuner, maxTaskQueueActivitiesPerSecond, maxConcurrentWorkflowTaskPollers == 0 @@ -482,6 +524,9 @@ public WorkerOptions validateAndBuildWithDefaults() { maxConcurrentActivityTaskPollers == 0 ? DEFAULT_MAX_CONCURRENT_ACTIVITY_TASK_POLLERS : maxConcurrentActivityTaskPollers, + maxConcurrentNexusTaskPollers == 0 + ? DEFAULT_MAX_CONCURRENT_NEXUS_TASK_POLLERS + : maxConcurrentNexusTaskPollers, localActivityWorkerOnly, defaultDeadlockDetectionTimeout == 0 ? DEFAULT_DEADLOCK_DETECTION_TIMEOUT @@ -509,10 +554,12 @@ public WorkerOptions validateAndBuildWithDefaults() { private final int maxConcurrentActivityExecutionSize; private final int maxConcurrentWorkflowTaskExecutionSize; private final int maxConcurrentLocalActivityExecutionSize; + private final int maxConcurrentNexusExecutionSize; private final WorkerTuner workerTuner; private final double maxTaskQueueActivitiesPerSecond; private final int maxConcurrentWorkflowTaskPollers; private final int maxConcurrentActivityTaskPollers; + private final int maxConcurrentNexusTaskPollers; private final boolean localActivityWorkerOnly; private final long defaultDeadlockDetectionTimeout; private final Duration maxHeartbeatThrottleInterval; @@ -529,10 +576,12 @@ private WorkerOptions( int maxConcurrentActivityExecutionSize, int maxConcurrentWorkflowTaskExecutionSize, int maxConcurrentLocalActivityExecutionSize, + int maxConcurrentNexusExecutionSize, WorkerTuner workerTuner, double maxTaskQueueActivitiesPerSecond, int workflowPollThreadCount, int activityPollThreadCount, + int nexusPollThreadCount, boolean localActivityWorkerOnly, long defaultDeadlockDetectionTimeout, Duration maxHeartbeatThrottleInterval, @@ -547,10 +596,12 @@ private WorkerOptions( this.maxConcurrentActivityExecutionSize = maxConcurrentActivityExecutionSize; this.maxConcurrentWorkflowTaskExecutionSize = maxConcurrentWorkflowTaskExecutionSize; this.maxConcurrentLocalActivityExecutionSize = maxConcurrentLocalActivityExecutionSize; + this.maxConcurrentNexusExecutionSize = maxConcurrentNexusExecutionSize; this.workerTuner = workerTuner; this.maxTaskQueueActivitiesPerSecond = maxTaskQueueActivitiesPerSecond; this.maxConcurrentWorkflowTaskPollers = workflowPollThreadCount; this.maxConcurrentActivityTaskPollers = activityPollThreadCount; + this.maxConcurrentNexusTaskPollers = nexusPollThreadCount; this.localActivityWorkerOnly = localActivityWorkerOnly; this.defaultDeadlockDetectionTimeout = defaultDeadlockDetectionTimeout; this.maxHeartbeatThrottleInterval = maxHeartbeatThrottleInterval; @@ -579,6 +630,10 @@ public int getMaxConcurrentLocalActivityExecutionSize() { return maxConcurrentLocalActivityExecutionSize; } + public int getMaxConcurrentNexusExecutionSize() { + return maxConcurrentNexusExecutionSize; + } + public double getMaxTaskQueueActivitiesPerSecond() { return maxTaskQueueActivitiesPerSecond; } @@ -607,6 +662,10 @@ public int getMaxConcurrentActivityTaskPollers() { return maxConcurrentActivityTaskPollers; } + public int getMaxConcurrentNexusTaskPollers() { + return maxConcurrentNexusTaskPollers; + } + public long getDefaultDeadlockDetectionTimeout() { return defaultDeadlockDetectionTimeout; } @@ -662,9 +721,11 @@ public boolean equals(Object o) { && maxConcurrentActivityExecutionSize == that.maxConcurrentActivityExecutionSize && maxConcurrentWorkflowTaskExecutionSize == that.maxConcurrentWorkflowTaskExecutionSize && maxConcurrentLocalActivityExecutionSize == that.maxConcurrentLocalActivityExecutionSize + && maxConcurrentNexusExecutionSize == that.maxConcurrentNexusExecutionSize && compare(maxTaskQueueActivitiesPerSecond, that.maxTaskQueueActivitiesPerSecond) == 0 && maxConcurrentWorkflowTaskPollers == that.maxConcurrentWorkflowTaskPollers && maxConcurrentActivityTaskPollers == that.maxConcurrentActivityTaskPollers + && maxConcurrentNexusTaskPollers == that.maxConcurrentNexusTaskPollers && localActivityWorkerOnly == that.localActivityWorkerOnly && defaultDeadlockDetectionTimeout == that.defaultDeadlockDetectionTimeout && disableEagerExecution == that.disableEagerExecution @@ -685,10 +746,12 @@ public int hashCode() { maxConcurrentActivityExecutionSize, maxConcurrentWorkflowTaskExecutionSize, maxConcurrentLocalActivityExecutionSize, + maxConcurrentNexusExecutionSize, workerTuner, maxTaskQueueActivitiesPerSecond, maxConcurrentWorkflowTaskPollers, maxConcurrentActivityTaskPollers, + maxConcurrentNexusTaskPollers, localActivityWorkerOnly, defaultDeadlockDetectionTimeout, maxHeartbeatThrottleInterval, @@ -712,6 +775,8 @@ public String toString() { + maxConcurrentWorkflowTaskExecutionSize + ", maxConcurrentLocalActivityExecutionSize=" + maxConcurrentLocalActivityExecutionSize + + ", maxConcurrentNexusExecutionSize=" + + maxConcurrentNexusExecutionSize + ", workerTuner=" + workerTuner + ", maxTaskQueueActivitiesPerSecond=" @@ -720,6 +785,8 @@ public String toString() { + maxConcurrentWorkflowTaskPollers + ", maxConcurrentActivityTaskPollers=" + maxConcurrentActivityTaskPollers + + ", maxConcurrentNexusTaskPollers=" + + maxConcurrentNexusTaskPollers + ", localActivityWorkerOnly=" + localActivityWorkerOnly + ", defaultDeadlockDetectionTimeout=" diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java index 43d065501..19db55e85 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java @@ -22,6 +22,7 @@ import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; +import io.temporal.workflow.NexusServiceOptions; import java.util.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -42,6 +43,14 @@ public static Builder newBuilder() { return new Builder(); } + public static Builder newBuilder(WorkflowImplementationOptions options) { + return new Builder(options); + } + + public Builder toBuilder() { + return new Builder(this); + } + public static final class Builder { private Class[] failWorkflowExceptionTypes; @@ -49,9 +58,24 @@ public static final class Builder { private ActivityOptions defaultActivityOptions; private Map localActivityOptions; private LocalActivityOptions defaultLocalActivityOptions; + private Map nexusServiceOptions; + private NexusServiceOptions defaultNexusServiceOptions; private Builder() {} + private Builder(WorkflowImplementationOptions options) { + if (options == null) { + return; + } + this.failWorkflowExceptionTypes = options.getFailWorkflowExceptionTypes(); + this.activityOptions = options.getActivityOptions(); + this.defaultActivityOptions = options.getDefaultActivityOptions(); + this.localActivityOptions = options.getLocalActivityOptions(); + this.defaultLocalActivityOptions = options.getDefaultLocalActivityOptions(); + this.nexusServiceOptions = options.getNexusServiceOptions(); + this.defaultNexusServiceOptions = options.getDefaultNexusServiceOptions(); + } + /** * Optional: Sets how workflow worker deals with exceptions thrown from the workflow code which * include non-deterministic history events (presumably arising from non-deterministic workflow @@ -126,13 +150,39 @@ public Builder setDefaultLocalActivityOptions( return this; } + /** + * Set individual Nexus Service options per service. Will be merged with the map from {@link + * io.temporal.workflow.Workflow#newNexusServiceStub(Class, NexusServiceOptions)} which has the + * highest precedence. + * + * @param nexusServiceOptions map from service to NexusServiceOptions + */ + public Builder setNexusServiceOptions(Map nexusServiceOptions) { + this.nexusServiceOptions = new HashMap<>(Objects.requireNonNull(nexusServiceOptions)); + return this; + } + + /** + * These nexus service options to use if no specific options are passed for a service. Will be + * used for a stub created with {@link io.temporal.workflow.Workflow#newNexusServiceStub(Class)} + * + * @param defaultNexusServiceOptions default NexusServiceOptions for all services in the + * workflow. + */ + public Builder setDefaultNexusServiceOptions(NexusServiceOptions defaultNexusServiceOptions) { + this.defaultNexusServiceOptions = Objects.requireNonNull(defaultNexusServiceOptions); + return this; + } + public WorkflowImplementationOptions build() { return new WorkflowImplementationOptions( failWorkflowExceptionTypes == null ? new Class[0] : failWorkflowExceptionTypes, activityOptions == null ? null : activityOptions, defaultActivityOptions, localActivityOptions == null ? null : localActivityOptions, - defaultLocalActivityOptions); + defaultLocalActivityOptions, + nexusServiceOptions == null ? null : nexusServiceOptions, + defaultNexusServiceOptions); } } @@ -141,18 +191,24 @@ public WorkflowImplementationOptions build() { private final ActivityOptions defaultActivityOptions; private final @Nullable Map localActivityOptions; private final LocalActivityOptions defaultLocalActivityOptions; + private final @Nullable Map nexusServiceOptions; + private final NexusServiceOptions defaultNexusServiceOptions; public WorkflowImplementationOptions( Class[] failWorkflowExceptionTypes, @Nullable Map activityOptions, ActivityOptions defaultActivityOptions, @Nullable Map localActivityOptions, - LocalActivityOptions defaultLocalActivityOptions) { + LocalActivityOptions defaultLocalActivityOptions, + @Nullable Map nexusServiceOptions, + NexusServiceOptions defaultNexusServiceOptions) { this.failWorkflowExceptionTypes = failWorkflowExceptionTypes; this.activityOptions = activityOptions; this.defaultActivityOptions = defaultActivityOptions; this.localActivityOptions = localActivityOptions; this.defaultLocalActivityOptions = defaultLocalActivityOptions; + this.nexusServiceOptions = nexusServiceOptions; + this.defaultNexusServiceOptions = defaultNexusServiceOptions; } public Class[] getFailWorkflowExceptionTypes() { @@ -179,6 +235,16 @@ public LocalActivityOptions getDefaultLocalActivityOptions() { return defaultLocalActivityOptions; } + public @Nonnull Map getNexusServiceOptions() { + return nexusServiceOptions != null + ? Collections.unmodifiableMap(nexusServiceOptions) + : Collections.emptyMap(); + } + + public NexusServiceOptions getDefaultNexusServiceOptions() { + return defaultNexusServiceOptions; + } + @Override public String toString() { return "WorkflowImplementationOptions{" @@ -192,6 +258,10 @@ public String toString() { + localActivityOptions + ", defaultLocalActivityOptions=" + defaultLocalActivityOptions + + ", nexusServiceOptions=" + + nexusServiceOptions + + ", defaultNexusServiceOptions=" + + defaultNexusServiceOptions + '}'; } @@ -204,7 +274,9 @@ public boolean equals(Object o) { && Objects.equals(activityOptions, that.activityOptions) && Objects.equals(defaultActivityOptions, that.defaultActivityOptions) && Objects.equals(localActivityOptions, that.localActivityOptions) - && Objects.equals(defaultLocalActivityOptions, that.defaultLocalActivityOptions); + && Objects.equals(defaultLocalActivityOptions, that.defaultLocalActivityOptions) + && Objects.equals(nexusServiceOptions, that.nexusServiceOptions) + && Objects.equals(defaultNexusServiceOptions, that.defaultNexusServiceOptions); } @Override @@ -214,7 +286,9 @@ public int hashCode() { activityOptions, defaultActivityOptions, localActivityOptions, - defaultLocalActivityOptions); + defaultLocalActivityOptions, + nexusServiceOptions, + defaultNexusServiceOptions); result = 31 * result + Arrays.hashCode(failWorkflowExceptionTypes); return result; } diff --git a/temporal-sdk/src/main/java/io/temporal/worker/tuning/CompositeTuner.java b/temporal-sdk/src/main/java/io/temporal/worker/tuning/CompositeTuner.java index 679050493..f0cdca555 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/tuning/CompositeTuner.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/tuning/CompositeTuner.java @@ -33,19 +33,23 @@ public class CompositeTuner implements WorkerTuner { private final @Nonnull SlotSupplier workflowTaskSlotSupplier; private final @Nonnull SlotSupplier activityTaskSlotSupplier; private final @Nonnull SlotSupplier localActivitySlotSupplier; + private final @Nonnull SlotSupplier nexusSlotSupplier; public CompositeTuner( @Nonnull SlotSupplier workflowTaskSlotSupplier, @Nonnull SlotSupplier activityTaskSlotSupplier, - @Nonnull SlotSupplier localActivitySlotSupplier) { + @Nonnull SlotSupplier localActivitySlotSupplier, + @Nonnull SlotSupplier nexusSlotSupplier) { this.workflowTaskSlotSupplier = Objects.requireNonNull(workflowTaskSlotSupplier); this.activityTaskSlotSupplier = Objects.requireNonNull(activityTaskSlotSupplier); this.localActivitySlotSupplier = Objects.requireNonNull(localActivitySlotSupplier); + this.nexusSlotSupplier = Objects.requireNonNull(nexusSlotSupplier); // All resource-based slot suppliers must use the same controller validateResourceController(workflowTaskSlotSupplier, activityTaskSlotSupplier); validateResourceController(workflowTaskSlotSupplier, localActivitySlotSupplier); validateResourceController(activityTaskSlotSupplier, localActivitySlotSupplier); + validateResourceController(workflowTaskSlotSupplier, nexusSlotSupplier); } @Nonnull @@ -66,6 +70,12 @@ public SlotSupplier getLocalActivitySlotSupplier() { return localActivitySlotSupplier; } + @Nonnull + @Override + public SlotSupplier getNexusSlotSupplier() { + return nexusSlotSupplier; + } + private void validateResourceController( @Nonnull SlotSupplier supplier1, @Nonnull SlotSupplier supplier2) { if (supplier1 instanceof ResourceBasedSlotSupplier diff --git a/temporal-sdk/src/main/java/io/temporal/worker/tuning/NexusSlotInfo.java b/temporal-sdk/src/main/java/io/temporal/worker/tuning/NexusSlotInfo.java new file mode 100644 index 000000000..edb5ba57c --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/worker/tuning/NexusSlotInfo.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.worker.tuning; + +import io.temporal.common.Experimental; +import java.util.Objects; + +/** Contains information about a slot that is being used to execute a nexus task. */ +@Experimental +public class NexusSlotInfo extends SlotInfo { + private final String service; + private final String operation; + private final String taskQueue; + private final String workerIdentity; + private final String workerBuildId; + + public NexusSlotInfo( + String service, + String operation, + String taskQueue, + String workerIdentity, + String workerBuildId) { + this.service = service; + this.operation = operation; + this.taskQueue = taskQueue; + this.workerIdentity = workerIdentity; + this.workerBuildId = workerBuildId; + } + + public String getService() { + return service; + } + + public String getOperation() { + return operation; + } + + public String getTaskQueue() { + return taskQueue; + } + + public String getWorkerIdentity() { + return workerIdentity; + } + + public String getWorkerBuildId() { + return workerBuildId; + } + + @Override + public String toString() { + return "NexusSlotInfo{" + + "service='" + + service + + '\'' + + ", operation='" + + operation + + '\'' + + ", taskQueue='" + + taskQueue + + '\'' + + ", workerIdentity='" + + workerIdentity + + '\'' + + ", workerBuildId='" + + workerBuildId + + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NexusSlotInfo that = (NexusSlotInfo) o; + return Objects.equals(service, that.service) + && Objects.equals(operation, that.operation) + && Objects.equals(taskQueue, that.taskQueue) + && Objects.equals(workerIdentity, that.workerIdentity) + && Objects.equals(workerBuildId, that.workerBuildId); + } + + @Override + public int hashCode() { + return Objects.hash(service, operation, taskQueue, workerIdentity, workerBuildId); + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedSlotSupplier.java b/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedSlotSupplier.java index b01ed9dbb..db671b586 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedSlotSupplier.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedSlotSupplier.java @@ -69,6 +69,17 @@ public static ResourceBasedSlotSupplier createForLocalAct LocalActivitySlotInfo.class, resourceBasedController, options); } + /** + * Construct a slot supplier for nexus tasks with the given resource controller and options. + * + *

    The resource controller must be the same among all slot suppliers in a worker. If you want + * to use resource-based tuning for all slot suppliers, prefer {@link ResourceBasedTuner}. + */ + public static ResourceBasedSlotSupplier createForNexus( + ResourceBasedController resourceBasedController, ResourceBasedSlotOptions options) { + return new ResourceBasedSlotSupplier<>(NexusSlotInfo.class, resourceBasedController, options); + } + private ResourceBasedSlotSupplier( Class clazz, ResourceBasedController resourceBasedController, @@ -91,7 +102,8 @@ private ResourceBasedSlotSupplier( ? ResourceBasedTuner.DEFAULT_WORKFLOW_SLOT_OPTIONS.getRampThrottle() : options.getRampThrottle()) .build(); - } else { + } else if (ActivitySlotInfo.class.isAssignableFrom(clazz) + || LocalActivitySlotInfo.class.isAssignableFrom(clazz)) { this.options = ResourceBasedSlotOptions.newBuilder() .setMinimumSlots( @@ -107,6 +119,22 @@ private ResourceBasedSlotSupplier( ? ResourceBasedTuner.DEFAULT_ACTIVITY_SLOT_OPTIONS.getRampThrottle() : options.getRampThrottle()) .build(); + } else { + this.options = + ResourceBasedSlotOptions.newBuilder() + .setMinimumSlots( + options.getMinimumSlots() == 0 + ? ResourceBasedTuner.DEFAULT_NEXUS_SLOT_OPTIONS.getMinimumSlots() + : options.getMinimumSlots()) + .setMaximumSlots( + options.getMaximumSlots() == 0 + ? ResourceBasedTuner.DEFAULT_NEXUS_SLOT_OPTIONS.getMaximumSlots() + : options.getMaximumSlots()) + .setRampThrottle( + options.getRampThrottle() == null + ? ResourceBasedTuner.DEFAULT_NEXUS_SLOT_OPTIONS.getRampThrottle() + : options.getRampThrottle()) + .build(); } } diff --git a/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedTuner.java b/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedTuner.java index 47ad34e19..77573f93f 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedTuner.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/tuning/ResourceBasedTuner.java @@ -39,11 +39,18 @@ public class ResourceBasedTuner implements WorkerTuner { .setMaximumSlots(1000) .setRampThrottle(Duration.ofMillis(50)) .build(); + public static final ResourceBasedSlotOptions DEFAULT_NEXUS_SLOT_OPTIONS = + ResourceBasedSlotOptions.newBuilder() + .setMinimumSlots(1) + .setMaximumSlots(1000) + .setRampThrottle(Duration.ofMillis(50)) + .build(); private final ResourceBasedController controller; private final ResourceBasedSlotOptions workflowSlotOptions; private final ResourceBasedSlotOptions activitySlotOptions; private final ResourceBasedSlotOptions localActivitySlotOptions; + private final ResourceBasedSlotOptions nexusSlotOptions; public static Builder newBuilder() { return new Builder(); @@ -55,6 +62,7 @@ public static final class Builder { private @Nonnull ResourceBasedSlotOptions activitySlotOptions = DEFAULT_ACTIVITY_SLOT_OPTIONS; private @Nonnull ResourceBasedSlotOptions localActivitySlotOptions = DEFAULT_ACTIVITY_SLOT_OPTIONS; + private @Nonnull ResourceBasedSlotOptions nexusSlotOptions = DEFAULT_NEXUS_SLOT_OPTIONS; private Builder() {} @@ -97,9 +105,23 @@ public Builder setLocalActivitySlotOptions( return this; } + /** + * Set the slot options for nexus tasks. Has no effect after the worker using this tuner starts. + * + *

    Defaults to minimum 1 slot, maximum 1000 slots, and 50ms ramp throttle. + */ + public Builder setNexusSlotOptions(@Nonnull ResourceBasedSlotOptions nexusSlotOptions) { + this.nexusSlotOptions = nexusSlotOptions; + return this; + } + public ResourceBasedTuner build() { return new ResourceBasedTuner( - controllerOptions, workflowSlotOptions, activitySlotOptions, localActivitySlotOptions); + controllerOptions, + workflowSlotOptions, + activitySlotOptions, + localActivitySlotOptions, + nexusSlotOptions); } } @@ -110,11 +132,13 @@ public ResourceBasedTuner( ResourceBasedControllerOptions controllerOptions, ResourceBasedSlotOptions workflowSlotOptions, ResourceBasedSlotOptions activitySlotOptions, - ResourceBasedSlotOptions localActivitySlotOptions) { + ResourceBasedSlotOptions localActivitySlotOptions, + ResourceBasedSlotOptions nexusSlotOptions) { this.controller = ResourceBasedController.newSystemInfoController(controllerOptions); this.workflowSlotOptions = workflowSlotOptions; this.activitySlotOptions = activitySlotOptions; this.localActivitySlotOptions = localActivitySlotOptions; + this.nexusSlotOptions = nexusSlotOptions; } @Nonnull @@ -134,4 +158,10 @@ public SlotSupplier getActivityTaskSlotSupplier() { public SlotSupplier getLocalActivitySlotSupplier() { return ResourceBasedSlotSupplier.createForLocalActivity(controller, localActivitySlotOptions); } + + @Nonnull + @Override + public SlotSupplier getNexusSlotSupplier() { + return ResourceBasedSlotSupplier.createForNexus(controller, nexusSlotOptions); + } } diff --git a/temporal-sdk/src/main/java/io/temporal/worker/tuning/WorkerTuner.java b/temporal-sdk/src/main/java/io/temporal/worker/tuning/WorkerTuner.java index a25099569..291d8ea2e 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/tuning/WorkerTuner.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/tuning/WorkerTuner.java @@ -43,4 +43,10 @@ public interface WorkerTuner { */ @Nonnull SlotSupplier getLocalActivitySlotSupplier(); + + /** + * @return A {@link SlotSupplier} for nexus tasks. + */ + @Nonnull + SlotSupplier getNexusSlotSupplier(); } diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationExecution.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationExecution.java new file mode 100644 index 000000000..ae6ba353b --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationExecution.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow; + +import java.util.Optional; + +/** NexusOperationExecution identifies a specific Nexus operation execution. */ +public interface NexusOperationExecution { + /** + * @return the Operation ID as set by the Operation's handler. May be empty if the operation + * hasn't started yet or completed synchronously. + */ + Optional getOperationId(); +} diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationHandle.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationHandle.java new file mode 100644 index 000000000..daf519e5e --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationHandle.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow; + +import io.temporal.common.Experimental; + +/** + * OperationHandle is used to interact with a scheduled nexus operation. Created through {@link + * Workflow#startNexusOperation}. + */ +@Experimental +public interface NexusOperationHandle { + /** + * Returns a promise that is resolved when the operation reaches the STARTED state. For + * synchronous operations, this will be resolved at the same time as the promise from + * executeAsync. For asynchronous operations, this promises is resolved independently. If the + * operation is unsuccessful, this promise will throw the same exception as executeAsync. Use this + * method to extract the Operation ID of an asynchronous operation. OperationID will be empty for + * synchronous operations. If the workflow completes before this promise is ready then the + * operation might not start at all. + * + * @return promise that becomes ready once the operation has started. + */ + Promise getExecution(); + + /** Returns a promise that will be resolved when the operation completes. */ + Promise getResult(); +} diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java new file mode 100644 index 000000000..e5016f9db --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow; + +import io.temporal.common.Experimental; +import java.time.Duration; +import java.util.Objects; + +/** + * NexusOperationOptions is used to specify the options for starting a Nexus operation from a + * Workflow. + * + *

    Use {@link NexusOperationOptions#newBuilder()} to construct an instance. + */ +@Experimental +public final class NexusOperationOptions { + public static NexusOperationOptions.Builder newBuilder() { + return new NexusOperationOptions.Builder(); + } + + public static NexusOperationOptions.Builder newBuilder(NexusOperationOptions options) { + return new NexusOperationOptions.Builder(options); + } + + public static NexusOperationOptions getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final NexusOperationOptions DEFAULT_INSTANCE; + + static { + DEFAULT_INSTANCE = NexusOperationOptions.newBuilder().build(); + } + + public static final class Builder { + private Duration scheduleToCloseTimeout; + + /** + * Sets the schedule to close timeout for the Nexus operation. + * + * @param scheduleToCloseTimeout the schedule to close timeout for the Nexus operation + * @return this + */ + public NexusOperationOptions.Builder setScheduleToCloseTimeout( + Duration scheduleToCloseTimeout) { + this.scheduleToCloseTimeout = scheduleToCloseTimeout; + return this; + } + + private Builder() {} + + private Builder(NexusOperationOptions options) { + if (options == null) { + return; + } + this.scheduleToCloseTimeout = options.getScheduleToCloseTimeout(); + } + + public NexusOperationOptions build() { + return new NexusOperationOptions(scheduleToCloseTimeout); + } + + public NexusOperationOptions.Builder mergeNexusOperationOptions( + NexusOperationOptions override) { + if (override == null) { + return this; + } + this.scheduleToCloseTimeout = + (override.scheduleToCloseTimeout == null) + ? this.scheduleToCloseTimeout + : override.scheduleToCloseTimeout; + return this; + } + } + + private NexusOperationOptions(Duration scheduleToCloseTimeout) { + this.scheduleToCloseTimeout = scheduleToCloseTimeout; + } + + public NexusOperationOptions.Builder toBuilder() { + return new NexusOperationOptions.Builder(this); + } + + private Duration scheduleToCloseTimeout; + + public Duration getScheduleToCloseTimeout() { + return scheduleToCloseTimeout; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NexusOperationOptions that = (NexusOperationOptions) o; + return Objects.equals(scheduleToCloseTimeout, that.scheduleToCloseTimeout); + } + + @Override + public int hashCode() { + return Objects.hash(scheduleToCloseTimeout); + } + + @Override + public String toString() { + return "NexusOperationOptions{" + "scheduleToCloseTimeout=" + scheduleToCloseTimeout + '}'; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java new file mode 100644 index 000000000..8f03704cd --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow; + +import com.google.common.base.Preconditions; +import io.temporal.common.Experimental; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +/** + * Options for configuring a NexusService in a Workflow. + * + *

    Use {@link NexusServiceOptions#newBuilder()} to construct an instance. + */ +@Experimental +public final class NexusServiceOptions { + + public static NexusServiceOptions.Builder newBuilder() { + return new NexusServiceOptions.Builder(); + } + + public static NexusServiceOptions.Builder newBuilder(NexusServiceOptions options) { + return new NexusServiceOptions.Builder(options); + } + + public static NexusServiceOptions getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final NexusServiceOptions DEFAULT_INSTANCE; + + static { + DEFAULT_INSTANCE = NexusServiceOptions.newBuilder().build(); + } + + public static final class Builder { + private String endpoint; + private NexusOperationOptions operationOptions; + private Map operationMethodOptions; + + /** + * Sets the operation options for the NexusService. These options are used as the default for + * all operations. + */ + public NexusServiceOptions.Builder setOperationOptions(NexusOperationOptions operationOptions) { + this.operationOptions = operationOptions; + return this; + } + + /** + * Sets the endpoint for the NexusService. + * + * @param endpoint the endpoint for the NexusService, cannot be empty. + */ + public NexusServiceOptions.Builder setEndpoint(String endpoint) { + // We allow a null endpoint here because it possible a valid endpoint will be merged in later + Preconditions.checkArgument( + endpoint == null || !endpoint.isEmpty(), "endpoint cannot be empty if set"); + this.endpoint = endpoint; + return this; + } + + /** + * Sets operation specific options by the operation name. Merged with the base operation + * options. + * + * @param operationMethodOptions the operation specific options by the operation name + */ + public NexusServiceOptions.Builder setOperationMethodOptions( + Map operationMethodOptions) { + this.operationMethodOptions = operationMethodOptions; + return this; + } + + private Builder() {} + + private Builder(NexusServiceOptions options) { + if (options == null) { + return; + } + this.endpoint = options.getEndpoint(); + this.operationOptions = options.getOperationOptions(); + this.operationMethodOptions = options.getOperationMethodOptions(); + } + + public NexusServiceOptions build() { + return new NexusServiceOptions(endpoint, operationOptions, operationMethodOptions); + } + + public NexusServiceOptions.Builder mergeNexusServiceOptions(NexusServiceOptions override) { + if (override == null) { + return this; + } + this.endpoint = (override.endpoint == null) ? this.endpoint : override.endpoint; + this.operationOptions = + (override.operationOptions == null) ? this.operationOptions : override.operationOptions; + Map mergeTo = this.operationMethodOptions; + if (override.getOperationMethodOptions() != null) { + override + .getOperationMethodOptions() + .forEach( + (key, value) -> + mergeTo.merge( + key, + value, + (o1, o2) -> + NexusOperationOptions.newBuilder(o1) + .mergeNexusOperationOptions(o2) + .build())); + } + return this; + } + } + + private final NexusOperationOptions operationOptions; + + private final Map operationMethodOptions; + private final String endpoint; + + private NexusServiceOptions( + String endpoint, + NexusOperationOptions operationOptions, + Map operationMethodOptions) { + this.endpoint = endpoint; + this.operationOptions = operationOptions; + this.operationMethodOptions = + (operationMethodOptions == null) + ? Collections.emptyMap() + : Collections.unmodifiableMap(operationMethodOptions); + } + + public NexusServiceOptions.Builder toBuilder() { + return new NexusServiceOptions.Builder(this); + } + + public NexusOperationOptions getOperationOptions() { + return operationOptions; + } + + public String getEndpoint() { + return endpoint; + } + + public Map getOperationMethodOptions() { + return operationMethodOptions; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NexusServiceOptions that = (NexusServiceOptions) o; + return Objects.equals(operationOptions, that.operationOptions) + && Objects.equals(operationMethodOptions, that.operationMethodOptions) + && Objects.equals(endpoint, that.endpoint); + } + + @Override + public int hashCode() { + return Objects.hash(operationOptions, operationMethodOptions, endpoint); + } + + @Override + public String toString() { + return "NexusServiceOptions{" + + "operationOptions=" + + operationOptions + + ", operationMethodOptions=" + + operationMethodOptions + + ", endpoint='" + + endpoint + + '\'' + + '}'; + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceStub.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceStub.java new file mode 100644 index 000000000..f8f6f7084 --- /dev/null +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceStub.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow; + +import io.temporal.common.Experimental; +import java.lang.reflect.Type; + +/** + * NexusServiceStub is used to start operations on a Nexus service without referencing an interface + * it implements. This is useful to call operations when their type is not known at compile time or + * to execute operations implemented in other languages. Created through {@link + * Workflow#newNexusServiceStub(Class)}. + */ +@Experimental +public interface NexusServiceStub { + + /** + * Executes an operation by its type name and arguments. Blocks until the operation completion. + * + * @param operationName name of the operation type to execute. + * @param resultClass the expected return type of the operation. + * @param arg argument of the operation. + * @param return type. + * @return an operation result. + */ + R execute(String operationName, Class resultClass, Object arg); + + /** + * Executes an operation by its type name and arguments. Blocks until the operation completion. + * + * @param operationName name of the operation type to execute. + * @param resultClass the expected return type of the operation. + * @param resultType the expected return type of the nexus operation. Differs from resultClass for + * generic types. + * @param arg argument of the operation. + * @param return type. + * @return an operation result. + */ + R execute(String operationName, Class resultClass, Type resultType, Object arg); + + /** + * Executes an operation asynchronously by its type name and arguments. + * + * @param operationName name of an operation type to execute. + * @param resultClass the expected return type of the operation. Use Void.class for operations + * that return void type. + * @param arg argument of the operation. + * @param return type. + * @return Promise to the operation result. + */ + Promise executeAsync(String operationName, Class resultClass, Object arg); + + /** + * Executes an operation asynchronously by its type name and arguments. + * + * @param operationName name of an operation type to execute. + * @param resultClass the expected return type of the operation. Use Void.class for operations + * that return void type. + * @param resultType the expected return type of the nexus operation. Differs from resultClass for + * generic types. + * @param arg argument of the operation. + * @param return type. + * @return Promise to the operation result. + */ + Promise executeAsync( + String operationName, Class resultClass, Type resultType, Object arg); + + /** + * Request to start an operation by its type name and arguments + * + * @param operationName name of an operation type to execute. + * @param resultClass the expected return type of the operation. Use Void.class for operations + * that return void type. + * @param arg argument of the operation. + * @param return type. + * @return A handle that can be used to wait for the operation to start or wait for it to finish + */ + NexusOperationHandle start(String operationName, Class resultClass, Object arg); + + /** + * Request to start an operation by its type name and arguments + * + * @param operationName name of an operation type to execute. + * @param resultClass the expected return type of the operation. Use Void.class for operations + * that return void type. + * @param resultType the expected return type of the nexus operation. Differs from resultClass for + * generic types. + * @param arg argument of the operation. + * @param return type. + * @return A handle that can be used to wait for the operation to start or wait for it to finish + */ + NexusOperationHandle start( + String operationName, Class resultClass, Type resultType, Object arg); +} diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/Workflow.java b/temporal-sdk/src/main/java/io/temporal/workflow/Workflow.java index fa30673b5..94528e913 100644 --- a/temporal-sdk/src/main/java/io/temporal/workflow/Workflow.java +++ b/temporal-sdk/src/main/java/io/temporal/workflow/Workflow.java @@ -24,6 +24,7 @@ import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.common.Experimental; import io.temporal.common.RetryOptions; import io.temporal.common.SearchAttributeUpdate; import io.temporal.common.SearchAttributes; @@ -1302,6 +1303,67 @@ public static boolean isEveryHandlerFinished() { return WorkflowInternal.isEveryHandlerFinished(); } + /** + * Creates a Nexus service stub that can be used to start Nexus operations on the given service + * interface. + * + * @param service interface that given service implements. + */ + @Experimental + public static T newNexusServiceStub(Class service) { + return WorkflowInternal.newNexusServiceStub(service, null); + } + + /** + * Creates a Nexus service stub that can be used to start Nexus operations on the given service + * interface. + * + * @param service interface that given service implements. + * @param options options passed to the Nexus service. + */ + @Experimental + public static T newNexusServiceStub(Class service, NexusServiceOptions options) { + return WorkflowInternal.newNexusServiceStub(service, options); + } + + /** + * Creates untyped nexus service stub that can be used to execute Nexus operations. + * + * @param service name of the service the operation is part of. + * @param options options passed to the Nexus service. + */ + @Experimental + public static NexusServiceStub newUntypedNexusServiceStub( + String service, NexusServiceOptions options) { + return WorkflowInternal.newUntypedNexusServiceStub(service, options); + } + + /** + * Start a nexus operation. + * + * @param operation The only supported value is method reference to a proxy created through {@link + * #newNexusServiceStub(Class)}. + * @param arg operation argument + * @return OperationHandle a handle to the operation. + */ + @Experimental + public static NexusOperationHandle startNexusOperation( + Functions.Func1 operation, T arg) { + return WorkflowInternal.startNexusOperation(operation, arg); + } + + /** + * Start a Nexus operation. + * + * @param operation The only supported value is method reference to a proxy created through {@link + * #newNexusServiceStub(Class)}. + * @return OperationHandle a handle to the operation. + */ + @Experimental + public static NexusOperationHandle startNexusOperation(Functions.Func operation) { + return WorkflowInternal.startNexusOperation(operation); + } + /** Prohibit instantiation. */ private Workflow() {} } diff --git a/temporal-sdk/src/test/java/io/temporal/client/functional/StartTest.java b/temporal-sdk/src/test/java/io/temporal/client/functional/StartTest.java index 094f9799f..b7d0cb0fa 100644 --- a/temporal-sdk/src/test/java/io/temporal/client/functional/StartTest.java +++ b/temporal-sdk/src/test/java/io/temporal/client/functional/StartTest.java @@ -100,9 +100,9 @@ public void startOneArgsFuncWithDefault() { testWorkflowRule.getWorkflowClient().newWorkflowStub(Test1ArgWorkflowFunc.class, options); // Use worker that polls on a task queue configured through @WorkflowMethod annotation of // func1 - assertResult(1, WorkflowClient.start(stubF1::func1, 1)); + assertResult(1, WorkflowClient.start(stubF1::func1, "1")); Assert.assertEquals( - 1, stubF1.func1(1)); // Check that duplicated start just returns the result. + "1", stubF1.func1("1")); // Check that duplicated start just returns the result. } } diff --git a/temporal-test-server/src/test/java/io/temporal/internal/testservice/LinkConverterTest.java b/temporal-sdk/src/test/java/io/temporal/internal/common/LinkConverterTest.java similarity index 98% rename from temporal-test-server/src/test/java/io/temporal/internal/testservice/LinkConverterTest.java rename to temporal-sdk/src/test/java/io/temporal/internal/common/LinkConverterTest.java index 597b34ee9..46c99d244 100644 --- a/temporal-test-server/src/test/java/io/temporal/internal/testservice/LinkConverterTest.java +++ b/temporal-sdk/src/test/java/io/temporal/internal/common/LinkConverterTest.java @@ -18,10 +18,10 @@ * limitations under the License. */ -package io.temporal.internal.testservice; +package io.temporal.internal.common; -import static io.temporal.internal.testservice.LinkConverter.nexusLinkToWorkflowEvent; -import static io.temporal.internal.testservice.LinkConverter.workflowEventToNexusLink; +import static io.temporal.internal.common.LinkConverter.nexusLinkToWorkflowEvent; +import static io.temporal.internal.common.LinkConverter.workflowEventToNexusLink; import static org.junit.Assert.*; import io.temporal.api.common.v1.Link; diff --git a/temporal-sdk/src/test/java/io/temporal/internal/common/NexusUtilTest.java b/temporal-sdk/src/test/java/io/temporal/internal/common/NexusUtilTest.java new file mode 100644 index 000000000..86e979eec --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/internal/common/NexusUtilTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.common; + +import org.junit.Assert; +import org.junit.Test; + +public class NexusUtilTest { + @Test + public void testParseRequestTimeout() { + Assert.assertThrows( + IllegalArgumentException.class, () -> NexusUtil.parseRequestTimeout("invalid")); + Assert.assertThrows(IllegalArgumentException.class, () -> NexusUtil.parseRequestTimeout("1h")); + Assert.assertEquals(java.time.Duration.ofMillis(10), NexusUtil.parseRequestTimeout("10ms")); + Assert.assertEquals(java.time.Duration.ofMillis(10), NexusUtil.parseRequestTimeout("10.1ms")); + Assert.assertEquals(java.time.Duration.ofSeconds(1), NexusUtil.parseRequestTimeout("1s")); + Assert.assertEquals(java.time.Duration.ofMinutes(999), NexusUtil.parseRequestTimeout("999m")); + Assert.assertEquals(java.time.Duration.ofMillis(1300), NexusUtil.parseRequestTimeout("1.3s")); + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java b/temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java new file mode 100644 index 000000000..6a1a85eb4 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.nexus; + +import com.google.protobuf.ByteString; +import com.uber.m3.tally.RootScopeBuilder; +import com.uber.m3.tally.Scope; +import com.uber.m3.util.Duration; +import io.nexusrpc.Header; +import io.nexusrpc.OperationInfo; +import io.nexusrpc.OperationStillRunningException; +import io.nexusrpc.handler.*; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.nexus.v1.Request; +import io.temporal.api.nexus.v1.StartOperationRequest; +import io.temporal.api.workflowservice.v1.PollNexusTaskQueueResponse; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.DefaultDataConverter; +import io.temporal.common.reporter.TestStatsReporter; +import io.temporal.internal.worker.NexusTask; +import io.temporal.internal.worker.NexusTaskHandler; +import io.temporal.workflow.shared.TestNexusServices; +import java.util.concurrent.TimeoutException; +import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class NexusTaskHandlerImplTest { + static final String NAMESPACE = "testNamespace"; + static final String TASK_QUEUE = "testTaskQueue"; + static final DataConverter dataConverter = DefaultDataConverter.STANDARD_INSTANCE; + private Scope metricsScope; + private TestStatsReporter reporter; + + @Before + public void setUp() { + reporter = new TestStatsReporter(); + metricsScope = new RootScopeBuilder().reporter(reporter).reportEvery(Duration.ofMillis(10)); + } + + @Test + public void nexusTaskHandlerImplStartNoService() { + NexusTaskHandlerImpl nexusTaskHandlerImpl = + new NexusTaskHandlerImpl(null, NAMESPACE, TASK_QUEUE, dataConverter); + // Verify if no service is registered, start should return false + Assert.assertFalse(nexusTaskHandlerImpl.start()); + } + + @Test + public void nexusTaskHandlerImplStart() { + NexusTaskHandlerImpl nexusTaskHandlerImpl = + new NexusTaskHandlerImpl(null, NAMESPACE, TASK_QUEUE, dataConverter); + nexusTaskHandlerImpl.registerNexusServiceImplementations( + new Object[] {new TestNexusServiceImpl()}); + // Verify if any services are registered, start should return true + Assert.assertTrue(nexusTaskHandlerImpl.start()); + } + + @Test + public void startSyncTask() throws TimeoutException { + NexusTaskHandlerImpl nexusTaskHandlerImpl = + new NexusTaskHandlerImpl(null, NAMESPACE, TASK_QUEUE, dataConverter); + nexusTaskHandlerImpl.registerNexusServiceImplementations( + new Object[] {new TestNexusServiceImpl()}); + nexusTaskHandlerImpl.start(); + + Payload originalPayload = dataConverter.toPayload("world").get(); + PollNexusTaskQueueResponse.Builder task = + PollNexusTaskQueueResponse.newBuilder() + .setRequest( + Request.newBuilder() + .setStartOperation( + StartOperationRequest.newBuilder() + .setOperation("operation") + .setService("TestNexusService1") + // Passing bytes that are not valid UTF-8 to make sure this does not + // error out + .setPayload( + Payload.newBuilder(originalPayload) + .putMetadata( + "ByteKey", ByteString.copyFrom("\\xc3\\x28".getBytes())) + .build()) + .build())); + + NexusTaskHandler.Result result = + nexusTaskHandlerImpl.handle(new NexusTask(task, null, null), metricsScope); + Assert.assertNull(result.getHandlerError()); + Assert.assertNotNull(result.getResponse()); + Assert.assertEquals( + "Hello, world!", + dataConverter.fromPayload( + result.getResponse().getStartOperation().getSyncSuccess().getPayload(), + String.class, + String.class)); + } + + @Test + public void syncTimeoutTask() { + NexusTaskHandlerImpl nexusTaskHandlerImpl = + new NexusTaskHandlerImpl(null, NAMESPACE, TASK_QUEUE, dataConverter); + nexusTaskHandlerImpl.registerNexusServiceImplementations( + new Object[] {new TestNexusServiceImpl2()}); + nexusTaskHandlerImpl.start(); + + PollNexusTaskQueueResponse.Builder task = + PollNexusTaskQueueResponse.newBuilder() + .setRequest( + Request.newBuilder() + .putHeader(Header.REQUEST_TIMEOUT, "100ms") + .setStartOperation( + StartOperationRequest.newBuilder() + .setOperation("operation") + .setService("TestNexusService2") + .setPayload(dataConverter.toPayload(1).get()) + .build())); + + Assert.assertThrows( + TimeoutException.class, + () -> nexusTaskHandlerImpl.handle(new NexusTask(task, null, null), metricsScope)); + } + + @Test + public void startAsyncSyncOperation() throws TimeoutException { + NexusTaskHandlerImpl nexusTaskHandlerImpl = + new NexusTaskHandlerImpl(null, NAMESPACE, TASK_QUEUE, dataConverter); + nexusTaskHandlerImpl.registerNexusServiceImplementations( + new Object[] {new TestNexusServiceImplAsync()}); + nexusTaskHandlerImpl.start(); + + PollNexusTaskQueueResponse.Builder task = + PollNexusTaskQueueResponse.newBuilder() + .setRequest( + Request.newBuilder() + .setStartOperation( + StartOperationRequest.newBuilder() + .setOperation("operation") + .setService("TestNexusService1") + .setPayload(dataConverter.toPayload("test id").get()) + .build())); + + NexusTaskHandler.Result result = + nexusTaskHandlerImpl.handle(new NexusTask(task, null, null), metricsScope); + Assert.assertNull(result.getHandlerError()); + Assert.assertNotNull(result.getResponse()); + Assert.assertEquals( + "test id", result.getResponse().getStartOperation().getAsyncSuccess().getOperationId()); + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync((ctx, details, name) -> "Hello, " + name + "!"); + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService2.class) + public class TestNexusServiceImpl2 { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync( + (ctx, details, i) -> { + while (!ctx.isMethodCancelled()) { + try { + Thread.sleep(1000 * i); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return i; + }); + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImplAsync { + @OperationImpl + public OperationHandler operation() { + // Implemented via handler + return new AsyncHandle(); + } + + // Naive example showing async operations tracked in-memory + private class AsyncHandle implements OperationHandler { + + @Override + public OperationStartResult start( + OperationContext context, OperationStartDetails details, @Nullable String id) { + return OperationStartResult.async(id); + } + + @Override + public String fetchResult(OperationContext context, OperationFetchResultDetails details) + throws OperationStillRunningException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public OperationInfo fetchInfo(OperationContext context, OperationFetchInfoDetails details) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void cancel(OperationContext context, OperationCancelDetails details) { + throw new UnsupportedOperationException("Not implemented"); + } + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/internal/nexus/PayloadSerializerTest.java b/temporal-sdk/src/test/java/io/temporal/internal/nexus/PayloadSerializerTest.java new file mode 100644 index 000000000..8ab02c87c --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/internal/nexus/PayloadSerializerTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.nexus; + +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.DefaultDataConverter; +import org.junit.Assert; +import org.junit.Test; + +public class PayloadSerializerTest { + static DataConverter dataConverter = DefaultDataConverter.STANDARD_INSTANCE; + PayloadSerializer payloadSerializer = new PayloadSerializer(dataConverter); + + @Test + public void testPayload() { + String original = "test"; + PayloadSerializer.Content content = payloadSerializer.serialize(original); + Assert.assertEquals(original, payloadSerializer.deserialize(content, String.class)); + } + + @Test + public void testNull() { + PayloadSerializer.Content content = payloadSerializer.serialize(null); + payloadSerializer.deserialize(content, String.class); + Assert.assertEquals(null, payloadSerializer.deserialize(content, String.class)); + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachineTest.java b/temporal-sdk/src/test/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachineTest.java new file mode 100644 index 000000000..6297500f6 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/internal/statemachines/CancelNexusOperationStateMachineTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.statemachines; + +import static io.temporal.internal.statemachines.NexusOperationStateMachineTest.*; +import static io.temporal.internal.statemachines.TestHistoryBuilder.assertCommand; +import static org.junit.Assert.*; + +import io.temporal.api.command.v1.Command; +import io.temporal.api.command.v1.RequestCancelNexusOperationCommandAttributes; +import io.temporal.api.command.v1.ScheduleNexusOperationCommandAttributes; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.enums.v1.CommandType; +import io.temporal.api.enums.v1.EventType; +import io.temporal.api.failure.v1.Failure; +import io.temporal.api.history.v1.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.junit.AfterClass; +import org.junit.Test; + +public class CancelNexusOperationStateMachineTest { + private WorkflowStateMachines stateMachines; + + private static final List< + StateMachine< + CancelNexusOperationStateMachine.State, + CancelNexusOperationStateMachine.ExplicitEvent, + CancelNexusOperationStateMachine>> + stateMachineList = new ArrayList<>(); + + private WorkflowStateMachines newStateMachines(TestEntityManagerListenerBase listener) { + return new WorkflowStateMachines(listener, (stateMachineList::add)); + } + + @AfterClass + public static void generateCoverage() { + List< + Transition< + CancelNexusOperationStateMachine.State, + TransitionEvent>> + missed = + CancelNexusOperationStateMachine.STATE_MACHINE_DEFINITION.getUnvisitedTransitions( + stateMachineList); + if (!missed.isEmpty()) { + CommandsGeneratePlantUMLStateDiagrams.writeToFile( + "test", + CancelNexusOperationStateMachine.class, + CancelNexusOperationStateMachine.STATE_MACHINE_DEFINITION.asPlantUMLStateDiagramCoverage( + stateMachineList)); + fail( + "CancelNexusOperationStateMachine is missing test coverage for the following transitions:\n" + + missed); + } + } + + @Test + public void testCancelNexusOperationStateMachine() { + class TestListener extends TestEntityManagerListenerBase { + @Override + protected void buildWorkflow(AsyncWorkflowBuilder builder) { + RequestCancelNexusOperationCommandAttributes cancelAttributes = + RequestCancelNexusOperationCommandAttributes.newBuilder() + .setScheduledEventId(5) + .build(); + ScheduleNexusOperationCommandAttributes scheduleAttributes = + newScheduleNexusOperationCommandAttributesBuilder().build(); + NexusOperationStateMachineTest.DelayedCallback2, Failure> + delayedCallback = new NexusOperationStateMachineTest.DelayedCallback2(); + builder + ., Failure>add2( + (v, c) -> + stateMachines.startNexusOperation(scheduleAttributes, c, delayedCallback::run)) + .add((v) -> stateMachines.requestCancelNexusOperation(cancelAttributes)) + ., Failure>add2((pair, c) -> delayedCallback.set(c)) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + } + } + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_STARTED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + 9: EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED + 10: EVENT_TYPE_NEXUS_OPERATION_CANCELED + 11: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 12: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = + new TestHistoryBuilder() + .add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED) + .addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + NexusOperationStartedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setOperationId(OPERATION_ID) + .build()) + .addWorkflowTask() + .add( + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED, + NexusOperationCancelRequestedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .build()) + .add( + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCELED, + NexusOperationCanceledEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setFailure(Failure.newBuilder().setMessage("canceled").build()) + .build()) + .addWorkflowTaskScheduledAndStarted(); + { + TestEntityManagerListenerBase listener = new TestListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertEquals(1, commands.size()); + assertCommand(CommandType.COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION, commands); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 3); + assertEquals(1, commands.size()); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/internal/statemachines/NexusOperationStateMachineTest.java b/temporal-sdk/src/test/java/io/temporal/internal/statemachines/NexusOperationStateMachineTest.java new file mode 100644 index 000000000..8edf61048 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/internal/statemachines/NexusOperationStateMachineTest.java @@ -0,0 +1,802 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.internal.statemachines; + +import static io.temporal.internal.statemachines.TestHistoryBuilder.assertCommand; +import static org.junit.Assert.*; + +import io.temporal.api.command.v1.Command; +import io.temporal.api.command.v1.ScheduleNexusOperationCommandAttributes; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.common.v1.Payloads; +import io.temporal.api.enums.v1.CommandType; +import io.temporal.api.enums.v1.EventType; +import io.temporal.api.failure.v1.Failure; +import io.temporal.api.failure.v1.NexusOperationFailureInfo; +import io.temporal.api.history.v1.*; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.DefaultDataConverter; +import io.temporal.workflow.Functions; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Test; + +public class NexusOperationStateMachineTest { + private static final String OPERATION = "test-operation"; + private static final String SERVICE = "test-service"; + private static final String ENDPOINT = "test-endpoint"; + static final String OPERATION_ID = "test-operation-id"; + private final DataConverter converter = DefaultDataConverter.STANDARD_INSTANCE; + private static final List< + StateMachine< + NexusOperationStateMachine.State, + NexusOperationStateMachine.ExplicitEvent, + NexusOperationStateMachine>> + stateMachineList = new ArrayList<>(); + private WorkflowStateMachines stateMachines; + + private WorkflowStateMachines newStateMachines(TestEntityManagerListenerBase listener) { + return new WorkflowStateMachines(listener, (stateMachineList::add)); + } + + @AfterClass + public static void generateCoverage() { + List< + Transition< + NexusOperationStateMachine.State, + TransitionEvent>> + missed = + NexusOperationStateMachine.STATE_MACHINE_DEFINITION.getUnvisitedTransitions( + stateMachineList); + if (!missed.isEmpty()) { + CommandsGeneratePlantUMLStateDiagrams.writeToFile( + "test", + NexusOperationStateMachine.class, + NexusOperationStateMachine.STATE_MACHINE_DEFINITION.asPlantUMLStateDiagramCoverage( + stateMachineList)); + fail( + "NexusOperationStateMachine is missing test coverage for the following transitions:\n" + + missed); + } + } + + @Test + public void testSyncNexusOperationCompletion() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + builder + ., Failure>add2( + (v, c) -> stateMachines.startNexusOperation(attributes.build(), (o, f) -> {}, c)) + .add( + (pair) -> + stateMachines.completeWorkflow( + Optional.of( + Payloads.newBuilder().addPayloads(pair.getT1().get()).build()))); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_COMPLETED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + NexusOperationCompletedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setResult(converter.toPayload("result1").get())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(2, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, commands); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, commands); + assertEquals( + "result1", + converter.fromPayloads( + 0, + Optional.of( + commands.get(0).getCompleteWorkflowExecutionCommandAttributes().getResult()), + String.class, + String.class)); + } + } + + @Test + public void testSyncNexusOperationFailure() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + builder + ., Failure>add2( + (v, c) -> stateMachines.startNexusOperation(attributes.build(), (o, f) -> {}, c)) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_FAILED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_FAILED, + NexusOperationFailedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setFailure( + Failure.newBuilder() + .setMessage("failed") + .setNexusOperationExecutionFailureInfo( + NexusOperationFailureInfo.newBuilder().build()) + .build())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(2, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + assertEquals( + "failed", + commands.get(0).getFailWorkflowExecutionCommandAttributes().getFailure().getMessage()); + } + } + + @Test + public void testSyncNexusOperationCanceled() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + builder + ., Failure>add2( + (v, c) -> stateMachines.startNexusOperation(attributes.build(), (o, f) -> {}, c)) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_CANCELED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCELED, + NexusOperationCanceledEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setFailure( + Failure.newBuilder() + .setMessage("canceled") + .setNexusOperationExecutionFailureInfo( + NexusOperationFailureInfo.newBuilder().build()) + .build())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(2, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + assertEquals( + "canceled", + commands.get(0).getFailWorkflowExecutionCommandAttributes().getFailure().getMessage()); + } + } + + @Test + public void testSyncNexusOperationTimedout() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + builder + ., Failure>add2( + (v, c) -> stateMachines.startNexusOperation(attributes.build(), (o, f) -> {}, c)) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT, + NexusOperationTimedOutEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setFailure( + Failure.newBuilder() + .setMessage("timed out") + .setNexusOperationExecutionFailureInfo( + NexusOperationFailureInfo.newBuilder().build()) + .build())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(2, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + assertEquals( + "timed out", + commands.get(0).getFailWorkflowExecutionCommandAttributes().getFailure().getMessage()); + } + } + + @Test + public void testSyncNexusOperationImmediateCancellation() { + class TestNexusListener extends TestEntityManagerListenerBase { + private Functions.Proc cancellationHandler; + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + builder + ., Failure>add2( + (v, c) -> + cancellationHandler = + stateMachines.startNexusOperation(attributes.build(), (o, f) -> {}, c)) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + // Immediate cancellation + builder.add((v) -> cancellationHandler.apply()); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTaskScheduledAndStarted(); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + } + } + + @Test + public void testAsyncNexusOperationCompletion() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + DelayedCallback2, Failure> delayedCallback = new DelayedCallback2(); + builder + ., Failure>add2( + (v, c) -> + stateMachines.startNexusOperation(attributes.build(), c, delayedCallback::run)) + ., Failure>add2( + (pair, c) -> { + Assert.assertEquals(OPERATION_ID, pair.getT1().get()); + delayedCallback.set(c); + }) + .add( + (pair) -> + stateMachines.completeWorkflow( + Optional.of( + Payloads.newBuilder().addPayloads(pair.getT1().get()).build()))); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_STARTED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + 9: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 6: EVENT_TYPE_NEXUS_OPERATION_COMPLETED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + NexusOperationStartedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setOperationId(OPERATION_ID) + .build()); + h.addWorkflowTask(); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, + NexusOperationCompletedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setResult(converter.toPayload("result1").get())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(3, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertEquals(0, commands.size()); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 3); + assertCommand(CommandType.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, commands); + assertEquals( + "result1", + converter.fromPayloads( + 0, + Optional.of( + commands.get(0).getCompleteWorkflowExecutionCommandAttributes().getResult()), + String.class, + String.class)); + } + } + + @Test + public void testAsyncNexusOperationFailed() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + DelayedCallback2, Failure> delayedCallback = new DelayedCallback2(); + builder + ., Failure>add2( + (v, c) -> + stateMachines.startNexusOperation(attributes.build(), c, delayedCallback::run)) + ., Failure>add2( + (pair, c) -> { + Assert.assertEquals(OPERATION_ID, pair.getT1().get()); + delayedCallback.set(c); + }) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_STARTED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + 9: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 6: EVENT_TYPE_NEXUS_OPERATION_FAILED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + NexusOperationStartedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setOperationId(OPERATION_ID) + .build()); + h.addWorkflowTask(); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_FAILED, + NexusOperationFailedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setFailure( + Failure.newBuilder() + .setMessage("failed") + .setNexusOperationExecutionFailureInfo( + NexusOperationFailureInfo.newBuilder().build()) + .build())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(3, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertEquals(0, commands.size()); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 3); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + assertEquals( + "failed", + commands.get(0).getFailWorkflowExecutionCommandAttributes().getFailure().getMessage()); + } + } + + @Test + public void testAsyncNexusOperationCanceled() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + DelayedCallback2, Failure> delayedCallback = new DelayedCallback2(); + builder + ., Failure>add2( + (v, c) -> + stateMachines.startNexusOperation(attributes.build(), c, delayedCallback::run)) + ., Failure>add2( + (pair, c) -> { + Assert.assertEquals(OPERATION_ID, pair.getT1().get()); + delayedCallback.set(c); + }) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_STARTED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + 9: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 6: EVENT_TYPE_NEXUS_OPERATION_CANCELED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + NexusOperationStartedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setOperationId(OPERATION_ID) + .build()); + h.addWorkflowTask(); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_CANCELED, + NexusOperationCanceledEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setFailure( + Failure.newBuilder() + .setMessage("canceled") + .setNexusOperationExecutionFailureInfo( + NexusOperationFailureInfo.newBuilder().build()) + .build())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(3, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertEquals(0, commands.size()); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 3); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + assertEquals( + "canceled", + commands.get(0).getFailWorkflowExecutionCommandAttributes().getFailure().getMessage()); + } + } + + @Test + public void testAsyncNexusOperationTimeout() { + class TestNexusListener extends TestEntityManagerListenerBase { + + @Override + public void buildWorkflow(AsyncWorkflowBuilder builder) { + ScheduleNexusOperationCommandAttributes.Builder attributes = + newScheduleNexusOperationCommandAttributesBuilder(); + DelayedCallback2, Failure> delayedCallback = new DelayedCallback2(); + builder + ., Failure>add2( + (v, c) -> + stateMachines.startNexusOperation(attributes.build(), c, delayedCallback::run)) + ., Failure>add2( + (pair, c) -> { + Assert.assertEquals(OPERATION_ID, pair.getT1().get()); + delayedCallback.set(c); + }) + .add((pair) -> stateMachines.failWorkflow(pair.getT2())); + } + } + + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_NEXUS_OPERATION_SCHEDULED + 6: EVENT_TYPE_NEXUS_OPERATION_STARTED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + 9: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 6: EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + TestHistoryBuilder h = new TestHistoryBuilder(); + { + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + h.add(EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED); + h.addWorkflowTask(); + long scheduledEventId = + h.addGetEventId( + EventType.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, + newNexusOperationScheduledEventAttributesBuilder().build()); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED, + NexusOperationStartedEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setOperationId(OPERATION_ID) + .build()); + h.addWorkflowTask(); + h.add( + EventType.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT, + NexusOperationTimedOutEventAttributes.newBuilder() + .setScheduledEventId(scheduledEventId) + .setRequestId("requestId") + .setFailure( + Failure.newBuilder() + .setMessage("timed out") + .setNexusOperationExecutionFailureInfo( + NexusOperationFailureInfo.newBuilder().build()) + .build())); + h.addWorkflowTaskScheduledAndStarted(); + assertEquals(3, h.getWorkflowTaskCount()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 1); + assertCommand(CommandType.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, commands); + assertEquals( + OPERATION, commands.get(0).getScheduleNexusOperationCommandAttributes().getOperation()); + } + { + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 2); + assertEquals(0, commands.size()); + } + { + // Full replay + TestEntityManagerListenerBase listener = new TestNexusListener(); + stateMachines = newStateMachines(listener); + List commands = h.handleWorkflowTaskTakeCommands(stateMachines, 3); + assertCommand(CommandType.COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, commands); + assertEquals( + "timed out", + commands.get(0).getFailWorkflowExecutionCommandAttributes().getFailure().getMessage()); + } + } + + static ScheduleNexusOperationCommandAttributes.Builder + newScheduleNexusOperationCommandAttributesBuilder() { + return ScheduleNexusOperationCommandAttributes.newBuilder() + .setOperation(OPERATION) + .setService(SERVICE) + .setEndpoint(ENDPOINT); + } + + static NexusOperationScheduledEventAttributes.Builder + newNexusOperationScheduledEventAttributesBuilder() { + return NexusOperationScheduledEventAttributes.newBuilder() + .setOperation(OPERATION) + .setService(SERVICE) + .setEndpoint(ENDPOINT); + } + + public static class DelayedCallback2 { + private final AtomicReference> callback = new AtomicReference<>(); + + public void set(Functions.Proc2 callback) { + this.callback.set(callback); + } + + public void run(T1 t1, T2 t2) { + callback.get().apply(t1, t2); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/internal/statemachines/TestHistoryBuilder.java b/temporal-sdk/src/test/java/io/temporal/internal/statemachines/TestHistoryBuilder.java index 2c85e06e5..b52d8c5c8 100644 --- a/temporal-sdk/src/test/java/io/temporal/internal/statemachines/TestHistoryBuilder.java +++ b/temporal-sdk/src/test/java/io/temporal/internal/statemachines/TestHistoryBuilder.java @@ -408,7 +408,6 @@ private Object newAttributes(EventType type, long initiatedEventId) { case EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TERMINATED: return ChildWorkflowExecutionTerminatedEventAttributes.newBuilder() .setInitiatedEventId(initiatedEventId); - case EVENT_TYPE_UNSPECIFIED: case EVENT_TYPE_WORKFLOW_EXECUTION_FAILED: case EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT: @@ -565,7 +564,34 @@ private HistoryEvent newAttributes(EventType type, Object attributes) { result.setExternalWorkflowExecutionCancelRequestedEventAttributes( (ExternalWorkflowExecutionCancelRequestedEventAttributes) attributes); break; - + case EVENT_TYPE_NEXUS_OPERATION_SCHEDULED: + result.setNexusOperationScheduledEventAttributes( + (NexusOperationScheduledEventAttributes) attributes); + break; + case EVENT_TYPE_NEXUS_OPERATION_STARTED: + result.setNexusOperationStartedEventAttributes( + (NexusOperationStartedEventAttributes) attributes); + break; + case EVENT_TYPE_NEXUS_OPERATION_COMPLETED: + result.setNexusOperationCompletedEventAttributes( + (NexusOperationCompletedEventAttributes) attributes); + break; + case EVENT_TYPE_NEXUS_OPERATION_CANCELED: + result.setNexusOperationCanceledEventAttributes( + (NexusOperationCanceledEventAttributes) attributes); + break; + case EVENT_TYPE_NEXUS_OPERATION_FAILED: + result.setNexusOperationFailedEventAttributes( + (NexusOperationFailedEventAttributes) attributes); + break; + case EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT: + result.setNexusOperationTimedOutEventAttributes( + (NexusOperationTimedOutEventAttributes) attributes); + break; + case EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED: + result.setNexusOperationCancelRequestedEventAttributes( + (NexusOperationCancelRequestedEventAttributes) attributes); + break; case EVENT_TYPE_UNSPECIFIED: case EVENT_TYPE_WORKFLOW_EXECUTION_FAILED: case EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT: diff --git a/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotGrpcInterceptedTests.java b/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotGrpcInterceptedTests.java index 274a066b2..3da0d4dd8 100644 --- a/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotGrpcInterceptedTests.java +++ b/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotGrpcInterceptedTests.java @@ -37,10 +37,7 @@ import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.worker.MetricsType; import io.temporal.worker.WorkerOptions; -import io.temporal.worker.tuning.ActivitySlotInfo; -import io.temporal.worker.tuning.CompositeTuner; -import io.temporal.worker.tuning.LocalActivitySlotInfo; -import io.temporal.worker.tuning.WorkflowSlotInfo; +import io.temporal.worker.tuning.*; import io.temporal.workflow.Workflow; import io.temporal.workflow.shared.TestActivities; import io.temporal.workflow.shared.TestWorkflows; @@ -56,12 +53,15 @@ public class WorkflowSlotGrpcInterceptedTests { private final int MAX_CONCURRENT_WORKFLOW_TASK_EXECUTION_SIZE = 100; private final int MAX_CONCURRENT_ACTIVITY_EXECUTION_SIZE = 1000; private final int MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE = 10000; + private final int MAX_CONCURRENT_NEXUS_EXECUTION_SIZE = 10000; private final CountingSlotSupplier workflowTaskSlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_WORKFLOW_TASK_EXECUTION_SIZE); private final CountingSlotSupplier activityTaskSlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_ACTIVITY_EXECUTION_SIZE); private final CountingSlotSupplier localActivitySlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE); + private final CountingSlotSupplier nexusSlotSupplier = + new CountingSlotSupplier<>(MAX_CONCURRENT_NEXUS_EXECUTION_SIZE); private final TestStatsReporter reporter = new TestStatsReporter(); private static final MaybeFailWFTResponseInterceptor MAYBE_FAIL_INTERCEPTOR = new MaybeFailWFTResponseInterceptor(); @@ -81,7 +81,8 @@ public class WorkflowSlotGrpcInterceptedTests { new CompositeTuner( workflowTaskSlotSupplier, activityTaskSlotSupplier, - localActivitySlotSupplier)) + localActivitySlotSupplier, + nexusSlotSupplier)) .build()) .setMetricsScope(metricsScope) .setActivityImplementations(new TestActivities.TestActivitiesImpl()) diff --git a/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotTests.java b/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotTests.java index 45dfb2884..435fbd1f5 100644 --- a/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotTests.java +++ b/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotTests.java @@ -25,6 +25,9 @@ import com.uber.m3.tally.RootScopeBuilder; import com.uber.m3.tally.Scope; import com.uber.m3.util.ImmutableMap; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; import io.temporal.activity.*; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; @@ -34,11 +37,9 @@ import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.worker.MetricsType; import io.temporal.worker.WorkerOptions; -import io.temporal.worker.tuning.ActivitySlotInfo; -import io.temporal.worker.tuning.CompositeTuner; -import io.temporal.worker.tuning.LocalActivitySlotInfo; -import io.temporal.worker.tuning.WorkflowSlotInfo; +import io.temporal.worker.tuning.*; import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; import java.time.Duration; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -51,15 +52,18 @@ public class WorkflowSlotTests { private final int MAX_CONCURRENT_WORKFLOW_TASK_EXECUTION_SIZE = 100; private final int MAX_CONCURRENT_ACTIVITY_EXECUTION_SIZE = 1000; private final int MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE = 10000; + private final int MAX_CONCURRENT_NEXUS_EXECUTION_SIZE = 2000; private final CountingSlotSupplier workflowTaskSlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_WORKFLOW_TASK_EXECUTION_SIZE); private final CountingSlotSupplier activityTaskSlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_ACTIVITY_EXECUTION_SIZE); private final CountingSlotSupplier localActivitySlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE); + private final CountingSlotSupplier nexusSlotSupplier = + new CountingSlotSupplier<>(MAX_CONCURRENT_NEXUS_EXECUTION_SIZE); private final TestStatsReporter reporter = new TestStatsReporter(); - static CountDownLatch activityBlockLatch = new CountDownLatch(1); - static CountDownLatch activityRunningLatch = new CountDownLatch(1); + static CountDownLatch blockLatch = new CountDownLatch(1); + static CountDownLatch runningLatch = new CountDownLatch(1); static boolean didFail = false; Scope metricsScope = @@ -74,19 +78,21 @@ public class WorkflowSlotTests { new CompositeTuner( workflowTaskSlotSupplier, activityTaskSlotSupplier, - localActivitySlotSupplier)) + localActivitySlotSupplier, + nexusSlotSupplier)) .build()) .setMetricsScope(metricsScope) .setActivityImplementations(new TestActivityImpl()) .setWorkflowTypes(SleepingWorkflowImpl.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) .setDoNotStart(true) .build(); @Before public void setup() { reporter.flush(); - activityBlockLatch = new CountDownLatch(1); - activityRunningLatch = new CountDownLatch(1); + blockLatch = new CountDownLatch(1); + runningLatch = new CountDownLatch(1); localActivitySlotSupplier.usedCount.set(0); didFail = false; } @@ -101,10 +107,11 @@ public void tearDown() { assertEquals( localActivitySlotSupplier.reservedCount.get(), localActivitySlotSupplier.releasedCount.get()); + assertEquals(nexusSlotSupplier.reservedCount.get(), nexusSlotSupplier.releasedCount.get()); } // Arguments are the number of used slots by type - private void assertWorkerSlotCount(int worker, int activity, int localActivity) { + private void assertWorkerSlotCount(int worker, int activity, int localActivity, int nexus) { try { // There can be a delay in metrics emission, another option if this // is too flaky is to poll the metrics. @@ -164,6 +171,16 @@ public static class SleepingWorkflowImpl implements TestWorkflow { .build()) .validateAndBuildWithDefaults()); + private final TestNexusServices.TestNexusService1 nexusService = + Workflow.newNexusServiceStub( + TestNexusServices.TestNexusService1.class, + NexusServiceOptions.newBuilder() + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()) + .build()); + @Override public String workflow(String action) { Workflow.await(() -> unblocked); @@ -176,6 +193,8 @@ public String workflow(String action) { localActivity.activity("fail"); } else if (action.equals("activity")) { activity.activity("test"); + } else if (action.equals("nexus")) { + nexusService.operation("test"); } return "ok"; } @@ -196,13 +215,13 @@ public interface TestActivity { public static class TestActivityImpl implements TestActivity { @Override public String activity(String input) { - activityRunningLatch.countDown(); + runningLatch.countDown(); try { ActivityExecutionContext executionContext = Activity.getExecutionContext(); if (input.equals("fail") && executionContext.getInfo().getAttempt() < 4) { throw new RuntimeException("fail on purpose"); } - activityBlockLatch.await(); + blockLatch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -210,6 +229,23 @@ public String activity(String input) { } } + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync( + (ctx, details, input) -> { + runningLatch.countDown(); + try { + blockLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return ""; + }); + } + } + private Map getWorkerTags(String workerType) { return ImmutableMap.of( "worker_type", @@ -232,7 +268,7 @@ public void TestTaskSlotsEmittedOnStart() { // Start the worker testWorkflowRule.getTestEnvironment().start(); // All slots should be available - assertWorkerSlotCount(0, 0, 0); + assertWorkerSlotCount(0, 0, 0, 0); } @Test @@ -247,15 +283,38 @@ public void TestActivityTaskSlots() throws InterruptedException { .validateBuildWithDefaults()); WorkflowClient.start(workflow::workflow, "activity"); workflow.unblock(); - activityRunningLatch.await(); + runningLatch.await(); // The activity slot should be taken and the workflow slot should not be taken - assertWorkerSlotCount(0, 1, 0); + assertWorkerSlotCount(0, 1, 0, 0); - activityBlockLatch.countDown(); + blockLatch.countDown(); // Wait for the workflow to finish workflow.workflow("activity"); // All slots should be available - assertWorkerSlotCount(0, 0, 0); + assertWorkerSlotCount(0, 0, 0, 0); + } + + @Test + public void TestNexusTaskSlots() throws InterruptedException { + testWorkflowRule.getTestEnvironment().start(); + WorkflowClient client = testWorkflowRule.getWorkflowClient(); + TestWorkflow workflow = + client.newWorkflowStub( + TestWorkflow.class, + WorkflowOptions.newBuilder() + .setTaskQueue(testWorkflowRule.getTaskQueue()) + .validateBuildWithDefaults()); + WorkflowClient.start(workflow::workflow, "nexus"); + workflow.unblock(); + runningLatch.await(); + // The nexus slot should be taken and the workflow slot should not be taken + assertWorkerSlotCount(0, 0, 0, 1); + + blockLatch.countDown(); + // Wait for the workflow to finish + workflow.workflow("nexus"); + // All slots should be available + assertWorkerSlotCount(0, 0, 0, 0); } @Test @@ -270,14 +329,14 @@ public void TestLocalActivityTaskSlots() throws InterruptedException { .validateBuildWithDefaults()); WorkflowClient.start(workflow::workflow, "local-activity"); workflow.unblock(); - activityRunningLatch.await(); + runningLatch.await(); // The local activity slot should be taken and the workflow slot should be taken - assertWorkerSlotCount(1, 0, 1); + assertWorkerSlotCount(1, 0, 1, 0); - activityBlockLatch.countDown(); + blockLatch.countDown(); workflow.workflow("local-activity"); // All slots should be available - assertWorkerSlotCount(0, 0, 0); + assertWorkerSlotCount(0, 0, 0, 0); } @Test @@ -293,17 +352,17 @@ public void TestLocalActivityHeartbeat() throws InterruptedException { .validateBuildWithDefaults()); WorkflowClient.start(workflow::workflow, "local-activity"); workflow.unblock(); - activityRunningLatch.await(); + runningLatch.await(); // The local activity slot should be taken and the workflow slot should be taken - assertWorkerSlotCount(1, 0, 1); + assertWorkerSlotCount(1, 0, 1, 0); // Take long enough to heartbeat Thread.sleep(1000); - assertWorkerSlotCount(1, 0, 1); + assertWorkerSlotCount(1, 0, 1, 0); - activityBlockLatch.countDown(); + blockLatch.countDown(); workflow.workflow("local-activity"); // All slots should be available - assertWorkerSlotCount(0, 0, 0); + assertWorkerSlotCount(0, 0, 0, 0); } @Test @@ -319,13 +378,13 @@ public void TestLocalActivityFailsThenPasses() throws InterruptedException { .validateBuildWithDefaults()); WorkflowClient.start(workflow::workflow, "local-activity-fail"); workflow.unblock(); - activityRunningLatch.await(); + runningLatch.await(); // The local activity slot should be taken and the workflow slot should be taken - assertWorkerSlotCount(1, 0, 1); + assertWorkerSlotCount(1, 0, 1, 0); - activityBlockLatch.countDown(); + blockLatch.countDown(); workflow.workflow("local-activity-fail"); - assertWorkerSlotCount(0, 0, 0); + assertWorkerSlotCount(0, 0, 0, 0); // LA slots should only have been used once per attempt assertEquals(4, localActivitySlotSupplier.usedCount.get()); // We should have seen releases *per* attempt as well @@ -347,6 +406,6 @@ public void TestWFTFailure() { workflow.unblock(); workflow.workflow("fail"); // All slots should be available - assertWorkerSlotCount(0, 0, 0); + assertWorkerSlotCount(0, 0, 0, 0); } } diff --git a/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotsSmallSizeTests.java b/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotsSmallSizeTests.java index e70d1bfba..7020e37b3 100644 --- a/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotsSmallSizeTests.java +++ b/temporal-sdk/src/test/java/io/temporal/internal/worker/WorkflowSlotsSmallSizeTests.java @@ -33,10 +33,7 @@ import io.temporal.testUtils.CountingSlotSupplier; import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.worker.WorkerOptions; -import io.temporal.worker.tuning.ActivitySlotInfo; -import io.temporal.worker.tuning.CompositeTuner; -import io.temporal.worker.tuning.LocalActivitySlotInfo; -import io.temporal.worker.tuning.WorkflowSlotInfo; +import io.temporal.worker.tuning.*; import io.temporal.workflow.*; import java.time.Duration; import java.util.ArrayList; @@ -56,12 +53,15 @@ public class WorkflowSlotsSmallSizeTests { private final int MAX_CONCURRENT_WORKFLOW_TASK_EXECUTION_SIZE = 2; private final int MAX_CONCURRENT_ACTIVITY_EXECUTION_SIZE = 2; private final int MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE = 2; + private final int MAX_CONCURRENT_NEXUS_EXECUTION_SIZE = 2; private final CountingSlotSupplier workflowTaskSlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_WORKFLOW_TASK_EXECUTION_SIZE); private final CountingSlotSupplier activityTaskSlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_ACTIVITY_EXECUTION_SIZE); private final CountingSlotSupplier localActivitySlotSupplier = new CountingSlotSupplier<>(MAX_CONCURRENT_LOCAL_ACTIVITY_EXECUTION_SIZE); + private final CountingSlotSupplier nexusSlotSupplier = + new CountingSlotSupplier<>(MAX_CONCURRENT_NEXUS_EXECUTION_SIZE); static Semaphore parallelSemRunning = new Semaphore(0); static Semaphore parallelSemBlocked = new Semaphore(0); @@ -81,7 +81,8 @@ public static Object[] data() { new CompositeTuner( workflowTaskSlotSupplier, activityTaskSlotSupplier, - localActivitySlotSupplier)) + localActivitySlotSupplier, + nexusSlotSupplier)) .build()) .setActivityImplementations(new TestActivitySemaphoreImpl()) .setWorkflowTypes(ParallelActivities.class) diff --git a/temporal-sdk/src/test/java/io/temporal/worker/WorkerIsNotGettingStartedTest.java b/temporal-sdk/src/test/java/io/temporal/worker/WorkerIsNotGettingStartedTest.java index 4c11addde..0ac8011ed 100644 --- a/temporal-sdk/src/test/java/io/temporal/worker/WorkerIsNotGettingStartedTest.java +++ b/temporal-sdk/src/test/java/io/temporal/worker/WorkerIsNotGettingStartedTest.java @@ -28,7 +28,6 @@ import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowEnvironment; import java.util.Map; -import java.util.function.Function; import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; @@ -37,10 +36,12 @@ public class WorkerIsNotGettingStartedTest { private static final int WORKFLOW_POLL_COUNT = 11; private static final int ACTIVITY_POLL_COUNT = 18; + private static final int NEXUS_POLL_COUNT = 13; private static final String TASK_QUEUE = "test-workflow"; private static final String ACTIVITY_POLLER_THREAD_NAME_PREFIX = "Activity Poller task"; private static final String WORKFLOW_POLLER_THREAD_NAME_PREFIX = "Workflow Poller task"; + private static final String NEXUS_POLLER_THREAD_NAME_PREFIX = "Nexus Poller task"; private TestWorkflowEnvironment env; private Worker worker; @@ -55,6 +56,7 @@ public void setUp() throws Exception { WorkerOptions.newBuilder() .setMaxConcurrentWorkflowTaskPollers(WORKFLOW_POLL_COUNT) .setMaxConcurrentActivityTaskPollers(ACTIVITY_POLL_COUNT) + .setMaxConcurrentNexusExecutionSize(NEXUS_POLL_COUNT) .setLocalActivityWorkerOnly(true) .build()); // Need to register something for workers to start @@ -75,9 +77,26 @@ public void verifyThatWorkerIsNotGettingStarted() throws InterruptedException { Thread.sleep(1000); Map threads = Thread.getAllStackTraces().keySet().stream() - .map((t) -> t.getName().substring(0, Math.min(20, t.getName().length()))) - .collect(groupingBy(Function.identity(), Collectors.counting())); + .filter( + (t) -> + t.getName().startsWith(WORKFLOW_POLLER_THREAD_NAME_PREFIX) + || t.getName().startsWith(ACTIVITY_POLLER_THREAD_NAME_PREFIX) + || t.getName().startsWith(NEXUS_POLLER_THREAD_NAME_PREFIX)) + .map(Thread::getName) + .collect( + groupingBy( + (t) -> { + if (t.startsWith(WORKFLOW_POLLER_THREAD_NAME_PREFIX)) { + return WORKFLOW_POLLER_THREAD_NAME_PREFIX; + } else if (t.startsWith(ACTIVITY_POLLER_THREAD_NAME_PREFIX)) { + return ACTIVITY_POLLER_THREAD_NAME_PREFIX; + } else { + return NEXUS_POLLER_THREAD_NAME_PREFIX; + } + }, + Collectors.counting())); assertEquals(WORKFLOW_POLL_COUNT, (long) threads.get(WORKFLOW_POLLER_THREAD_NAME_PREFIX)); + assertFalse(threads.containsKey(NEXUS_POLLER_THREAD_NAME_PREFIX)); assertFalse(threads.containsKey(ACTIVITY_POLLER_THREAD_NAME_PREFIX)); assertNull(worker.activityWorker); } diff --git a/temporal-sdk/src/test/java/io/temporal/worker/WorkerOptionsTest.java b/temporal-sdk/src/test/java/io/temporal/worker/WorkerOptionsTest.java index 8bc91d274..893f10c8f 100644 --- a/temporal-sdk/src/test/java/io/temporal/worker/WorkerOptionsTest.java +++ b/temporal-sdk/src/test/java/io/temporal/worker/WorkerOptionsTest.java @@ -61,10 +61,12 @@ public void verifyNewBuilderFromExistingWorkerOptions() { .setMaxConcurrentActivityExecutionSize(1000) .setMaxConcurrentWorkflowTaskExecutionSize(500) .setMaxConcurrentLocalActivityExecutionSize(200) + .setMaxConcurrentNexusExecutionSize(300) .setWorkerTuner(mock(WorkerTuner.class)) .setMaxTaskQueueActivitiesPerSecond(50) .setMaxConcurrentWorkflowTaskPollers(4) .setMaxConcurrentActivityTaskPollers(3) + .setMaxConcurrentNexusTaskPollers(6) .setLocalActivityWorkerOnly(false) .setDefaultDeadlockDetectionTimeout(2) .setMaxHeartbeatThrottleInterval(Duration.ofSeconds(10)) @@ -88,6 +90,7 @@ public void verifyNewBuilderFromExistingWorkerOptions() { assertEquals( w1.getMaxConcurrentLocalActivityExecutionSize(), w2.getMaxConcurrentLocalActivityExecutionSize()); + assertEquals(w1.getMaxConcurrentNexusExecutionSize(), w2.getMaxConcurrentNexusExecutionSize()); assertSame(w1.getWorkerTuner(), w2.getWorkerTuner()); assertEquals( w1.getMaxTaskQueueActivitiesPerSecond(), w2.getMaxTaskQueueActivitiesPerSecond(), 0); @@ -95,6 +98,7 @@ public void verifyNewBuilderFromExistingWorkerOptions() { w1.getMaxConcurrentWorkflowTaskPollers(), w2.getMaxConcurrentWorkflowTaskPollers()); assertEquals( w1.getMaxConcurrentActivityTaskPollers(), w2.getMaxConcurrentActivityTaskPollers()); + assertEquals(w1.getMaxConcurrentNexusTaskPollers(), w2.getMaxConcurrentNexusTaskPollers()); assertEquals(w1.isLocalActivityWorkerOnly(), w2.isLocalActivityWorkerOnly()); assertEquals(w1.getMaxHeartbeatThrottleInterval(), w2.getMaxHeartbeatThrottleInterval()); assertEquals( @@ -121,11 +125,15 @@ public void canBuildMixedSlotSupplierTuner() { SlotSupplier localActivitySlotSupplier = ResourceBasedSlotSupplier.createForLocalActivity( resourceController, ResourceBasedTuner.DEFAULT_ACTIVITY_SLOT_OPTIONS); + SlotSupplier nexusSlotSupplier = new FixedSizeSlotSupplier<>(10); WorkerOptions.newBuilder() .setWorkerTuner( new CompositeTuner( - workflowTaskSlotSupplier, activityTaskSlotSupplier, localActivitySlotSupplier)) + workflowTaskSlotSupplier, + activityTaskSlotSupplier, + localActivitySlotSupplier, + nexusSlotSupplier)) .build(); } @@ -145,11 +153,15 @@ public void throwsIfResourceControllerIsNotSame() { SlotSupplier localActivitySlotSupplier = ResourceBasedSlotSupplier.createForLocalActivity( resourceController2, ResourceBasedTuner.DEFAULT_ACTIVITY_SLOT_OPTIONS); + SlotSupplier nexusSlotSupplier = new FixedSizeSlotSupplier<>(10); assertThrows( IllegalArgumentException.class, () -> new CompositeTuner( - workflowTaskSlotSupplier, activityTaskSlotSupplier, localActivitySlotSupplier)); + workflowTaskSlotSupplier, + activityTaskSlotSupplier, + localActivitySlotSupplier, + nexusSlotSupplier)); } } diff --git a/temporal-sdk/src/test/java/io/temporal/worker/WorkerPollerThreadCountTest.java b/temporal-sdk/src/test/java/io/temporal/worker/WorkerPollerThreadCountTest.java index acf13fbfb..38e2c1fc3 100644 --- a/temporal-sdk/src/test/java/io/temporal/worker/WorkerPollerThreadCountTest.java +++ b/temporal-sdk/src/test/java/io/temporal/worker/WorkerPollerThreadCountTest.java @@ -23,13 +23,16 @@ import static java.util.stream.Collectors.groupingBy; import static org.junit.Assert.assertEquals; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; import io.temporal.activity.ActivityInterface; import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.workflow.WorkflowInterface; import io.temporal.workflow.WorkflowMethod; +import io.temporal.workflow.shared.TestNexusServices; import java.util.Map; -import java.util.function.Function; import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; @@ -39,9 +42,11 @@ public class WorkerPollerThreadCountTest { private static final String ACTIVITY_POLLER_THREAD_NAME_PREFIX = "Activity Poller task"; private static final String WORKFLOW_POLLER_THREAD_NAME_PREFIX = "Workflow Poller task"; + private static final String NEXUS_POLLER_THREAD_NAME_PREFIX = "Nexus Poller task"; private static final int WORKFLOW_POLL_COUNT = 11; private static final int ACTIVITY_POLL_COUNT = 18; + private static final int NEXUS_POLL_COUNT = 13; private TestWorkflowEnvironment env; @@ -55,9 +60,11 @@ public void setUp() throws Exception { WorkerOptions.newBuilder() .setMaxConcurrentWorkflowTaskPollers(WORKFLOW_POLL_COUNT) .setMaxConcurrentActivityTaskPollers(ACTIVITY_POLL_COUNT) + .setMaxConcurrentNexusTaskPollers(NEXUS_POLL_COUNT) .build()); // Need to register something for workers to start worker.registerActivitiesImplementations(new ActivityImpl()); + worker.registerNexusServiceImplementation(new TestNexusServiceImpl()); worker.registerWorkflowImplementationTypes(WorkflowImpl.class); env.start(); } @@ -89,14 +96,40 @@ public static class WorkflowImpl implements Workflow { public void bar() {} } + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync((ctx, details, name) -> "Hello, " + name + "!"); + } + } + @Test public void testPollThreadCount() throws InterruptedException { Thread.sleep(1000); Map threads = Thread.getAllStackTraces().keySet().stream() - .map((t) -> t.getName().substring(0, Math.min(20, t.getName().length()))) - .collect(groupingBy(Function.identity(), Collectors.counting())); + .filter( + (t) -> + t.getName().startsWith(WORKFLOW_POLLER_THREAD_NAME_PREFIX) + || t.getName().startsWith(ACTIVITY_POLLER_THREAD_NAME_PREFIX) + || t.getName().startsWith(NEXUS_POLLER_THREAD_NAME_PREFIX)) + .map(Thread::getName) + .collect( + groupingBy( + (t) -> { + if (t.startsWith(WORKFLOW_POLLER_THREAD_NAME_PREFIX)) { + return WORKFLOW_POLLER_THREAD_NAME_PREFIX; + } else if (t.startsWith(ACTIVITY_POLLER_THREAD_NAME_PREFIX)) { + return ACTIVITY_POLLER_THREAD_NAME_PREFIX; + } else { + return NEXUS_POLLER_THREAD_NAME_PREFIX; + } + }, + Collectors.counting())); assertEquals(WORKFLOW_POLL_COUNT, (long) threads.get(WORKFLOW_POLLER_THREAD_NAME_PREFIX)); assertEquals(ACTIVITY_POLL_COUNT, (long) threads.get(ACTIVITY_POLLER_THREAD_NAME_PREFIX)); + assertEquals(NEXUS_POLL_COUNT, (long) threads.get(NEXUS_POLLER_THREAD_NAME_PREFIX)); } } diff --git a/temporal-sdk/src/test/java/io/temporal/worker/WorkerRegistrationTest.java b/temporal-sdk/src/test/java/io/temporal/worker/WorkerRegistrationTest.java new file mode 100644 index 000000000..33e53ec6e --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/worker/WorkerRegistrationTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.worker; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.shared.TestNexusServices; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class WorkerRegistrationTest { + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder().setDoNotStart(true).build(); + + @Test + public void testDuplicateRegistration() { + Worker worker = testWorkflowRule.getWorker(); + worker.registerNexusServiceImplementation(new TestNexusServiceImpl1()); + Assert.assertThrows( + TypeAlreadyRegisteredException.class, + () -> worker.registerNexusServiceImplementation(new TestNexusServiceImpl2())); + } + + @Test + public void testDuplicateRegistrationInSameCall() { + Worker worker = testWorkflowRule.getWorker(); + Assert.assertThrows( + TypeAlreadyRegisteredException.class, + () -> + worker.registerNexusServiceImplementation( + new TestNexusServiceImpl1(), new TestNexusServiceImpl2())); + } + + @Test + public void testRegistrationAfterStart() { + Worker worker = testWorkflowRule.getWorker(); + worker.start(); + Assert.assertThrows( + IllegalStateException.class, + () -> worker.registerNexusServiceImplementation(new TestNexusServiceImpl1())); + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl1 { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync((ctx, details, now) -> "Hello " + now); + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl2 { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync((ctx, details, now) -> "Hello " + now); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanWorkerShutdownTest.java b/temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanActivityWorkerShutdownTest.java similarity index 99% rename from temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanWorkerShutdownTest.java rename to temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanActivityWorkerShutdownTest.java index 39bf9b1d6..40090e966 100644 --- a/temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanWorkerShutdownTest.java +++ b/temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanActivityWorkerShutdownTest.java @@ -42,7 +42,7 @@ import org.junit.Rule; import org.junit.Test; -public class CleanWorkerShutdownTest { +public class CleanActivityWorkerShutdownTest { private static final String COMPLETED = "Completed"; private static final String INTERRUPTED = "Interrupted"; diff --git a/temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanNexusWorkerShutdownTest.java b/temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanNexusWorkerShutdownTest.java new file mode 100644 index 000000000..e9e8e260a --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/worker/shutdown/CleanNexusWorkerShutdownTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.worker.shutdown; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.api.enums.v1.EventType; +import io.temporal.api.history.v1.HistoryEvent; +import io.temporal.client.WorkflowClient; +import io.temporal.common.converter.DefaultDataConverter; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.worker.WorkerOptions; +import io.temporal.workflow.NexusOperationOptions; +import io.temporal.workflow.NexusServiceOptions; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows.TestWorkflow1; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; + +public class CleanNexusWorkerShutdownTest { + + private static final String COMPLETED = "Completed"; + private static final String INTERRUPTED = "Interrupted"; + private static final CountDownLatch shutdownLatch = new CountDownLatch(1); + private static final CountDownLatch shutdownNowLatch = new CountDownLatch(1); + private static final TestNexusServiceImpl nexusServiceImpl = + new TestNexusServiceImpl(shutdownLatch, shutdownNowLatch); + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestWorkflowImpl.class) + .setNexusServiceImplementation(nexusServiceImpl) + .setWorkerOptions(WorkerOptions.newBuilder().setLocalActivityWorkerOnly(true).build()) + .build(); + + @Test + public void testShutdown() throws InterruptedException { + TestWorkflow1 workflow = testWorkflowRule.newWorkflowStub(TestWorkflow1.class); + WorkflowExecution execution = WorkflowClient.start(workflow::execute, null); + shutdownLatch.await(); + testWorkflowRule.getTestEnvironment().shutdown(); + testWorkflowRule.getTestEnvironment().awaitTermination(10, TimeUnit.MINUTES); + List events = + testWorkflowRule + .getExecutionHistory(execution.getWorkflowId()) + .getHistory() + .getEventsList(); + boolean found = false; + for (HistoryEvent e : events) { + if (e.getEventType() == EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED) { + found = true; + Payload ar = e.getNexusOperationCompletedEventAttributes().getResult(); + String r = + DefaultDataConverter.STANDARD_INSTANCE.fromPayload(ar, String.class, String.class); + assertEquals(COMPLETED, r); + } + } + assertTrue("Contains NexusOperationCompleted", found); + } + + @Test + public void testShutdownNow() throws InterruptedException { + TestWorkflow1 workflow = testWorkflowRule.newWorkflowStub(TestWorkflow1.class); + WorkflowExecution execution = WorkflowClient.start(workflow::execute, "now"); + shutdownNowLatch.await(); + testWorkflowRule.getTestEnvironment().shutdownNow(); + testWorkflowRule.getTestEnvironment().awaitTermination(10, TimeUnit.MINUTES); + List events = + testWorkflowRule + .getExecutionHistory(execution.getWorkflowId()) + .getHistory() + .getEventsList(); + boolean found = false; + for (HistoryEvent e : events) { + if (e.getEventType() == EventType.EVENT_TYPE_NEXUS_OPERATION_COMPLETED) { + found = true; + Payload ar = e.getNexusOperationCompletedEventAttributes().getResult(); + String r = + DefaultDataConverter.STANDARD_INSTANCE.fromPayload(ar, String.class, String.class); + assertEquals(INTERRUPTED, r); + } + } + assertTrue("Contains NexusOperationCompleted", found); + } + + public static class TestWorkflowImpl implements TestWorkflow1 { + + private final TestNexusServices.TestNexusService1 service = + Workflow.newNexusServiceStub( + TestNexusServices.TestNexusService1.class, + NexusServiceOptions.newBuilder() + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()) + .build()); + + @Override + public String execute(String now) { + return service.operation(now); + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl { + private final CountDownLatch shutdownLatch; + private final CountDownLatch shutdownNowLatch; + + public TestNexusServiceImpl(CountDownLatch shutdownLatch, CountDownLatch shutdownNowLatch) { + this.shutdownLatch = shutdownLatch; + this.shutdownNowLatch = shutdownNowLatch; + } + + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync( + (ctx, details, now) -> { + if (now == null) { + shutdownLatch.countDown(); + } else { + shutdownNowLatch.countDown(); + } + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + // We ignore the interrupted exception here to let the operation complete and return + // the + // result. Otherwise, the result is not reported: + return INTERRUPTED; + } + return COMPLETED; + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/EagerWorkflowTaskDispatchTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/EagerWorkflowTaskDispatchTest.java index 5ad08635a..cb84e139f 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/EagerWorkflowTaskDispatchTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/EagerWorkflowTaskDispatchTest.java @@ -53,6 +53,8 @@ public class EagerWorkflowTaskDispatchTest { new CountingSlotSupplier<>(100); private final CountingSlotSupplier localActivitySlotSupplier = new CountingSlotSupplier<>(100); + private final CountingSlotSupplier nexusSlotSupplier = + new CountingSlotSupplier<>(100); @Rule public SDKTestWorkflowRule testWorkflowRule = @@ -102,7 +104,8 @@ private WorkerFactory setupWorkerFactory( new CompositeTuner( workflowTaskSlotSupplier, activityTaskSlotSupplier, - localActivitySlotSupplier)) + localActivitySlotSupplier, + nexusSlotSupplier)) .build()); if (registerWorkflows) { worker.registerWorkflowImplementationTypes(EagerWorkflowTaskWorkflowImpl.class); diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/ExecuteTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/ExecuteTest.java index 2981a7be3..53b370a2c 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/ExecuteTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/ExecuteTest.java @@ -41,8 +41,9 @@ public void testExecute() throws ExecutionException, InterruptedException { Assert.assertEquals("func", WorkflowClient.execute(stubF::func).get()); Test1ArgWorkflowFunc stubF1 = testWorkflowRule.newWorkflowStubTimeoutOptions(Test1ArgWorkflowFunc.class); - Assert.assertEquals(1, (int) WorkflowClient.execute(stubF1::func1, 1).get()); - Assert.assertEquals(1, stubF1.func1(1)); // Check that duplicated start just returns the result. + Assert.assertEquals("1", WorkflowClient.execute(stubF1::func1, "1").get()); + Assert.assertEquals( + "1", stubF1.func1("1")); // Check that duplicated start just returns the result. Test2ArgWorkflowFunc stubF2 = testWorkflowRule.newWorkflowStubTimeoutOptions(Test2ArgWorkflowFunc.class); Assert.assertEquals("12", WorkflowClient.execute(stubF2::func2, "1", 2).get()); diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/WorkflowIdReusePolicyTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/WorkflowIdReusePolicyTest.java index 9eb28ccbc..17b18d17c 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/WorkflowIdReusePolicyTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/WorkflowIdReusePolicyTest.java @@ -52,12 +52,12 @@ public void testWorkflowIdResuePolicy() { testWorkflowRule .getWorkflowClient() .newWorkflowStub(Test1ArgWorkflowFunc.class, workflowOptions); - Assert.assertEquals(1, stubF1_1.func1(1)); + Assert.assertEquals("1", stubF1_1.func1("1")); Test1ArgWorkflowFunc stubF1_2 = testWorkflowRule .getWorkflowClient() .newWorkflowStub(Test1ArgWorkflowFunc.class, workflowOptions); - Assert.assertEquals(1, stubF1_2.func1(2)); + Assert.assertEquals("1", stubF1_2.func1("2")); // Setting WorkflowIdReusePolicy to AllowDuplicate will trigger new run. workflowOptions = @@ -70,7 +70,7 @@ public void testWorkflowIdResuePolicy() { testWorkflowRule .getWorkflowClient() .newWorkflowStub(Test1ArgWorkflowFunc.class, workflowOptions); - Assert.assertEquals(2, stubF1_3.func1(2)); + Assert.assertEquals("2", stubF1_3.func1("2")); // Setting WorkflowIdReusePolicy to RejectDuplicate or AllowDuplicateFailedOnly does not work as // expected. See https://github.com/uber/cadence-java-client/issues/295. diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/EagerActivityDispatchingTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/EagerActivityDispatchingTest.java index a64e381a7..3f2d1779e 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/EagerActivityDispatchingTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/activityTests/EagerActivityDispatchingTest.java @@ -37,10 +37,7 @@ import io.temporal.worker.Worker; import io.temporal.worker.WorkerFactory; import io.temporal.worker.WorkerOptions; -import io.temporal.worker.tuning.ActivitySlotInfo; -import io.temporal.worker.tuning.CompositeTuner; -import io.temporal.worker.tuning.LocalActivitySlotInfo; -import io.temporal.worker.tuning.WorkflowSlotInfo; +import io.temporal.worker.tuning.*; import io.temporal.workflow.*; import io.temporal.workflow.shared.TestActivities; import io.temporal.workflow.shared.TestActivities.TestActivitiesImpl; @@ -61,6 +58,7 @@ public class EagerActivityDispatchingTest { CountingSlotSupplier activityTaskSlotSupplier = new CountingSlotSupplier<>(100); CountingSlotSupplier localActivitySlotSupplier = new CountingSlotSupplier<>(100); + CountingSlotSupplier nexusSlotSupplier = new CountingSlotSupplier<>(100); @Before public void setUp() throws Exception { @@ -98,7 +96,10 @@ private void setupWorker( workerOptions.setWorkerTuner( new CompositeTuner( - workflowTaskSlotSupplier, activityTaskSlotSupplier, localActivitySlotSupplier)); + workflowTaskSlotSupplier, + activityTaskSlotSupplier, + localActivitySlotSupplier, + nexusSlotSupplier)); Worker worker = workerFactory.newWorker(TASK_QUEUE, workerOptions.build()); worker.registerActivitiesImplementations(activitiesImpl); if (registerWorkflows) diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/childWorkflowTests/ChildAsyncWorkflowTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/childWorkflowTests/ChildAsyncWorkflowTest.java index d6c06fb5e..4df10bb30 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/childWorkflowTests/ChildAsyncWorkflowTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/childWorkflowTests/ChildAsyncWorkflowTest.java @@ -57,7 +57,7 @@ public String execute(String taskQueue) { Assert.assertEquals("func", Async.function(stubF::func).get()); Test1ArgWorkflowFunc stubF1 = Workflow.newChildWorkflowStub(Test1ArgWorkflowFunc.class, workflowOptions); - assertEquals(1, (int) Async.function(stubF1::func1, 1).get()); + assertEquals("1", Async.function(stubF1::func1, "1").get()); Test2ArgWorkflowFunc stubF2 = Workflow.newChildWorkflowStub(Test2ArgWorkflowFunc.class, workflowOptions); assertEquals("12", Async.function(stubF2::func2, "1", 2).get()); diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/AsyncWorkflowOperationTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/AsyncWorkflowOperationTest.java new file mode 100644 index 000000000..9697e24b6 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/AsyncWorkflowOperationTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowOptions; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.testing.WorkflowReplayer; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class AsyncWorkflowOperationTest extends BaseNexusTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class, TestOperationWorkflow.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Override + protected SDKTestWorkflowRule getTestWorkflowRule() { + return testWorkflowRule; + } + + @Test + public void testWorkflowOperation() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + Assert.assertEquals("Hello from operation workflow " + testWorkflowRule.getTaskQueue(), result); + } + + @Test + public void testWorkflowOperationReplay() throws Exception { + WorkflowReplayer.replayWorkflowExecutionFromResource( + "testAsyncWorkflowOperationTestHistory.json", AsyncWorkflowOperationTest.TestNexus.class); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder() + .setEndpoint(getEndpointName()) + .setOperationOptions(options) + .build(); + // Try to call an asynchronous operation in a blocking way + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + // Try to call an asynchronous operation in a blocking way + String asyncResult = serviceStub.operation(input); + // Try to call an asynchronous operation in a non-blocking way + Promise asyncPromise = Async.function(serviceStub::operation, input); + Assert.assertEquals(asyncPromise.get(), asyncResult); + // Try to call an asynchronous operation in a non-blocking way using a handle + NexusOperationHandle asyncOpHandle = + Workflow.startNexusOperation(serviceStub::operation, "block"); + NexusOperationExecution asyncExec = asyncOpHandle.getExecution().get(); + // Execution id is present for an asynchronous operations + Assert.assertTrue("Operation id should be present", asyncExec.getOperationId().isPresent()); + // Result should only be completed if the operation is completed + Assert.assertFalse("Result should not be completed", asyncOpHandle.getResult().isCompleted()); + // Unblock the operation + Workflow.newExternalWorkflowStub(OperationWorkflow.class, asyncExec.getOperationId().get()) + .unblock(); + // Wait for the operation to complete + Assert.assertEquals("Hello from operation workflow block", asyncOpHandle.getResult().get()); + return asyncResult; + } + } + + @WorkflowInterface + public interface OperationWorkflow { + @WorkflowMethod + String execute(String arg); + + @SignalMethod + void unblock(); + } + + public static class TestOperationWorkflow implements OperationWorkflow { + boolean unblocked = false; + + @Override + public String execute(String arg) { + if (arg.equals("block")) { + Workflow.await(() -> unblocked); + } + return "Hello from operation workflow " + arg; + } + + @Override + public void unblock() { + unblocked = true; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return WorkflowClientOperationHandlers.fromWorkflowMethod( + (context, details, client, input) -> + client.newWorkflowStub( + OperationWorkflow.class, + WorkflowOptions.newBuilder().setWorkflowId(details.getRequestId()).build()) + ::execute); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/BaseNexusTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/BaseNexusTest.java new file mode 100644 index 000000000..7ebf7bccb --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/BaseNexusTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import com.google.protobuf.ByteString; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.nexus.v1.Endpoint; +import io.temporal.api.nexus.v1.EndpointSpec; +import io.temporal.api.nexus.v1.EndpointTarget; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointRequest; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointResponse; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.Workflow; +import org.junit.After; +import org.junit.Before; + +public abstract class BaseNexusTest { + + protected abstract SDKTestWorkflowRule getTestWorkflowRule(); + + Endpoint endpoint; + + public static String getEndpointName() { + return "test-endpoint-" + Workflow.getInfo().getTaskQueue(); + } + + @Before + public void setUp() { + endpoint = + createTestEndpoint( + getTestEndpointSpecBuilder("test-endpoint-" + getTestWorkflowRule().getTaskQueue())); + } + + @After + public void tearDown() { + getTestWorkflowRule() + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .deleteNexusEndpoint( + io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest.newBuilder() + .setId(endpoint.getId()) + .setVersion(endpoint.getVersion()) + .build()); + } + + private EndpointSpec.Builder getTestEndpointSpecBuilder(String name) { + return EndpointSpec.newBuilder() + .setName(name) + .setDescription(Payload.newBuilder().setData(ByteString.copyFromUtf8("test endpoint"))) + .setTarget( + EndpointTarget.newBuilder() + .setWorker( + EndpointTarget.Worker.newBuilder() + .setNamespace(getTestWorkflowRule().getTestEnvironment().getNamespace()) + .setTaskQueue(getTestWorkflowRule().getTaskQueue()))); + } + + private Endpoint createTestEndpoint(EndpointSpec.Builder spec) { + CreateNexusEndpointResponse resp = + getTestWorkflowRule() + .getTestEnvironment() + .getOperatorServiceStubs() + .blockingStub() + .createNexusEndpoint(CreateNexusEndpointRequest.newBuilder().setSpec(spec).build()); + return resp.getEndpoint(); + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/CancelAsyncOperationTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/CancelAsyncOperationTest.java new file mode 100644 index 000000000..eb0bd1e42 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/CancelAsyncOperationTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowFailedException; +import io.temporal.client.WorkflowOptions; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.NexusOperationFailure; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class CancelAsyncOperationTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class, AsyncWorkflowOperationTest.TestOperationWorkflow.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void asyncOperationImmediatelyCancelled() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows( + WorkflowFailedException.class, () -> workflowStub.execute("immediately")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof CanceledFailure); + CanceledFailure canceledFailure = (CanceledFailure) nexusFailure.getCause(); + Assert.assertEquals( + "operation canceled before it was started", canceledFailure.getOriginalMessage()); + } + + @Test + public void asyncOperationCancelled() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows(WorkflowFailedException.class, () -> workflowStub.execute("")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof CanceledFailure); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + Workflow.newCancellationScope( + () -> { + NexusOperationHandle handle = + Workflow.startNexusOperation(serviceStub::operation, "block"); + if (input.isEmpty()) { + handle.getExecution().get(); + } + CancellationScope.current().cancel(); + handle.getResult().get(); + }) + .run(); + return "Should not get here"; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return WorkflowClientOperationHandlers.fromWorkflowMethod( + (context, details, client, input) -> + client.newWorkflowStub( + AsyncWorkflowOperationTest.OperationWorkflow.class, + WorkflowOptions.newBuilder().setWorkflowId(details.getRequestId()).build()) + ::execute); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java new file mode 100644 index 000000000..763ae43c9 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.client.WorkflowExecutionAlreadyStarted; +import io.temporal.client.WorkflowFailedException; +import io.temporal.failure.ApplicationFailure; +import io.temporal.failure.NexusOperationFailure; +import io.temporal.failure.TimeoutFailure; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows.TestWorkflow1; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class OperationFailureConversionTest { + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void nexusOperationApplicationFailureNonRetryableFailureConversion() { + TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows( + WorkflowFailedException.class, + () -> workflowStub.execute("ApplicationFailureNonRetryable")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + } + + @Test + public void nexusOperationWorkflowExecutionAlreadyStartedFailureConversion() { + TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows( + WorkflowFailedException.class, + () -> workflowStub.execute("WorkflowExecutionAlreadyStarted")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + } + + @Test + public void nexusOperationApplicationFailureFailureConversion() { + TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows( + WorkflowFailedException.class, () -> workflowStub.execute("ApplicationFailure")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof TimeoutFailure); + } + + public static class TestNexus implements TestWorkflow1 { + @Override + public String execute(String testcase) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(5)) + .build(); + + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + TestNexusServices.TestNexusService1 testNexusService = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + testNexusService.operation(testcase); + return "fail"; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync( + (ctx, details, name) -> { + if (name.equals("ApplicationFailure")) { + throw ApplicationFailure.newFailure("failed to call operation", "TestFailure"); + } else if (name.equals("ApplicationFailureNonRetryable")) { + throw ApplicationFailure.newNonRetryableFailure( + "failed to call operation", "TestFailure"); + } else if (name.equals("WorkflowExecutionAlreadyStarted")) { + throw new WorkflowExecutionAlreadyStarted( + WorkflowExecution.newBuilder().setWorkflowId("id").setRunId("runId").build(), + "TestWorkflow", + new RuntimeException("already started")); + } + Assert.fail(); + return "fail"; + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/ParallelWorkflowOperationTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/ParallelWorkflowOperationTest.java new file mode 100644 index 000000000..154e500de --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/ParallelWorkflowOperationTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowOptions; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.testing.WorkflowReplayer; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class ParallelWorkflowOperationTest extends BaseNexusTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class, TestOperationWorkflow.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Override + protected SDKTestWorkflowRule getTestWorkflowRule() { + return testWorkflowRule; + } + + @Test + public void testParallelOperations() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + Assert.assertEquals("0123456789", result); + } + + @Test + public void testParallelOperationsReplay() throws Exception { + WorkflowReplayer.replayWorkflowExecutionFromResource( + "testParallelWorkflowOperationTestHistory.json", TestNexus.class); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder() + .setEndpoint(getEndpointName()) + .setOperationOptions(options) + .build(); + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + List> asyncResult = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + NexusOperationHandle h = + Workflow.startNexusOperation(serviceStub::operation, String.valueOf(i)); + asyncResult.add(h.getResult()); + } + StringBuilder result = new StringBuilder(); + for (Promise promise : asyncResult) { + result.append(promise.get()); + } + return result.toString(); + } + } + + @WorkflowInterface + public interface OperationWorkflow { + @WorkflowMethod + String execute(String arg); + } + + public static class TestOperationWorkflow implements OperationWorkflow { + @Override + public String execute(String arg) { + Workflow.sleep(Workflow.newRandom().ints(1, 1000).findFirst().getAsInt()); + return arg; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return WorkflowClientOperationHandlers.fromWorkflowMethod( + (context, details, client, input) -> + client.newWorkflowStub( + OperationWorkflow.class, + WorkflowOptions.newBuilder().setWorkflowId(details.getRequestId()).build()) + ::execute); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncClientOperationTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncClientOperationTest.java new file mode 100644 index 000000000..875f4ac83 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncClientOperationTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import static io.temporal.testing.internal.SDKTestWorkflowRule.NAMESPACE; + +import com.google.common.collect.ImmutableMap; +import com.uber.m3.tally.RootScopeBuilder; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowFailedException; +import io.temporal.common.reporter.TestStatsReporter; +import io.temporal.failure.ApplicationFailure; +import io.temporal.nexus.Nexus; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.serviceclient.MetricsTag; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.worker.MetricsType; +import io.temporal.worker.WorkerMetricsTag; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import java.time.Duration; +import java.util.Map; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class SyncClientOperationTest { + private final TestStatsReporter reporter = new TestStatsReporter(); + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class) + .setMetricsScope( + new RootScopeBuilder() + .reporter(reporter) + .reportEvery(com.uber.m3.util.Duration.ofMillis(10))) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void syncClientOperationSuccess() { + TestUpdatedWorkflow workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestUpdatedWorkflow.class); + Assert.assertTrue(workflowStub.execute(false).startsWith("Update ID:")); + + // Test metrics all tasks should have + Map nexusWorkerTags = + ImmutableMap.builder() + .putAll(MetricsTag.defaultTags(NAMESPACE)) + .put(MetricsTag.WORKER_TYPE, WorkerMetricsTag.WorkerType.NEXUS_WORKER.getValue()) + .put(MetricsTag.TASK_QUEUE, testWorkflowRule.getTaskQueue()) + .buildKeepingLast(); + reporter.assertTimer(MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY, nexusWorkerTags); + Map operationTags = + ImmutableMap.builder() + .putAll(nexusWorkerTags) + .put(MetricsTag.NEXUS_SERVICE, "TestNexusService1") + .put(MetricsTag.NEXUS_OPERATION, "operation") + .buildKeepingLast(); + reporter.assertTimer(MetricsType.NEXUS_EXEC_LATENCY, operationTags); + reporter.assertTimer(MetricsType.NEXUS_TASK_E2E_LATENCY, operationTags); + // Test our custom metric + reporter.assertCounter("operation", operationTags, 1); + } + + @Test + public void syncClientOperationFail() { + TestUpdatedWorkflow workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestUpdatedWorkflow.class); + Assert.assertThrows(WorkflowFailedException.class, () -> workflowStub.execute(true)); + + // Test metrics all failed tasks should have + Map nexusWorkerTags = + ImmutableMap.builder() + .putAll(MetricsTag.defaultTags(NAMESPACE)) + .put(MetricsTag.WORKER_TYPE, WorkerMetricsTag.WorkerType.NEXUS_WORKER.getValue()) + .put(MetricsTag.TASK_QUEUE, testWorkflowRule.getTaskQueue()) + .buildKeepingLast(); + reporter.assertTimer(MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY, nexusWorkerTags); + Map operationTags = + ImmutableMap.builder() + .putAll(nexusWorkerTags) + .put(MetricsTag.NEXUS_SERVICE, "TestNexusService1") + .put(MetricsTag.NEXUS_OPERATION, "operation") + .buildKeepingLast(); + reporter.assertTimer(MetricsType.NEXUS_EXEC_LATENCY, operationTags); + reporter.assertTimer(MetricsType.NEXUS_TASK_E2E_LATENCY, operationTags); + reporter.assertCounter(MetricsType.NEXUS_EXEC_FAILED_COUNTER, operationTags, 1); + } + + @WorkflowInterface + public interface TestUpdatedWorkflow { + + @WorkflowMethod + String execute(boolean fail); + + @UpdateMethod + String update(String arg); + } + + public static class TestNexus implements TestUpdatedWorkflow { + @Override + public String execute(boolean fail) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(1)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + // Try to call a synchronous operation in a blocking way + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + return serviceStub.operation(fail ? "" : Workflow.getInfo().getWorkflowId()); + } + + @Override + public String update(String arg) { + return "Update ID: " + Workflow.getCurrentUpdateInfo().get().getUpdateId(); + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return WorkflowClientOperationHandlers.sync( + (ctx, details, client, id) -> { + if (id.isEmpty()) { + throw ApplicationFailure.newNonRetryableFailure("Invalid ID", "TestError"); + } + Nexus.getOperationContext().getMetricsScope().counter("operation").inc(1); + return client + .newWorkflowStub(TestUpdatedWorkflow.class, id) + .update("Update from operation"); + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java new file mode 100644 index 000000000..fde1572a7 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowFailedException; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.NexusOperationFailure; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class SyncOperationCancelledTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void syncOperationImmediatelyCancelled() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows( + WorkflowFailedException.class, () -> workflowStub.execute("immediately")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof CanceledFailure); + CanceledFailure canceledFailure = (CanceledFailure) nexusFailure.getCause(); + Assert.assertEquals( + "operation canceled before it was started", canceledFailure.getOriginalMessage()); + } + + @Test + public void syncOperationCancelled() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows(WorkflowFailedException.class, () -> workflowStub.execute("")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof CanceledFailure); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + Workflow.newCancellationScope( + () -> { + Promise promise = Async.function(serviceStub::operation, "to be cancelled"); + if (input.isEmpty()) { + Workflow.sleep(Duration.ofSeconds(1)); + } + CancellationScope.current().cancel(); + promise.get(); + }) + .run(); + return "Should not get here"; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync( + (ctx, details, name) -> { + throw new RuntimeException("failed to call operation"); + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java new file mode 100644 index 000000000..b6ec13642 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.OperationUnsuccessfulException; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowFailedException; +import io.temporal.failure.ApplicationFailure; +import io.temporal.failure.NexusOperationFailure; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows.TestWorkflow1; +import java.time.Duration; +import org.junit.*; + +public class SyncOperationFailTest { + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void failSyncOperation() { + TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows(WorkflowFailedException.class, () -> workflowStub.execute("")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + ApplicationFailure applicationFailure = (ApplicationFailure) nexusFailure.getCause(); + Assert.assertEquals("failed to call operation", applicationFailure.getOriginalMessage()); + } + + public static class TestNexus implements TestWorkflow1 { + @Override + public String execute(String endpoint) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(5)) + .build(); + + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + TestNexusServices.TestNexusService1 testNexusService = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + try { + testNexusService.operation(Workflow.getInfo().getWorkflowId()); + } catch (NexusOperationFailure nexusFailure) { + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + ApplicationFailure applicationFailure = (ApplicationFailure) nexusFailure.getCause(); + Assert.assertEquals("failed to call operation", applicationFailure.getOriginalMessage()); + } + + Promise failPromise = + Async.function(testNexusService::operation, Workflow.getInfo().getWorkflowId()); + try { + // Wait for the promise to fail + failPromise.get(); + } catch (NexusOperationFailure nexusFailure) { + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + ApplicationFailure applicationFailure = (ApplicationFailure) nexusFailure.getCause(); + Assert.assertEquals("failed to call operation", applicationFailure.getOriginalMessage()); + } + + NexusOperationHandle handle = + Workflow.startNexusOperation( + testNexusService::operation, Workflow.getInfo().getWorkflowId()); + try { + // Wait for the operation to fail + handle.getExecution().get(); + } catch (NexusOperationFailure nexusFailure) { + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + ApplicationFailure applicationFailure = (ApplicationFailure) nexusFailure.getCause(); + Assert.assertEquals("failed to call operation", applicationFailure.getOriginalMessage()); + } + try { + // Since the operation has failed, the result should throw the same exception as well + handle.getResult().get(); + } catch (NexusOperationFailure nexusFailure) { + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + ApplicationFailure applicationFailure = (ApplicationFailure) nexusFailure.getCause(); + Assert.assertEquals("failed to call operation", applicationFailure.getOriginalMessage()); + } + // Throw an exception to fail the workflow and test that the exception is propagated correctly + testNexusService.operation(Workflow.getInfo().getWorkflowId()); + // Workflow will not reach this point + return "fail"; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync( + (ctx, details, name) -> { + throw new OperationUnsuccessfulException("failed to call operation"); + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationStubTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationStubTest.java new file mode 100644 index 000000000..cce9ecc5a --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationStubTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class SyncOperationStubTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void typedNexusServiceStub() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + Assert.assertEquals("Hello, " + testWorkflowRule.getTaskQueue() + "!", result); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(5)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + // Try to call a synchronous operation in a blocking way + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + // Try to call a synchronous operation in a blocking way + String syncResult = serviceStub.operation(input); + // Try to call a synchronous operation in a non-blocking way + Promise syncPromise = Async.function(serviceStub::operation, input); + Assert.assertEquals(syncPromise.get(), syncResult); + // Try to call a synchronous operation in a non-blocking way using a handle + NexusOperationHandle syncOpHandle = + Workflow.startNexusOperation(serviceStub::operation, input); + NexusOperationExecution syncExec = syncOpHandle.getExecution().get(); + // Execution id is not present for synchronous operations + Assert.assertFalse( + "Operation id should not be present", syncExec.getOperationId().isPresent()); + // Result should always be completed for a synchronous operations when the Execution + // is resolved + Assert.assertTrue("Result should be completed", syncOpHandle.getResult().isCompleted()); + return syncResult; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync((ctx, details, name) -> "Hello, " + name + "!"); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationTimeoutTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationTimeoutTest.java new file mode 100644 index 000000000..e79731cba --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationTimeoutTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowFailedException; +import io.temporal.failure.NexusOperationFailure; +import io.temporal.failure.TimeoutFailure; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class SyncOperationTimeoutTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void typedOperationTimeout() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows(WorkflowFailedException.class, () -> workflowStub.execute("")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof TimeoutFailure); + TimeoutFailure timeoutFailure = (TimeoutFailure) nexusFailure.getCause(); + Assert.assertEquals("operation timed out", timeoutFailure.getOriginalMessage()); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(1)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + // Try to call a synchronous operation in a blocking way + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + return serviceStub.operation("test timeout"); + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync( + (ctx, details, name) -> { + throw new RuntimeException("failed to call operation"); + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/TerminateWorkflowAsyncOperationTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/TerminateWorkflowAsyncOperationTest.java new file mode 100644 index 000000000..3e3d164f0 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/TerminateWorkflowAsyncOperationTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.Operation; +import io.nexusrpc.Service; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowFailedException; +import io.temporal.client.WorkflowOptions; +import io.temporal.failure.ApplicationFailure; +import io.temporal.failure.NexusOperationFailure; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class TerminateWorkflowAsyncOperationTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class, AsyncWorkflowOperationTest.TestOperationWorkflow.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void terminateAsyncOperation() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + WorkflowFailedException exception = + Assert.assertThrows(WorkflowFailedException.class, () -> workflowStub.execute("")); + Assert.assertTrue(exception.getCause() instanceof NexusOperationFailure); + NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause(); + Assert.assertTrue(nexusFailure.getCause() instanceof ApplicationFailure); + Assert.assertEquals( + "operation terminated", + ((ApplicationFailure) nexusFailure.getCause()).getOriginalMessage()); + } + + @Service + public interface TestNexusService { + @Operation + String operation(String input); + + @Operation + String terminate(String workflowId); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofHours(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + TestNexusService serviceStub = + Workflow.newNexusServiceStub(TestNexusService.class, serviceOptions); + // Start an async operation + NexusOperationHandle handle = + Workflow.startNexusOperation(serviceStub::operation, "block"); + // Wait for the operation to start + String workflowId = handle.getExecution().get().getOperationId().get(); + // Terminate the operation + serviceStub.terminate(workflowId); + // Try to get the result, expect this to throw + handle.getResult().get(); + return "Should not get here"; + } + } + + @ServiceImpl(service = TestNexusService.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return WorkflowClientOperationHandlers.fromWorkflowMethod( + (context, details, client, input) -> + client.newWorkflowStub( + AsyncWorkflowOperationTest.OperationWorkflow.class, + WorkflowOptions.newBuilder().setWorkflowId(details.getRequestId()).build()) + ::execute); + } + + @OperationImpl + public OperationHandler terminate() { + return WorkflowClientOperationHandlers.sync( + (context, details, client, workflowId) -> { + client.newUntypedWorkflowStub(workflowId).terminate("terminate for test"); + return "terminated"; + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/UntypedSyncOperationStubTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/UntypedSyncOperationStubTest.java new file mode 100644 index 000000000..27340fbec --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/UntypedSyncOperationStubTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +public class UntypedSyncOperationStubTest { + @ClassRule + public static SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Test + public void untypedNexusServiceStub() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + Assert.assertEquals("Hello, " + testWorkflowRule.getTaskQueue() + "!", result); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String name) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(5)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder() + .setEndpoint(testWorkflowRule.getNexusEndpoint().getSpec().getName()) + .setOperationOptions(options) + .build(); + NexusServiceStub serviceStub = + Workflow.newUntypedNexusServiceStub("TestNexusService1", serviceOptions); + String syncResult = serviceStub.execute("operation", String.class, name); + // Try to call a synchronous operation in a non-blocking way + Promise syncPromise = serviceStub.executeAsync("operation", String.class, name); + Assert.assertEquals(syncPromise.get(), syncResult); + // Try to call a synchronous operation in a non-blocking way using a handle + NexusOperationHandle syncOpHandle = + serviceStub.start("operation", String.class, name); + NexusOperationExecution syncOpExec = syncOpHandle.getExecution().get(); + // Execution id is not present for synchronous operations + if (syncOpExec.getOperationId().isPresent()) { + Assert.fail("Execution id is present"); + } + // Result should always be completed for a synchronous operations when the Execution promise + // is resolved + if (!syncOpHandle.getResult().isCompleted()) { + Assert.fail("Result is not completed"); + } + + return syncResult; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public static class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync((ctx, details, name) -> "Hello, " + name + "!"); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleFuncTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleFuncTest.java new file mode 100644 index 000000000..83ffd0498 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleFuncTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.Operation; +import io.nexusrpc.Service; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowOptions; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.nexus.WorkflowHandle; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestMultiArgWorkflowFunctions; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class WorkflowHandleFuncTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes( + TestNexus.class, TestMultiArgWorkflowFunctions.TestMultiArgWorkflowImpl.class) + .setNexusServiceImplementation(new TestNexusServiceFuncImpl()) + .build(); + + @Test + public void handleTests() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + Assert.assertEquals("funcinputinput2input23input234input2345input23456", result); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + + TestNexusServiceFunc serviceStub = + Workflow.newNexusServiceStub(TestNexusServiceFunc.class, serviceOptions); + StringBuilder result = new StringBuilder(); + for (int i = 0; i < 7; i++) { + result.append(serviceStub.operation(i)); + } + return result.toString(); + } + } + + @Service + public interface TestNexusServiceFunc { + @Operation + String operation(Integer input); + } + + @ServiceImpl(service = TestNexusServiceFunc.class) + public class TestNexusServiceFuncImpl { + @OperationImpl + public OperationHandler operation() { + return WorkflowClientOperationHandlers.fromWorkflowHandle( + (context, details, client, input) -> { + switch (input) { + case 0: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.TestNoArgsWorkflowFunc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::func); + case 1: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test1ArgWorkflowFunc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::func1, + "input"); + case 2: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test2ArgWorkflowFunc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::func2, + "input", + 2); + case 3: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test3ArgWorkflowFunc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::func3, + "input", + 2, + 3); + case 4: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test4ArgWorkflowFunc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::func4, + "input", + 2, + 3, + 4); + case 5: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test5ArgWorkflowFunc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::func5, + "input", + 2, + 3, + 4, + 5); + case 6: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test6ArgWorkflowFunc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::func6, + "input", + 2, + 3, + 4, + 5, + 6); + default: + return null; + } + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleProcTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleProcTest.java new file mode 100644 index 000000000..c951bf8d7 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleProcTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import io.nexusrpc.Operation; +import io.nexusrpc.Service; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.client.WorkflowOptions; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.nexus.WorkflowHandle; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.NexusOperationOptions; +import io.temporal.workflow.NexusServiceOptions; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.shared.TestMultiArgWorkflowFunctions; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class WorkflowHandleProcTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes( + TestNexus.class, TestMultiArgWorkflowFunctions.TestMultiArgWorkflowImpl.class) + .setNexusServiceImplementation(new TestNexusServiceFuncImpl()) + .build(); + + @Test + public void handleTests() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + Assert.assertEquals("success", result); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder().setOperationOptions(options).build(); + + TestNexusServiceProc serviceStub = + Workflow.newNexusServiceStub(TestNexusServiceProc.class, serviceOptions); + for (int i = 0; i < 7; i++) { + serviceStub.operation(i); + } + return "success"; + } + } + + @Service + public interface TestNexusServiceProc { + @Operation + Void operation(Integer input); + } + + @ServiceImpl(service = TestNexusServiceProc.class) + public class TestNexusServiceFuncImpl { + @OperationImpl + public OperationHandler operation() { + return WorkflowClientOperationHandlers.fromWorkflowHandle( + (context, details, client, input) -> { + switch (input) { + case 0: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.TestNoArgsWorkflowProc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::proc); + case 1: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test1ArgWorkflowProc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::proc1, + "input"); + case 2: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test2ArgWorkflowProc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::proc2, + "input", + 2); + case 3: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test3ArgWorkflowProc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::proc3, + "input", + 2, + 3); + case 4: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test4ArgWorkflowProc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::proc4, + "input", + 2, + 3, + 4); + case 5: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test5ArgWorkflowProc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::proc5, + "input", + 2, + 3, + 4, + 5); + case 6: + return WorkflowHandle.fromWorkflowMethod( + client.newWorkflowStub( + TestMultiArgWorkflowFunctions.Test6ArgWorkflowProc.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::proc6, + "input", + 2, + 3, + 4, + 5, + 6); + default: + return null; + } + }); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowOperationLinkingTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowOperationLinkingTest.java new file mode 100644 index 000000000..fa70ac7ee --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowOperationLinkingTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.nexus; + +import static io.temporal.internal.common.WorkflowExecutionUtils.getEventOfType; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.api.enums.v1.EventType; +import io.temporal.api.history.v1.History; +import io.temporal.api.history.v1.HistoryEvent; +import io.temporal.client.WorkflowOptions; +import io.temporal.client.WorkflowStub; +import io.temporal.nexus.WorkflowClientOperationHandlers; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.*; +import io.temporal.workflow.shared.TestNexusServices; +import io.temporal.workflow.shared.TestWorkflows; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class WorkflowOperationLinkingTest extends BaseNexusTest { + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestNexus.class, TestOperationWorkflow.class) + .setNexusServiceImplementation(new TestNexusServiceImpl()) + .build(); + + @Override + protected SDKTestWorkflowRule getTestWorkflowRule() { + return testWorkflowRule; + } + + @Test + public void testWorkflowOperationLinks() { + TestWorkflows.TestWorkflow1 workflowStub = + testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflows.TestWorkflow1.class); + String result = workflowStub.execute(testWorkflowRule.getTaskQueue()); + Assert.assertEquals("Hello from operation workflow " + testWorkflowRule.getTaskQueue(), result); + String originalWorkflowId = WorkflowStub.fromTyped(workflowStub).getExecution().getWorkflowId(); + History history = + testWorkflowRule.getWorkflowClient().fetchHistory(originalWorkflowId).getHistory(); + // Assert that the operation started event has a link to the workflow execution started event + HistoryEvent nexusStartedEvent = + getEventOfType(history, EventType.EVENT_TYPE_NEXUS_OPERATION_STARTED); + Assert.assertEquals(1, nexusStartedEvent.getLinksCount()); + Assert.assertEquals( + EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED, + nexusStartedEvent.getLinks(0).getWorkflowEvent().getEventRef().getEventType()); + // Assert that the started workflow has a link to the original workflow + History linkedHistory = + testWorkflowRule + .getWorkflowClient() + .fetchHistory(nexusStartedEvent.getLinks(0).getWorkflowEvent().getWorkflowId()) + .getHistory(); + HistoryEvent linkedStartedEvent = linkedHistory.getEventsList().get(0); + Assert.assertEquals(1, linkedStartedEvent.getLinksCount()); + Assert.assertEquals( + originalWorkflowId, linkedStartedEvent.getLinks(0).getWorkflowEvent().getWorkflowId()); + } + + public static class TestNexus implements TestWorkflows.TestWorkflow1 { + @Override + public String execute(String input) { + NexusOperationOptions options = + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build(); + NexusServiceOptions serviceOptions = + NexusServiceOptions.newBuilder() + .setEndpoint(getEndpointName()) + .setOperationOptions(options) + .build(); + TestNexusServices.TestNexusService1 serviceStub = + Workflow.newNexusServiceStub(TestNexusServices.TestNexusService1.class, serviceOptions); + // Start an asynchronous operation backed by a workflow + NexusOperationHandle asyncOpHandle = + Workflow.startNexusOperation(serviceStub::operation, input); + NexusOperationExecution asyncExec = asyncOpHandle.getExecution().get(); + // Signal the operation to unblock, this makes sure the operation doesn't complete before the + // operation + // started event is written to history + Workflow.newExternalWorkflowStub(OperationWorkflow.class, asyncExec.getOperationId().get()) + .unblock(); + return asyncOpHandle.getResult().get(); + } + } + + @WorkflowInterface + public interface OperationWorkflow { + @WorkflowMethod + String execute(String arg); + + @SignalMethod + void unblock(); + } + + public static class TestOperationWorkflow implements OperationWorkflow { + boolean unblocked = false; + + @Override + public String execute(String arg) { + Workflow.await(() -> unblocked); + return "Hello from operation workflow " + arg; + } + + @Override + public void unblock() { + unblocked = true; + } + } + + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return WorkflowClientOperationHandlers.fromWorkflowMethod( + (context, details, client, input) -> + client.newWorkflowStub( + AsyncWorkflowOperationTest.OperationWorkflow.class, + WorkflowOptions.newBuilder().setWorkflowId(details.getRequestId()).build()) + ::execute); + } + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/shared/TestMultiArgWorkflowFunctions.java b/temporal-sdk/src/test/java/io/temporal/workflow/shared/TestMultiArgWorkflowFunctions.java index 76b0bba38..55c4c3387 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/shared/TestMultiArgWorkflowFunctions.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/shared/TestMultiArgWorkflowFunctions.java @@ -44,7 +44,7 @@ public interface TestNoArgsWorkflowFunc extends TestUpdateFunc { public interface Test1ArgWorkflowFunc extends TestUpdateFunc { @WorkflowMethod(name = "func1") - int func1(int input); + String func1(String input); } @WorkflowInterface @@ -162,7 +162,7 @@ public String func() { } @Override - public int func1(int a1) { + public String func1(String a1) { return a1; } diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/shared/TestNexusServices.java b/temporal-sdk/src/test/java/io/temporal/workflow/shared/TestNexusServices.java new file mode 100644 index 000000000..8aa568a0a --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/shared/TestNexusServices.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow.shared; + +import io.nexusrpc.Operation; +import io.nexusrpc.Service; + +/** Common set of Nexus Service interfaces for use in tests. */ +public class TestNexusServices { + @Service + public interface TestNexusService1 { + @Operation + String operation(String input); + } + + @Service + public interface TestNexusService2 { + @Operation + Integer operation(Integer input); + } + + @Service + public interface TestNexusServiceVoid { + @Operation + Void operation(); + } + + @Service + public interface TestNexusServiceVoidInput { + @Operation + String operation(); + } + + @Service + public interface TestNexusServiceVoidReturn { + @Operation + Void operation(String input); + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java index 866af8787..4ea62d832 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java @@ -175,7 +175,7 @@ public void startVariousFuncs() throws ExecutionException, InterruptedException TestMultiArgWorkflowFunctions.Test1ArgWorkflowFunc.class, createOptions()); UpdateWithStartWorkflowOperation updateOp1 = newUpdateOp.apply(stubF1::update, 1); WorkflowUpdateHandle handle1 = - WorkflowClient.updateWithStart(stubF1::func1, 1, updateOp1); + WorkflowClient.updateWithStart(stubF1::func1, "1", updateOp1); // 2 args TestMultiArgWorkflowFunctions.Test2ArgWorkflowFunc stubF2 = diff --git a/temporal-sdk/src/test/resources/testAsyncWorkflowOperationTestHistory.json b/temporal-sdk/src/test/resources/testAsyncWorkflowOperationTestHistory.json new file mode 100644 index 000000000..f860a9c5e --- /dev/null +++ b/temporal-sdk/src/test/resources/testAsyncWorkflowOperationTestHistory.json @@ -0,0 +1,555 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2024-09-21T22:04:51.255938Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1049652", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "TestWorkflow1" + }, + "taskQueue": { + "name": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IldvcmtmbG93VGVzdC10ZXN0V29ya2Zsb3dPcGVyYXRpb24tZmRiMjczYjYtZGUyNC00ODY3LWFiNDgtOTE2NmIxMzVkMjMwIg==" + } + ] + }, + "workflowExecutionTimeout": "0s", + "workflowRunTimeout": "200s", + "workflowTaskTimeout": "5s", + "originalExecutionRunId": "c55c636c-5344-4b8d-b22a-2a4c4eee8fa3", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "firstExecutionRunId": "c55c636c-5344-4b8d-b22a-2a4c4eee8fa3", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "header": {}, + "workflowId": "b3e872e4-74e6-4e76-a444-077b812e9a1b" + } + }, + { + "eventId": "2", + "eventTime": "2024-09-21T22:04:51.256089Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049653", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2024-09-21T22:04:51.259445Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049659", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "9bdeae08-5113-413d-8281-12b88b525c69", + "historySizeBytes": "508" + } + }, + { + "eventId": "4", + "eventTime": "2024-09-21T22:04:51.367145Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049663", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "sdkMetadata": { + "langUsedFlags": [ + 1 + ] + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2024-09-21T22:04:51.367233Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049664", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IldvcmtmbG93VGVzdC10ZXN0V29ya2Zsb3dPcGVyYXRpb24tZmRiMjczYjYtZGUyNC00ODY3LWFiNDgtOTE2NmIxMzVkMjMwIg==" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "73d3da51-b437-4202-a980-516c44bb7a84", + "endpointId": "941a6908-7da9-4ae9-902d-6a1821f0ed85" + } + }, + { + "eventId": "6", + "eventTime": "2024-09-21T22:04:51.394909Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049679", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "5", + "operationId": "53539d2b-c80d-4c6f-abf0-f8fd73c14410", + "requestId": "73d3da51-b437-4202-a980-516c44bb7a84" + } + }, + { + "eventId": "7", + "eventTime": "2024-09-21T22:04:51.394926Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049680", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "11805@Quinn-Klassens-MacBook-Pro.local:c294db9c-9a58-41e8-97bc-31888b633bab", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "8", + "eventTime": "2024-09-21T22:04:51.395768Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049684", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "7", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "e9686c4b-9502-444d-a991-b7578a55ed1a", + "historySizeBytes": "1318" + } + }, + { + "eventId": "9", + "eventTime": "2024-09-21T22:04:51.402442Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049695", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "7", + "startedEventId": "8", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "10", + "eventTime": "2024-09-21T22:04:51.399172Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049696", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "5", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIGZyb20gb3BlcmF0aW9uIHdvcmtmbG93IFdvcmtmbG93VGVzdC10ZXN0V29ya2Zsb3dPcGVyYXRpb24tZmRiMjczYjYtZGUyNC00ODY3LWFiNDgtOTE2NmIxMzVkMjMwIg==" + }, + "requestId": "73d3da51-b437-4202-a980-516c44bb7a84" + } + }, + { + "eventId": "11", + "eventTime": "2024-09-21T22:04:51.402476Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049697", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "11805@Quinn-Klassens-MacBook-Pro.local:c294db9c-9a58-41e8-97bc-31888b633bab", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "12", + "eventTime": "2024-09-21T22:04:51.404313Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049701", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "c4c512f3-3b89-422a-b114-0f98d520287d", + "historySizeBytes": "1891" + } + }, + { + "eventId": "13", + "eventTime": "2024-09-21T22:04:51.410790Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049705", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "14", + "eventTime": "2024-09-21T22:04:51.410809Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049706", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IldvcmtmbG93VGVzdC10ZXN0V29ya2Zsb3dPcGVyYXRpb24tZmRiMjczYjYtZGUyNC00ODY3LWFiNDgtOTE2NmIxMzVkMjMwIg==" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "13", + "requestId": "aedc84ce-5870-4a58-9aa6-dc166e56cc0d", + "endpointId": "941a6908-7da9-4ae9-902d-6a1821f0ed85" + } + }, + { + "eventId": "15", + "eventTime": "2024-09-21T22:04:51.416236Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049719", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "14", + "operationId": "92f0b987-c98f-4bd9-98b5-bd53ac85a92a", + "requestId": "aedc84ce-5870-4a58-9aa6-dc166e56cc0d" + } + }, + { + "eventId": "16", + "eventTime": "2024-09-21T22:04:51.416250Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049720", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "11805@Quinn-Klassens-MacBook-Pro.local:c294db9c-9a58-41e8-97bc-31888b633bab", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "17", + "eventTime": "2024-09-21T22:04:51.416855Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049724", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "16", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "cbe3345b-3a4b-4782-97e1-4ea9e2230375", + "historySizeBytes": "2697" + } + }, + { + "eventId": "18", + "eventTime": "2024-09-21T22:04:51.419110Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049735", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "16", + "startedEventId": "17", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "19", + "eventTime": "2024-09-21T22:04:51.419961Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049737", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "14", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIGZyb20gb3BlcmF0aW9uIHdvcmtmbG93IFdvcmtmbG93VGVzdC10ZXN0V29ya2Zsb3dPcGVyYXRpb24tZmRiMjczYjYtZGUyNC00ODY3LWFiNDgtOTE2NmIxMzVkMjMwIg==" + }, + "requestId": "aedc84ce-5870-4a58-9aa6-dc166e56cc0d" + } + }, + { + "eventId": "20", + "eventTime": "2024-09-21T22:04:51.419976Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049738", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "11805@Quinn-Klassens-MacBook-Pro.local:c294db9c-9a58-41e8-97bc-31888b633bab", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "21", + "eventTime": "2024-09-21T22:04:51.420653Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049742", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "20", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "1371ab54-9c3b-4d9d-9721-4638f46b5838", + "historySizeBytes": "3270" + } + }, + { + "eventId": "22", + "eventTime": "2024-09-21T22:04:51.423700Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049746", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "20", + "startedEventId": "21", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "23", + "eventTime": "2024-09-21T22:04:51.423718Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049747", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImJsb2NrIg==" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "22", + "requestId": "e89c12bc-cc30-4ba0-ab9f-70974b4f052f", + "endpointId": "941a6908-7da9-4ae9-902d-6a1821f0ed85" + } + }, + { + "eventId": "24", + "eventTime": "2024-09-21T22:04:51.427450Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049760", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "23", + "operationId": "aaaf6547-84de-4d79-bc0c-6b59a64ea061", + "requestId": "e89c12bc-cc30-4ba0-ab9f-70974b4f052f" + } + }, + { + "eventId": "25", + "eventTime": "2024-09-21T22:04:51.427463Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049761", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "11805@Quinn-Klassens-MacBook-Pro.local:c294db9c-9a58-41e8-97bc-31888b633bab", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "26", + "eventTime": "2024-09-21T22:04:51.428158Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049765", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "25", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "45fee988-4673-41cc-b677-4d05890fae42", + "historySizeBytes": "4010" + } + }, + { + "eventId": "27", + "eventTime": "2024-09-21T22:04:51.436705Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049772", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "25", + "startedEventId": "26", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "28", + "eventTime": "2024-09-21T22:04:51.436729Z", + "eventType": "EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1049773", + "signalExternalWorkflowExecutionInitiatedEventAttributes": { + "workflowTaskCompletedEventId": "27", + "namespaceId": "64b5c2fb-2aae-4b9c-a3e1-564c49fab4b8", + "workflowExecution": { + "workflowId": "aaaf6547-84de-4d79-bc0c-6b59a64ea061" + }, + "signalName": "unblock", + "header": {} + } + }, + { + "eventId": "29", + "eventTime": "2024-09-21T22:04:51.438172Z", + "eventType": "EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1049781", + "externalWorkflowExecutionSignaledEventAttributes": { + "initiatedEventId": "28", + "namespace": "UnitTest", + "namespaceId": "64b5c2fb-2aae-4b9c-a3e1-564c49fab4b8", + "workflowExecution": { + "workflowId": "aaaf6547-84de-4d79-bc0c-6b59a64ea061" + } + } + }, + { + "eventId": "30", + "eventTime": "2024-09-21T22:04:51.438174Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049782", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "11805@Quinn-Klassens-MacBook-Pro.local:c294db9c-9a58-41e8-97bc-31888b633bab", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "31", + "eventTime": "2024-09-21T22:04:51.438656Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049789", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "30", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "29b1a0d7-d39d-493c-bf05-256d3ed99412", + "historySizeBytes": "4617" + } + }, + { + "eventId": "32", + "eventTime": "2024-09-21T22:04:51.442440Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049794", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "30", + "startedEventId": "31", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "33", + "eventTime": "2024-09-21T22:04:51.443830Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049803", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "23", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIGZyb20gb3BlcmF0aW9uIHdvcmtmbG93IGJsb2NrIg==" + }, + "requestId": "e89c12bc-cc30-4ba0-ab9f-70974b4f052f" + } + }, + { + "eventId": "34", + "eventTime": "2024-09-21T22:04:51.443840Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049804", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "11805@Quinn-Klassens-MacBook-Pro.local:c294db9c-9a58-41e8-97bc-31888b633bab", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testWorkflowOperation-fdb273b6-de24-4867-ab48-9166b135d230" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "35", + "eventTime": "2024-09-21T22:04:51.444745Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049808", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "34", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "requestId": "f5fad756-9be6-4ff9-8a9f-1d3bee637720", + "historySizeBytes": "5122" + } + }, + { + "eventId": "36", + "eventTime": "2024-09-21T22:04:51.447003Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049812", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "34", + "startedEventId": "35", + "identity": "11805@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "37", + "eventTime": "2024-09-21T22:04:51.447018Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1049813", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIGZyb20gb3BlcmF0aW9uIHdvcmtmbG93IFdvcmtmbG93VGVzdC10ZXN0V29ya2Zsb3dPcGVyYXRpb24tZmRiMjczYjYtZGUyNC00ODY3LWFiNDgtOTE2NmIxMzVkMjMwIg==" + } + ] + }, + "workflowTaskCompletedEventId": "36" + } + } + ] +} \ No newline at end of file diff --git a/temporal-sdk/src/test/resources/testParallelWorkflowOperationTestHistory.json b/temporal-sdk/src/test/resources/testParallelWorkflowOperationTestHistory.json new file mode 100644 index 000000000..2131fd08b --- /dev/null +++ b/temporal-sdk/src/test/resources/testParallelWorkflowOperationTestHistory.json @@ -0,0 +1,742 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2024-09-21T21:58:59.291887Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1049233", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "TestWorkflow1" + }, + "taskQueue": { + "name": "WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IldvcmtmbG93VGVzdC10ZXN0UGFyYWxsZWxPcGVyYXRpb25zLTFlMjUxNTM4LTQ4MTQtNDUzNS1iOTk5LWU4NmQ4ZjE3YTY5MSI=" + } + ] + }, + "workflowExecutionTimeout": "0s", + "workflowRunTimeout": "200s", + "workflowTaskTimeout": "5s", + "originalExecutionRunId": "cf054c98-0160-450e-9f01-061d861b5a54", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "firstExecutionRunId": "cf054c98-0160-450e-9f01-061d861b5a54", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "header": {}, + "workflowId": "531437b0-31dd-4222-bf02-96d6cbd8885b" + } + }, + { + "eventId": "2", + "eventTime": "2024-09-21T21:58:59.291967Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049234", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2024-09-21T21:58:59.295147Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049240", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "requestId": "7404551c-c4ce-4b89-88aa-856cce2feb74", + "historySizeBytes": "512" + } + }, + { + "eventId": "4", + "eventTime": "2024-09-21T21:58:59.409523Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049244", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "sdkMetadata": { + "langUsedFlags": [ + 1 + ] + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2024-09-21T21:58:59.409555Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049245", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjAi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "96b76c12-c846-4cce-9290-74ee9b1d79ab", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "6", + "eventTime": "2024-09-21T21:58:59.409595Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049246", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjEi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "c4ff6b12-5e3e-4838-919d-377854b1ea73", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "7", + "eventTime": "2024-09-21T21:58:59.409608Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049247", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjIi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "9dd9cf59-393d-4ecb-8270-9aa0b72adc8c", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "8", + "eventTime": "2024-09-21T21:58:59.409617Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049248", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjMi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "db9fb6d0-e7de-460f-b518-2d30678e8011", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "9", + "eventTime": "2024-09-21T21:58:59.409627Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049249", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjQi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "6bd05424-845c-426c-9798-00af06ad27d9", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "10", + "eventTime": "2024-09-21T21:58:59.409636Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049250", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjUi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "c2a445d2-b548-456f-91c6-d7319b2733b5", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "11", + "eventTime": "2024-09-21T21:58:59.409646Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049251", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjYi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "4a0df3c4-e1e0-4877-b3bd-c5185d18027d", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "12", + "eventTime": "2024-09-21T21:58:59.409665Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049252", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ijci" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "6eb32f69-5c3a-4259-a386-d5d2845aca77", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "13", + "eventTime": "2024-09-21T21:58:59.409673Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049253", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ijgi" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "68692641-f77f-46a0-accd-4916de8bcc15", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "14", + "eventTime": "2024-09-21T21:58:59.409681Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED", + "taskId": "1049254", + "nexusOperationScheduledEventAttributes": { + "endpoint": "test-endpoint-WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691", + "service": "TestNexusService1", + "operation": "operation", + "input": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ijki" + }, + "scheduleToCloseTimeout": "10s", + "workflowTaskCompletedEventId": "4", + "requestId": "6d69714a-df8f-4c7a-a2e3-a4eed95355b2", + "endpointId": "6870540a-d056-4f6d-a372-3ad23fedf16e" + } + }, + { + "eventId": "15", + "eventTime": "2024-09-21T21:58:59.441823Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049335", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "7", + "operationId": "b99dd681-6005-4120-a9f5-77f69e59ca8a", + "requestId": "9dd9cf59-393d-4ecb-8270-9aa0b72adc8c" + } + }, + { + "eventId": "16", + "eventTime": "2024-09-21T21:58:59.441839Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049336", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "6114@Quinn-Klassens-MacBook-Pro.local:5bff66e2-9eaf-49f2-aa60-1fa7e7cf7848", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "17", + "eventTime": "2024-09-21T21:58:59.443506Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049341", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "6", + "operationId": "26fc52ac-66c6-46c2-ba32-8f524e03b80b", + "requestId": "c4ff6b12-5e3e-4838-919d-377854b1ea73" + } + }, + { + "eventId": "18", + "eventTime": "2024-09-21T21:58:59.443897Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049343", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "9", + "operationId": "d4b13a56-dc16-4bac-af5e-b505c61ba569", + "requestId": "6bd05424-845c-426c-9798-00af06ad27d9" + } + }, + { + "eventId": "19", + "eventTime": "2024-09-21T21:58:59.444123Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049345", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "14", + "operationId": "1f3aced5-a1ff-4a0d-a02e-065c1b54179c", + "requestId": "6d69714a-df8f-4c7a-a2e3-a4eed95355b2" + } + }, + { + "eventId": "20", + "eventTime": "2024-09-21T21:58:59.444333Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049347", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "8", + "operationId": "0b4f0b4e-54d3-4185-8a0a-485f27462bc5", + "requestId": "db9fb6d0-e7de-460f-b518-2d30678e8011" + } + }, + { + "eventId": "21", + "eventTime": "2024-09-21T21:58:59.444519Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049349", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "5", + "operationId": "0777cf0f-3ea0-4e94-8b4e-9580ea4e6ed2", + "requestId": "96b76c12-c846-4cce-9290-74ee9b1d79ab" + } + }, + { + "eventId": "22", + "eventTime": "2024-09-21T21:58:59.444697Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049351", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "11", + "operationId": "7b694b12-6488-4317-8bec-c893f7f1c07b", + "requestId": "4a0df3c4-e1e0-4877-b3bd-c5185d18027d" + } + }, + { + "eventId": "23", + "eventTime": "2024-09-21T21:58:59.444885Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049353", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "13", + "operationId": "3afdbf97-3338-450f-914a-14a60dece2ab", + "requestId": "68692641-f77f-46a0-accd-4916de8bcc15" + } + }, + { + "eventId": "24", + "eventTime": "2024-09-21T21:58:59.445060Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049355", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "12", + "operationId": "e276e526-7fc3-42c5-b12b-2c22ed98b301", + "requestId": "6eb32f69-5c3a-4259-a386-d5d2845aca77" + } + }, + { + "eventId": "25", + "eventTime": "2024-09-21T21:58:59.445416Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_STARTED", + "taskId": "1049357", + "nexusOperationStartedEventAttributes": { + "scheduledEventId": "10", + "operationId": "e0561cad-d066-46ae-918c-34c52aae8aaa", + "requestId": "c2a445d2-b548-456f-91c6-d7319b2733b5" + } + }, + { + "eventId": "26", + "eventTime": "2024-09-21T21:58:59.446001Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049359", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "16", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "requestId": "152c4c51-fa99-4241-9707-2cd4e174d276", + "historySizeBytes": "4537" + } + }, + { + "eventId": "27", + "eventTime": "2024-09-21T21:58:59.455430Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049411", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "16", + "startedEventId": "26", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "28", + "eventTime": "2024-09-21T21:59:00.458552Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049480", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "9", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjQi" + }, + "requestId": "6bd05424-845c-426c-9798-00af06ad27d9" + } + }, + { + "eventId": "29", + "eventTime": "2024-09-21T21:59:00.458567Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049481", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "6114@Quinn-Klassens-MacBook-Pro.local:5bff66e2-9eaf-49f2-aa60-1fa7e7cf7848", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "30", + "eventTime": "2024-09-21T21:59:00.461771Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049508", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "29", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "requestId": "f8078c43-d081-4139-8eb8-29e178ddcbbe", + "historySizeBytes": "5005" + } + }, + { + "eventId": "31", + "eventTime": "2024-09-21T21:59:00.467152Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049560", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "29", + "startedEventId": "30", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "32", + "eventTime": "2024-09-21T21:59:00.464054Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049561", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "14", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ijki" + }, + "requestId": "6d69714a-df8f-4c7a-a2e3-a4eed95355b2" + } + }, + { + "eventId": "33", + "eventTime": "2024-09-21T21:59:00.467168Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049562", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "6114@Quinn-Klassens-MacBook-Pro.local:5bff66e2-9eaf-49f2-aa60-1fa7e7cf7848", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "34", + "eventTime": "2024-09-21T21:59:00.469553Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049580", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "6", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjEi" + }, + "requestId": "c4ff6b12-5e3e-4838-919d-377854b1ea73" + } + }, + { + "eventId": "35", + "eventTime": "2024-09-21T21:59:00.472643Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049601", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "7", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjIi" + }, + "requestId": "9dd9cf59-393d-4ecb-8270-9aa0b72adc8c" + } + }, + { + "eventId": "36", + "eventTime": "2024-09-21T21:59:00.474830Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049615", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "5", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjAi" + }, + "requestId": "96b76c12-c846-4cce-9290-74ee9b1d79ab" + } + }, + { + "eventId": "37", + "eventTime": "2024-09-21T21:59:00.476132Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049624", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "33", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "requestId": "f3014e31-1105-4efc-b197-d28c6bd6c59c", + "historySizeBytes": "5767" + } + }, + { + "eventId": "38", + "eventTime": "2024-09-21T21:59:00.480427Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049632", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "33", + "startedEventId": "37", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "39", + "eventTime": "2024-09-21T21:59:00.478102Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049633", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "12", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ijci" + }, + "requestId": "6eb32f69-5c3a-4259-a386-d5d2845aca77" + } + }, + { + "eventId": "40", + "eventTime": "2024-09-21T21:59:00.478529Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049634", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "13", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Ijgi" + }, + "requestId": "68692641-f77f-46a0-accd-4916de8bcc15" + } + }, + { + "eventId": "41", + "eventTime": "2024-09-21T21:59:00.478862Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049635", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "11", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjYi" + }, + "requestId": "4a0df3c4-e1e0-4877-b3bd-c5185d18027d" + } + }, + { + "eventId": "42", + "eventTime": "2024-09-21T21:59:00.479198Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049636", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "8", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjMi" + }, + "requestId": "db9fb6d0-e7de-460f-b518-2d30678e8011" + } + }, + { + "eventId": "43", + "eventTime": "2024-09-21T21:59:00.479584Z", + "eventType": "EVENT_TYPE_NEXUS_OPERATION_COMPLETED", + "taskId": "1049637", + "nexusOperationCompletedEventAttributes": { + "scheduledEventId": "10", + "result": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjUi" + }, + "requestId": "c2a445d2-b548-456f-91c6-d7319b2733b5" + } + }, + { + "eventId": "44", + "eventTime": "2024-09-21T21:59:00.480440Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1049638", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "6114@Quinn-Klassens-MacBook-Pro.local:5bff66e2-9eaf-49f2-aa60-1fa7e7cf7848", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "WorkflowTest-testParallelOperations-1e251538-4814-4535-b999-e86d8f17a691" + }, + "startToCloseTimeout": "5s", + "attempt": 1 + } + }, + { + "eventId": "45", + "eventTime": "2024-09-21T21:59:00.497213Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1049642", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "44", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "requestId": "14fbd347-f7de-40e3-a532-5a5a88e6b78f", + "historySizeBytes": "6627" + } + }, + { + "eventId": "46", + "eventTime": "2024-09-21T21:59:00.500049Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1049646", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "44", + "startedEventId": "45", + "identity": "6114@Quinn-Klassens-MacBook-Pro.local", + "workerVersion": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "47", + "eventTime": "2024-09-21T21:59:00.500065Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1049647", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IjAxMjM0NTY3ODki" + } + ] + }, + "workflowTaskCompletedEventId": "46" + } + } + ] +} \ No newline at end of file diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/LongPollUtil.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/LongPollUtil.java index 79c99c1bf..3398681cb 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/LongPollUtil.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/LongPollUtil.java @@ -30,6 +30,7 @@ static boolean isLongPoll( MethodDescriptor method, CallOptions callOptions) { if (method == WorkflowServiceGrpc.getPollWorkflowTaskQueueMethod() || method == WorkflowServiceGrpc.getPollActivityTaskQueueMethod() + || method == WorkflowServiceGrpc.getPollNexusTaskQueueMethod() || method == WorkflowServiceGrpc.getUpdateWorkflowExecutionMethod() || method == WorkflowServiceGrpc.getExecuteMultiOperationMethod() || method == WorkflowServiceGrpc.getPollWorkflowExecutionUpdateMethod()) { diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/MetricsTag.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/MetricsTag.java index 5ab55800e..c1f675a3b 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/MetricsTag.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/MetricsTag.java @@ -35,6 +35,8 @@ public class MetricsTag { public static final String ACTIVITY_TYPE = "activity_type"; public static final String WORKFLOW_TYPE = "workflow_type"; + public static final String NEXUS_SERVICE = "nexus_service"; + public static final String NEXUS_OPERATION = "nexus_operation"; public static final String SIGNAL_NAME = "signal_name"; public static final String QUERY_TYPE = "query_type"; public static final String UPDATE_NAME = "update_name"; diff --git a/temporal-shaded/build.gradle b/temporal-shaded/build.gradle index 4b149d058..316c51225 100644 --- a/temporal-shaded/build.gradle +++ b/temporal-shaded/build.gradle @@ -45,6 +45,9 @@ dependencies { shadow "com.google.guava:failureaccess:1.0.1" shadow "com.google.guava:guava:$guavaVersion" + // nexus + shadow "io.nexusrpc:nexus-sdk:$nexusVersion" + // grpc shadow "io.grpc:grpc-protobuf-lite:$grpcVersion" shadow "io.grpc:grpc-protobuf:$grpcVersion" diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java new file mode 100644 index 000000000..b890681b1 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/NexusServiceImpl.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.spring.boot; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Enables the Nexus service bean to be discovered by the Workers auto-discovery. This annotation is + * not needed if only an explicit config is used. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface NexusServiceImpl { + /** + * @return names of Workers to register this nexus service bean with. Workers with these names + * must be present in the application config. Worker is named by its task queue if its name is + * not specified. + */ + String[] workers() default {}; + + /** + * @return Worker Task Queues to register this nexus service bean with. If Worker with the + * specified Task Queue is not present in the application config, it will be created with a + * default config. Can be specified as a property key, e.g.: ${propertyKey}. + */ + String[] taskQueues() default {}; +} diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/WorkerProperties.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/WorkerProperties.java index f387b6949..73fe94e71 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/WorkerProperties.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/properties/WorkerProperties.java @@ -30,6 +30,7 @@ public class WorkerProperties { private final @Nullable String name; private final @Nullable Collection> workflowClasses; private final @Nullable Collection activityBeans; + private final @Nullable Collection nexusServiceBeans; private final @Nullable CapacityConfigurationProperties capacity; private final @Nullable RateLimitsConfigurationProperties rateLimits; private final @Nullable BuildIdConfigurationProperties buildId; @@ -40,6 +41,7 @@ public WorkerProperties( @Nullable String name, @Nullable Collection> workflowClasses, @Nullable Collection activityBeans, + @Nullable Collection nexusServiceBeans, @Nullable CapacityConfigurationProperties capacity, @Nullable RateLimitsConfigurationProperties rateLimits, @Nullable BuildIdConfigurationProperties buildId) { @@ -47,6 +49,7 @@ public WorkerProperties( this.taskQueue = taskQueue; this.workflowClasses = workflowClasses; this.activityBeans = activityBeans; + this.nexusServiceBeans = nexusServiceBeans; this.capacity = capacity; this.rateLimits = rateLimits; this.buildId = buildId; @@ -87,12 +90,19 @@ public BuildIdConfigurationProperties getBuildId() { return buildId; } + @Nullable + public Collection getNexusServiceBeans() { + return nexusServiceBeans; + } + public static class CapacityConfigurationProperties { private final @Nullable Integer maxConcurrentWorkflowTaskExecutors; private final @Nullable Integer maxConcurrentActivityExecutors; private final @Nullable Integer maxConcurrentLocalActivityExecutors; + private final @Nullable Integer maxConcurrentNexusTaskExecutors; private final @Nullable Integer maxConcurrentWorkflowTaskPollers; private final @Nullable Integer maxConcurrentActivityTaskPollers; + private final @Nullable Integer maxConcurrentNexusTaskPollers; /** * @param maxConcurrentWorkflowTaskExecutors defines {@link @@ -101,23 +111,31 @@ public static class CapacityConfigurationProperties { * io.temporal.worker.WorkerOptions.Builder#setMaxConcurrentActivityExecutionSize(int)} * @param maxConcurrentLocalActivityExecutors defines {@link * io.temporal.worker.WorkerOptions.Builder#setMaxConcurrentLocalActivityExecutionSize(int)} + * @param maxConcurrentNexusTaskExecutors defines {@link + * io.temporal.worker.WorkerOptions.Builder#setMaxConcurrentNexusTaskPollers(int)} (int)} * @param maxConcurrentWorkflowTaskPollers defines {@link * io.temporal.worker.WorkerOptions.Builder#setMaxConcurrentWorkflowTaskPollers(int)} * @param maxConcurrentActivityTaskPollers defines {@link * io.temporal.worker.WorkerOptions.Builder#setMaxConcurrentActivityTaskPollers(int)} + * @param maxConcurrentNexusTaskPollers defines {@link + * io.temporal.worker.WorkerOptions.Builder#setMaxConcurrentNexusTaskPollers(int)} (int)} */ @ConstructorBinding public CapacityConfigurationProperties( @Nullable Integer maxConcurrentWorkflowTaskExecutors, @Nullable Integer maxConcurrentActivityExecutors, @Nullable Integer maxConcurrentLocalActivityExecutors, + @Nullable Integer maxConcurrentNexusTaskExecutors, @Nullable Integer maxConcurrentWorkflowTaskPollers, - @Nullable Integer maxConcurrentActivityTaskPollers) { + @Nullable Integer maxConcurrentActivityTaskPollers, + @Nullable Integer maxConcurrentNexusTaskPollers) { this.maxConcurrentWorkflowTaskExecutors = maxConcurrentWorkflowTaskExecutors; this.maxConcurrentActivityExecutors = maxConcurrentActivityExecutors; this.maxConcurrentLocalActivityExecutors = maxConcurrentLocalActivityExecutors; + this.maxConcurrentNexusTaskExecutors = maxConcurrentNexusTaskExecutors; this.maxConcurrentWorkflowTaskPollers = maxConcurrentWorkflowTaskPollers; this.maxConcurrentActivityTaskPollers = maxConcurrentActivityTaskPollers; + this.maxConcurrentNexusTaskPollers = maxConcurrentNexusTaskPollers; } @Nullable @@ -135,6 +153,11 @@ public Integer getMaxConcurrentLocalActivityExecutors() { return maxConcurrentLocalActivityExecutors; } + @Nullable + public Integer getMaxConcurrentNexusTasksExecutors() { + return maxConcurrentNexusTaskExecutors; + } + @Nullable public Integer getMaxConcurrentWorkflowTaskPollers() { return maxConcurrentWorkflowTaskPollers; @@ -144,6 +167,11 @@ public Integer getMaxConcurrentWorkflowTaskPollers() { public Integer getMaxConcurrentActivityTaskPollers() { return maxConcurrentActivityTaskPollers; } + + @Nullable + public Integer getMaxConcurrentNexusTaskPollers() { + return maxConcurrentNexusTaskPollers; + } } public static class RateLimitsConfigurationProperties { diff --git a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerOptionsTemplate.java b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerOptionsTemplate.java index 473de302e..56a37deb4 100644 --- a/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerOptionsTemplate.java +++ b/temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/template/WorkerOptionsTemplate.java @@ -58,10 +58,14 @@ WorkerOptions createWorkerOptions() { .ifPresent(options::setMaxConcurrentActivityExecutionSize); Optional.ofNullable(threadsConfiguration.getMaxConcurrentLocalActivityExecutors()) .ifPresent(options::setMaxConcurrentLocalActivityExecutionSize); + Optional.ofNullable(threadsConfiguration.getMaxConcurrentNexusTasksExecutors()) + .ifPresent(options::setMaxConcurrentNexusExecutionSize); Optional.ofNullable(threadsConfiguration.getMaxConcurrentWorkflowTaskPollers()) .ifPresent(options::setMaxConcurrentWorkflowTaskPollers); Optional.ofNullable(threadsConfiguration.getMaxConcurrentActivityTaskPollers()) .ifPresent(options::setMaxConcurrentActivityTaskPollers); + Optional.ofNullable(threadsConfiguration.getMaxConcurrentNexusTaskPollers()) + .ifPresent(options::setMaxConcurrentNexusTaskPollers); } WorkerProperties.RateLimitsConfigurationProperties rateLimitConfiguration = 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 753662298..210506bb8 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 @@ -21,6 +21,8 @@ package io.temporal.spring.boot.autoconfigure.template; import com.google.common.base.Preconditions; +import io.nexusrpc.ServiceDefinition; +import io.nexusrpc.handler.ServiceImplInstance; import io.opentracing.Tracer; import io.temporal.client.WorkflowClient; import io.temporal.common.Experimental; @@ -28,6 +30,7 @@ import io.temporal.common.metadata.POJOWorkflowImplMetadata; import io.temporal.common.metadata.POJOWorkflowMethodMetadata; import io.temporal.spring.boot.ActivityImpl; +import io.temporal.spring.boot.NexusServiceImpl; import io.temporal.spring.boot.TemporalOptionsCustomizer; import io.temporal.spring.boot.WorkflowImpl; import io.temporal.spring.boot.autoconfigure.properties.NamespaceProperties; @@ -151,13 +154,17 @@ private Collection createWorkers(WorkerFactory workerFactory) { Collection> autoDiscoveredWorkflowImplementationClasses = autoDiscoverWorkflowImplementations(); Map autoDiscoveredActivityBeans = autoDiscoverActivityBeans(); + Map autoDiscoveredNexusServiceBeans = autoDiscoverNexusServiceBeans(); configureWorkflowImplementationsByTaskQueue( workerFactory, workers, autoDiscoveredWorkflowImplementationClasses); configureActivityBeansByTaskQueue(workerFactory, workers, autoDiscoveredActivityBeans); + configureNexusServiceBeansByTaskQueue( + workerFactory, workers, autoDiscoveredNexusServiceBeans); configureWorkflowImplementationsByWorkerName( workers, autoDiscoveredWorkflowImplementationClasses); configureActivityBeansByWorkerName(workers, autoDiscoveredActivityBeans); + configureNexusServiceBeansByWorkerName(workers, autoDiscoveredNexusServiceBeans); } return workers.getWorkers(); @@ -215,6 +222,35 @@ private void configureActivityBeansByTaskQueue( }); } + private void configureNexusServiceBeansByTaskQueue( + WorkerFactory workerFactory, + Workers workers, + Map autoDiscoveredNexusServiceBeans) { + autoDiscoveredNexusServiceBeans.forEach( + (beanName, bean) -> { + Class targetClass = AopUtils.getTargetClass(bean); + NexusServiceImpl annotation = + AnnotationUtils.findAnnotation(targetClass, NexusServiceImpl.class); + if (annotation != null) { + for (String taskQueue : annotation.taskQueues()) { + taskQueue = environment.resolvePlaceholders(taskQueue); + Worker worker = workerFactory.tryGetWorker(taskQueue); + if (worker == null) { + log.info( + "Creating a worker with default settings for a task queue '{}' " + + "caused by an auto-discovered nexus service class {}", + taskQueue, + targetClass); + worker = createNewWorker(taskQueue, null, workers); + } + + configureNexusServiceImplementationAutoDiscovery( + worker, bean, beanName, targetClass, null, workers); + } + } + }); + } + private void configureWorkflowImplementationsByWorkerName( Workers workers, Collection> autoDiscoveredWorkflowImplementationClasses) { for (Class clazz : autoDiscoveredWorkflowImplementationClasses) { @@ -259,6 +295,31 @@ private void configureActivityBeansByWorkerName( }); } + private void configureNexusServiceBeansByWorkerName( + Workers workers, Map autoDiscoveredNexusServiceBeans) { + autoDiscoveredNexusServiceBeans.forEach( + (beanName, bean) -> { + Class targetClass = AopUtils.getTargetClass(bean); + NexusServiceImpl annotation = + AnnotationUtils.findAnnotation(targetClass, NexusServiceImpl.class); + if (annotation != null) { + for (String workerName : annotation.workers()) { + Worker worker = workers.getByName(workerName); + if (worker == null) { + throw new BeanDefinitionValidationException( + "Worker with name " + + workerName + + " is not found in the config, but is referenced by auto-discovered nexus service bean " + + beanName); + } + + configureNexusServiceImplementationAutoDiscovery( + worker, bean, beanName, targetClass, workerName, workers); + } + } + }); + } + private Collection> autoDiscoverWorkflowImplementations() { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); @@ -282,6 +343,10 @@ private Map autoDiscoverActivityBeans() { return beanFactory.getBeansWithAnnotation(ActivityImpl.class); } + private Map autoDiscoverNexusServiceBeans() { + return beanFactory.getBeansWithAnnotation(NexusServiceImpl.class); + } + private void createWorkerFromAnExplicitConfig( WorkerFactory workerFactory, WorkerProperties workerProperties, Workers workers) { String taskQueue = workerProperties.getTaskQueue(); @@ -321,6 +386,25 @@ private void createWorkerFromAnExplicitConfig( worker, beanName, bean.getClass().getName(), activityImplMetadata); }); } + + Collection nexusServiceBeans = workerProperties.getNexusServiceBeans(); + if (nexusServiceBeans != null) { + nexusServiceBeans.forEach( + beanName -> { + Object bean = beanFactory.getBean(beanName); + log.info( + "Registering configured nexus service bean '{}' of a {} class on task queue '{}'", + beanName, + AopUtils.getTargetClass(bean), + taskQueue); + worker.registerNexusServiceImplementation(bean); + addRegisteredNexusServiceImpl( + worker, + beanName, + bean.getClass().getName(), + ServiceImplInstance.fromInstance(AopUtils.getTargetClass(bean)).getDefinition()); + }); + } } private void configureActivityImplementationAutoDiscovery( @@ -357,6 +441,42 @@ private void configureActivityImplementationAutoDiscovery( } } + private void configureNexusServiceImplementationAutoDiscovery( + Worker worker, + Object bean, + String beanName, + Class targetClass, + String byWorkerName, + Workers workers) { + try { + worker.registerNexusServiceImplementation(bean); + addRegisteredNexusServiceImpl( + worker, + beanName, + bean.getClass().getName(), + ServiceImplInstance.fromInstance(bean).getDefinition()); + if (log.isInfoEnabled()) { + log.info( + "Registering auto-discovered nexus service bean '{}' of class {} on a worker {} with a task queue '{}'", + beanName, + targetClass, + byWorkerName != null ? "'" + byWorkerName + "' " : "", + worker.getTaskQueue()); + } + } catch (TypeAlreadyRegisteredException registeredEx) { + if (log.isInfoEnabled()) { + log.info( + "Skipping auto-discovered nexus service bean '{}' of class {} on a worker {} with a task queue '{}'" + + " as nexus service type '{}' is already registered on the worker", + beanName, + targetClass, + byWorkerName != null ? "'" + byWorkerName + "' " : "", + worker.getTaskQueue(), + registeredEx.getRegisteredTypeName()); + } + } + } + private void configureWorkflowImplementationAutoDiscovery( Worker worker, Class clazz, String byWorkerName, Workers workers) { try { @@ -475,15 +595,44 @@ private void addRegisteredActivityImpl( } } + private void addRegisteredNexusServiceImpl( + Worker worker, String beanName, String beanClass, ServiceDefinition serviceDefinition) { + if (!registeredInfo.containsKey(worker.getTaskQueue())) { + registeredInfo.put( + worker.getTaskQueue(), + new RegisteredInfo() + .addNexusServiceInfo( + new RegisteredNexusServiceInfo() + .addBeanName(beanName) + .addClassName(beanClass) + .addDefinition(serviceDefinition))); + } else { + registeredInfo + .get(worker.getTaskQueue()) + .getRegisteredNexusServiceInfos() + .add( + new RegisteredNexusServiceInfo() + .addBeanName(beanName) + .addClassName(beanClass) + .addDefinition(serviceDefinition)); + } + } + public static class RegisteredInfo { private final List registeredActivityInfo = new ArrayList<>(); private final List registeredWorkflowInfo = new ArrayList<>(); + private final List registeredNexusServiceInfos = new ArrayList<>(); public RegisteredInfo addActivityInfo(RegisteredActivityInfo activityInfo) { registeredActivityInfo.add(activityInfo); return this; } + public RegisteredInfo addNexusServiceInfo(RegisteredNexusServiceInfo nexusServiceInfo) { + registeredNexusServiceInfos.add(nexusServiceInfo); + return this; + } + public RegisteredInfo addWorkflowInfo(RegisteredWorkflowInfo workflowInfo) { registeredWorkflowInfo.add(workflowInfo); return this; @@ -496,6 +645,10 @@ public List getRegisteredActivityInfo() { public List getRegisteredWorkflowInfo() { return registeredWorkflowInfo; } + + public List getRegisteredNexusServiceInfos() { + return registeredNexusServiceInfos; + } } @Experimental @@ -532,6 +685,40 @@ public POJOActivityImplMetadata getMetadata() { } } + @Experimental + public static class RegisteredNexusServiceInfo { + private String beanName; + private String className; + private ServiceDefinition definition; + + public RegisteredNexusServiceInfo addClassName(String className) { + this.className = className; + return this; + } + + public RegisteredNexusServiceInfo addBeanName(String beanName) { + this.beanName = beanName; + return this; + } + + public RegisteredNexusServiceInfo addDefinition(ServiceDefinition definition) { + this.definition = definition; + return this; + } + + public String getClassName() { + return className; + } + + public String getBeanName() { + return beanName; + } + + public ServiceDefinition getDefinition() { + return definition; + } + } + @Experimental public static class RegisteredWorkflowInfo { private String className; diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueResolverTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueResolverTest.java index 484c75f4e..c0cbb42d4 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueResolverTest.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueResolverTest.java @@ -20,14 +20,13 @@ package io.temporal.spring.boot.autoconfigure; +import io.temporal.api.nexus.v1.Endpoint; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.spring.boot.autoconfigure.bytaskqueue.TestWorkflow; +import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.worker.WorkerFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ConfigurableApplicationContext; @@ -43,11 +42,23 @@ public class AutoDiscoveryByTaskQueueResolverTest { @Autowired WorkflowClient workflowClient; + @Autowired TestWorkflowEnvironment testWorkflowEnvironment; + @Autowired WorkerFactory workerFactory; + Endpoint endpoint; + @BeforeEach void setUp() { applicationContext.start(); + endpoint = + testWorkflowEnvironment.createNexusEndpoint( + "AutoDiscoveryByTaskQueueEndpoint", "PropertyResolverTest"); + } + + @AfterEach + void tearDown() { + testWorkflowEnvironment.deleteNexusEndpoint(endpoint); } @Test @@ -57,7 +68,7 @@ public void testAutoDiscovery() { workflowClient.newWorkflowStub( TestWorkflow.class, WorkflowOptions.newBuilder().setTaskQueue("PropertyResolverTest").build()); - testWorkflow.execute("input"); + testWorkflow.execute("nexus"); } @ComponentScan( diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueTest.java index 757129166..24edf2c5a 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueTest.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByTaskQueueTest.java @@ -20,14 +20,12 @@ package io.temporal.spring.boot.autoconfigure; +import io.temporal.api.nexus.v1.Endpoint; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.spring.boot.autoconfigure.bytaskqueue.TestWorkflow; import io.temporal.testing.TestWorkflowEnvironment; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ConfigurableApplicationContext; @@ -44,10 +42,18 @@ public class AutoDiscoveryByTaskQueueTest { @Autowired TestWorkflowEnvironment testWorkflowEnvironment; @Autowired WorkflowClient workflowClient; + Endpoint endpoint; @BeforeEach void setUp() { applicationContext.start(); + endpoint = + testWorkflowEnvironment.createNexusEndpoint("AutoDiscoveryByTaskQueueEndpoint", "UnitTest"); + } + + @AfterEach + void tearDown() { + testWorkflowEnvironment.deleteNexusEndpoint(endpoint); } @Test @@ -56,7 +62,7 @@ public void testAutoDiscovery() { TestWorkflow testWorkflow = workflowClient.newWorkflowStub( TestWorkflow.class, WorkflowOptions.newBuilder().setTaskQueue("UnitTest").build()); - testWorkflow.execute("input"); + testWorkflow.execute("nexus"); } @ComponentScan( diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameTest.java index b6930b18c..98b1a4de5 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameTest.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/AutoDiscoveryByWorkerNameTest.java @@ -20,14 +20,12 @@ package io.temporal.spring.boot.autoconfigure; +import io.temporal.api.nexus.v1.Endpoint; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.spring.boot.autoconfigure.bytaskqueue.TestWorkflow; import io.temporal.testing.TestWorkflowEnvironment; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ConfigurableApplicationContext; @@ -44,10 +42,19 @@ public class AutoDiscoveryByWorkerNameTest { @Autowired TestWorkflowEnvironment testWorkflowEnvironment; @Autowired WorkflowClient workflowClient; + Endpoint endpoint; @BeforeEach void setUp() { applicationContext.start(); + endpoint = + testWorkflowEnvironment.createNexusEndpoint( + "AutoDiscoveryByWorkerNameTestEndpoint", "UnitTest"); + } + + @AfterEach + void tearDown() { + testWorkflowEnvironment.deleteNexusEndpoint(endpoint); } @Test @@ -56,7 +63,7 @@ public void testAutoDiscovery() { TestWorkflow testWorkflow = workflowClient.newWorkflowStub( TestWorkflow.class, WorkflowOptions.newBuilder().setTaskQueue("UnitTest").build()); - testWorkflow.execute("input"); + testWorkflow.execute("nexus"); } @ComponentScan( diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/OptionalWorkerOptionsTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/OptionalWorkerOptionsTest.java index 9884742f3..85f40d638 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/OptionalWorkerOptionsTest.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/OptionalWorkerOptionsTest.java @@ -113,6 +113,10 @@ public TemporalOptionsCustomizer workerCustomizer() { 1, options.getMaxConcurrentLocalActivityExecutionSize(), "Values from the Spring Config should be respected"); + assertEquals( + 1, + options.getMaxConcurrentNexusExecutionSize(), + "Values from the Spring Config should be respected"); assertEquals( 1, @@ -122,6 +126,10 @@ public TemporalOptionsCustomizer workerCustomizer() { 1, options.getMaxConcurrentActivityTaskPollers(), "Values from the Spring Config should be respected"); + assertEquals( + 1, + options.getMaxConcurrentNexusTaskPollers(), + "Values from the Spring Config should be respected"); assertEquals( 1.0, diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/RegisteredInfoTest.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/RegisteredInfoTest.java index e1a666ff5..d26e55db5 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/RegisteredInfoTest.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/RegisteredInfoTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.*; +import io.nexusrpc.ServiceDefinition; import io.temporal.common.metadata.POJOActivityImplMetadata; import io.temporal.common.metadata.POJOWorkflowImplMetadata; import io.temporal.spring.boot.autoconfigure.template.WorkersTemplate; @@ -95,6 +96,19 @@ public void testRegisteredInfo() { assertEquals( "execute", metadata.getActivityMethods().get(0).getMethod().getName()); }); + + info.getRegisteredNexusServiceInfos() + .forEach( + (nexusServiceInfo) -> { + assertEquals( + "io.temporal.spring.boot.autoconfigure.bytaskqueue.TestNexusServiceImpl", + nexusServiceInfo.getClassName()); + assertEquals("TestNexusServiceImpl", nexusServiceInfo.getBeanName()); + ServiceDefinition def = nexusServiceInfo.getDefinition(); + assertEquals("TestNexusService", def.getName()); + assertEquals(1, def.getOperations().size()); + assertEquals("operation", def.getOperations().get("operation").getName()); + }); }); } diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusService.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusService.java new file mode 100644 index 000000000..ddda00320 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusService.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.spring.boot.autoconfigure.bytaskqueue; + +import io.nexusrpc.Operation; +import io.nexusrpc.Service; + +@Service +public interface TestNexusService { + @Operation + String operation(String input); +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusServiceImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusServiceImpl.java new file mode 100644 index 000000000..16c0ff642 --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestNexusServiceImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.spring.boot.autoconfigure.bytaskqueue; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.spring.boot.NexusServiceImpl; +import org.springframework.stereotype.Component; + +@Component("TestNexusServiceImpl") +@NexusServiceImpl(taskQueues = "${default-queue.name:UnitTest}") +@ServiceImpl(service = TestNexusService.class) +public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync((ctx, details, name) -> "Hello, " + name + "!"); + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestWorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestWorkflowImpl.java index 83f4c898b..c7a39a212 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestWorkflowImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/bytaskqueue/TestWorkflowImpl.java @@ -22,6 +22,9 @@ import io.temporal.activity.ActivityOptions; import io.temporal.spring.boot.WorkflowImpl; +import io.temporal.spring.boot.autoconfigure.byworkername.TestNexusService; +import io.temporal.workflow.NexusOperationOptions; +import io.temporal.workflow.NexusServiceOptions; import io.temporal.workflow.Workflow; import java.time.Duration; @@ -29,6 +32,18 @@ public class TestWorkflowImpl implements TestWorkflow { @Override public String execute(String input) { + if (input.equals("nexus")) { + Workflow.newNexusServiceStub( + TestNexusService.class, + NexusServiceOptions.newBuilder() + .setEndpoint("AutoDiscoveryByTaskQueueEndpoint") + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()) + .build()) + .operation(input); + } return Workflow.newActivityStub( TestActivity.class, ActivityOptions.newBuilder() diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusService.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusService.java new file mode 100644 index 000000000..c2cba39be --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusService.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.spring.boot.autoconfigure.byworkername; + +import io.nexusrpc.Operation; +import io.nexusrpc.Service; + +@Service +public interface TestNexusService { + @Operation + String operation(String input); +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java new file mode 100644 index 000000000..c78e0646a --- /dev/null +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestNexusServiceImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.spring.boot.autoconfigure.byworkername; + +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; +import io.temporal.spring.boot.NexusServiceImpl; +import io.temporal.spring.boot.autoconfigure.bytaskqueue.TestNexusService; +import org.springframework.stereotype.Component; + +@Component("TestNexusServiceImpl") +@NexusServiceImpl(workers = "mainWorker") +@ServiceImpl(service = TestNexusService.class) +public class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + // Implemented inline + return OperationHandler.sync((ctx, details, name) -> "Hello, " + name + "!"); + } +} diff --git a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java index 2dce1ff4e..eae95af3f 100644 --- a/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java +++ b/temporal-spring-boot-autoconfigure/src/test/java/io/temporal/spring/boot/autoconfigure/byworkername/TestWorkflowImpl.java @@ -22,6 +22,8 @@ import io.temporal.activity.ActivityOptions; import io.temporal.spring.boot.WorkflowImpl; +import io.temporal.workflow.NexusOperationOptions; +import io.temporal.workflow.NexusServiceOptions; import io.temporal.workflow.Workflow; import java.time.Duration; @@ -29,6 +31,18 @@ public class TestWorkflowImpl implements TestWorkflow { @Override public String execute(String input) { + if (input.equals("nexus")) { + Workflow.newNexusServiceStub( + TestNexusService.class, + NexusServiceOptions.newBuilder() + .setEndpoint("AutoDiscoveryByWorkerNameTestEndpoint") + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()) + .build()) + .operation(input); + } return Workflow.newActivityStub( TestActivity.class, ActivityOptions.newBuilder() diff --git a/temporal-spring-boot-autoconfigure/src/test/resources/application.yml b/temporal-spring-boot-autoconfigure/src/test/resources/application.yml index d8a2e45a5..7f39c14d3 100644 --- a/temporal-spring-boot-autoconfigure/src/test/resources/application.yml +++ b/temporal-spring-boot-autoconfigure/src/test/resources/application.yml @@ -85,10 +85,12 @@ spring: - task-queue: UnitTest capacity: max-concurrent-workflow-task-executors: 1 + max-concurrent-nexus-task-executors: 1 max-concurrent-activity-executors: 1 max-concurrent-local-activity-executors: 1 max-concurrent-workflow-task-pollers: 1 max-concurrent-activity-task-pollers: 1 + max-concurrent-nexus-task-pollers: 1 rate-limits: max-worker-activities-per-second: 1.0 max-task-queue-activities-per-second: 1.0 diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java b/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java index 69541eae6..d878cd588 100644 --- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java +++ b/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java @@ -20,7 +20,7 @@ package io.temporal.internal.testservice; -import static io.temporal.internal.testservice.LinkConverter.*; +import static io.temporal.internal.common.LinkConverter.*; import static io.temporal.internal.testservice.StateMachines.Action.CANCEL; import static io.temporal.internal.testservice.StateMachines.Action.COMPLETE; import static io.temporal.internal.testservice.StateMachines.Action.CONTINUE_AS_NEW; @@ -715,6 +715,7 @@ private static void scheduleNexusOperation( .setPayload(attr.getInput()) .addLinks(link) .setCallback("http://test-env/operations") + .setRequestId(UUID.randomUUID().toString()) // The test server uses this to lookup the operation .putCallbackHeader( "operation-reference", ref.toBytes().toStringUtf8()))); @@ -2454,7 +2455,8 @@ static RetryPolicy ensureDefaultFieldsForActivityRetryPolicy(RetryPolicy origina static RetryPolicy defaultNexusRetryPolicy() { return RetryPolicy.newBuilder() .addAllNonRetryableErrorTypes( - Arrays.asList("INVALID_ARGUMENT", "NOT_FOUND", "DEADLINE_EXCEEDED", "CANCELLED")) + Arrays.asList( + "BAD_REQUEST", "INVALID_ARGUMENT", "NOT_FOUND", "DEADLINE_EXCEEDED", "CANCELLED")) .setInitialInterval(Durations.fromSeconds(1)) .setMaximumInterval(Durations.fromHours(1)) .setBackoffCoefficient(2.0) diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java index f2a016bed..6d26ed8c4 100644 --- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java +++ b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java @@ -33,6 +33,7 @@ import com.google.protobuf.util.Timestamps; import io.grpc.*; import io.grpc.stub.StreamObserver; +import io.nexusrpc.Header; import io.temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributes; import io.temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes; import io.temporal.api.common.v1.Payload; @@ -790,7 +791,7 @@ public void pollNexusTaskQueue( String taskTimeout = String.valueOf(Timestamps.between(store.currentTime(), task.getDeadline()).getSeconds()); Request.Builder req = - task.getTask().getRequestBuilder().putHeader("Request-Timeout", taskTimeout); + task.getTask().getRequestBuilder().putHeader(Header.REQUEST_TIMEOUT, taskTimeout + "s"); PollNexusTaskQueueResponse.Builder resp = task.getTask().setRequest(req); responseObserver.onNext(resp.build()); diff --git a/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java b/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java index a98c8c76e..9b209c02b 100644 --- a/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java +++ b/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java @@ -38,7 +38,7 @@ import io.temporal.api.workflowservice.v1.*; import io.temporal.client.WorkflowOptions; import io.temporal.client.WorkflowStub; -import io.temporal.internal.testservice.LinkConverter; +import io.temporal.internal.common.LinkConverter; import io.temporal.internal.testservice.NexusTaskToken; import io.temporal.testing.internal.SDKTestWorkflowRule; import io.temporal.testserver.functional.common.TestWorkflows; diff --git a/temporal-testing/src/main/java/io/temporal/internal/sync/DummySyncWorkflowContext.java b/temporal-testing/src/main/java/io/temporal/internal/sync/DummySyncWorkflowContext.java index e19ddde12..2a5468b12 100644 --- a/temporal-testing/src/main/java/io/temporal/internal/sync/DummySyncWorkflowContext.java +++ b/temporal-testing/src/main/java/io/temporal/internal/sync/DummySyncWorkflowContext.java @@ -23,6 +23,7 @@ import com.uber.m3.tally.NoopScope; import com.uber.m3.tally.Scope; import io.temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributes; +import io.temporal.api.command.v1.ScheduleNexusOperationCommandAttributes; import io.temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes; import io.temporal.api.common.v1.*; import io.temporal.api.failure.v1.Failure; @@ -208,6 +209,14 @@ public Functions.Proc1 startChildWorkflow( throw new UnsupportedOperationException("not implemented"); } + @Override + public Functions.Proc1 startNexusOperation( + ScheduleNexusOperationCommandAttributes attributes, + Functions.Proc2, Failure> startedCallback, + Functions.Proc2, Failure> completionCallback) { + throw new UnsupportedOperationException("not implemented"); + } + @Override public Functions.Proc1 signalExternalWorkflowExecution( SignalExternalWorkflowExecutionCommandAttributes.Builder attributes, diff --git a/temporal-testing/src/main/java/io/temporal/testing/TestActivityEnvironmentInternal.java b/temporal-testing/src/main/java/io/temporal/testing/TestActivityEnvironmentInternal.java index a8adb759d..1f13e16e8 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TestActivityEnvironmentInternal.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TestActivityEnvironmentInternal.java @@ -380,6 +380,12 @@ public ChildWorkflowOutput executeChildWorkflow(ChildWorkflowInput inp throw new UnsupportedOperationException("not implemented"); } + @Override + public ExecuteNexusOperationOutput executeNexusOperation( + ExecuteNexusOperationInput input) { + throw new UnsupportedOperationException("not implemented"); + } + @Override public Random newRandom() { throw new UnsupportedOperationException("not implemented"); diff --git a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java index b03e21cd4..0066a19c5 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java @@ -22,7 +22,9 @@ import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.api.enums.v1.IndexedValueType; +import io.temporal.api.nexus.v1.Endpoint; import io.temporal.client.WorkflowClient; +import io.temporal.common.Experimental; import io.temporal.common.WorkflowExecutionHistory; import io.temporal.serviceclient.OperatorServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubs; @@ -159,6 +161,24 @@ static TestWorkflowEnvironment newInstance(TestEnvironmentOptions options) { */ boolean registerSearchAttribute(String name, IndexedValueType type); + /** + * Register a Nexus Endpoint with the server. + * + * @param name Nexus Endpoint name + * @param taskQueue Task Queue to be used for the endpoint + * @return Endpoint object + */ + @Experimental + Endpoint createNexusEndpoint(String name, String taskQueue); + + /** + * Delete a Nexus Endpoint on the server. + * + * @param endpoint current endpoint to be deleted + */ + @Experimental + void deleteNexusEndpoint(Endpoint endpoint); + /** * @return the in-memory test Temporal service that is owned by this. * @deprecated use {{@link #getWorkflowServiceStubs()} diff --git a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java index 0c0868584..25cee90cb 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironmentInternal.java @@ -22,14 +22,20 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ObjectArrays; +import com.google.protobuf.ByteString; import com.google.protobuf.Empty; import com.uber.m3.tally.NoopScope; import com.uber.m3.tally.Scope; import io.grpc.Status; import io.grpc.StatusRuntimeException; +import io.temporal.api.common.v1.Payload; import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.api.enums.v1.IndexedValueType; +import io.temporal.api.nexus.v1.Endpoint; +import io.temporal.api.nexus.v1.EndpointSpec; +import io.temporal.api.nexus.v1.EndpointTarget; import io.temporal.api.operatorservice.v1.AddSearchAttributesRequest; +import io.temporal.api.operatorservice.v1.CreateNexusEndpointRequest; import io.temporal.api.testservice.v1.SleepRequest; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; @@ -219,6 +225,38 @@ public boolean registerSearchAttribute(String name, IndexedValueType type) { } } + @Override + public Endpoint createNexusEndpoint(String name, String taskQueue) { + EndpointSpec spec = + EndpointSpec.newBuilder() + .setName(name) + .setDescription( + Payload.newBuilder() + .setData( + ByteString.copyFromUtf8( + "Test Nexus endpoint created by the Java SDK WorkflowTestEnvironment"))) + .setTarget( + EndpointTarget.newBuilder() + .setWorker( + EndpointTarget.Worker.newBuilder() + .setNamespace(getNamespace()) + .setTaskQueue(taskQueue))) + .build(); + CreateNexusEndpointRequest request = + CreateNexusEndpointRequest.newBuilder().setSpec(spec).build(); + return operatorServiceStubs.blockingStub().createNexusEndpoint(request).getEndpoint(); + } + + public void deleteNexusEndpoint(Endpoint endpoint) { + operatorServiceStubs + .blockingStub() + .deleteNexusEndpoint( + io.temporal.api.operatorservice.v1.DeleteNexusEndpointRequest.newBuilder() + .setId(endpoint.getId()) + .setVersion(endpoint.getVersion()) + .build()); + } + @Deprecated public WorkflowServiceStubs getWorkflowService() { return getWorkflowServiceStubs(); diff --git a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowExtension.java b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowExtension.java index 391eb2c9e..80a7a70ee 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowExtension.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowExtension.java @@ -20,11 +20,15 @@ package io.temporal.testing; +import static io.temporal.testing.internal.TestServiceUtils.applyNexusServiceOptions; + import com.uber.m3.tally.Scope; import io.temporal.api.enums.v1.IndexedValueType; +import io.temporal.api.nexus.v1.Endpoint; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowOptions; +import io.temporal.common.Experimental; import io.temporal.common.metadata.POJOWorkflowImplMetadata; import io.temporal.common.metadata.POJOWorkflowInterfaceMetadata; import io.temporal.serviceclient.WorkflowServiceStubsOptions; @@ -36,11 +40,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Parameter; import java.time.Instant; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import javax.annotation.Nonnull; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; @@ -89,15 +89,18 @@ public class TestWorkflowExtension private static final String TEST_ENVIRONMENT_KEY = "testEnvironment"; private static final String WORKER_KEY = "worker"; private static final String WORKFLOW_OPTIONS_KEY = "workflowOptions"; + private static final String NEXUS_ENDPOINT_KEY = "nexusEndpoint"; private final WorkerOptions workerOptions; private final WorkflowClientOptions workflowClientOptions; private final WorkerFactoryOptions workerFactoryOptions; private final Map, WorkflowImplementationOptions> workflowTypes; private final Object[] activityImplementations; + private final Object[] nexusServiceImplementations; private final boolean useExternalService; private final String target; private final boolean doNotStart; + private final boolean doNotSetupNexusEndpoint; private final long initialTimeMillis; private final boolean useTimeskipping; @Nonnull private final Map searchAttributes; @@ -117,9 +120,11 @@ private TestWorkflowExtension(Builder builder) { workerFactoryOptions = builder.workerFactoryOptions; workflowTypes = builder.workflowTypes; activityImplementations = builder.activityImplementations; + nexusServiceImplementations = builder.nexusServiceImplementations; useExternalService = builder.useExternalService; target = builder.target; doNotStart = builder.doNotStart; + doNotSetupNexusEndpoint = builder.doNotSetupNexusEndpoint; initialTimeMillis = builder.initialTimeMillis; useTimeskipping = builder.useTimeskipping; this.searchAttributes = builder.searchAttributes; @@ -212,13 +217,26 @@ public void beforeEach(ExtensionContext context) { String taskQueue = String.format("WorkflowTest-%s-%s", context.getDisplayName(), context.getUniqueId()); + String nexusEndpointName = String.format("WorkflowTestNexusEndpoint-%s", UUID.randomUUID()); + boolean createNexusEndpoint = + !doNotSetupNexusEndpoint && nexusServiceImplementations.length > 0; Worker worker = testEnvironment.newWorker(taskQueue, workerOptions); - workflowTypes.forEach((wft, o) -> worker.registerWorkflowImplementationTypes(o, wft)); + workflowTypes.forEach( + (wft, o) -> { + if (createNexusEndpoint) { + o = applyNexusServiceOptions(o, nexusServiceImplementations, nexusEndpointName); + } + worker.registerWorkflowImplementationTypes(o, wft); + }); worker.registerActivitiesImplementations(activityImplementations); + worker.registerNexusServiceImplementation(nexusServiceImplementations); if (!doNotStart) { testEnvironment.start(); } + if (createNexusEndpoint) { + setNexusEndpoint(context, testEnvironment.createNexusEndpoint(nexusEndpointName, taskQueue)); + } setTestEnvironment(context, testEnvironment); setWorker(context, worker); @@ -239,8 +257,12 @@ protected TestEnvironmentOptions createTestEnvOptions(long initialTimeMillis) { } @Override - public void afterEach(ExtensionContext context) throws Exception { + public void afterEach(ExtensionContext context) { + Endpoint endpoint = getNexusEndpoint(context); TestWorkflowEnvironment testEnvironment = getTestEnvironment(context); + if (endpoint != null && !testEnvironment.getOperatorServiceStubs().isShutdown()) { + testEnvironment.deleteNexusEndpoint(endpoint); + } testEnvironment.close(); } @@ -267,6 +289,14 @@ private void setWorker(ExtensionContext context, Worker worker) { getStore(context).put(WORKER_KEY, worker); } + private Endpoint getNexusEndpoint(ExtensionContext context) { + return getStore(context).get(NEXUS_ENDPOINT_KEY, Endpoint.class); + } + + private void setNexusEndpoint(ExtensionContext context, Endpoint endpoint) { + getStore(context).put(NEXUS_ENDPOINT_KEY, endpoint); + } + private WorkflowOptions getWorkflowOptions(ExtensionContext context) { return getStore(context).get(WORKFLOW_OPTIONS_KEY, WorkflowOptions.class); } @@ -284,6 +314,7 @@ private ExtensionContext.Store getStore(ExtensionContext context) { public static class Builder { private static final Object[] NO_ACTIVITIES = new Object[0]; + private static final Object[] NO_NEXUS_SERVICES = new Object[0]; private WorkerOptions workerOptions = WorkerOptions.getDefaultInstance(); private WorkflowClientOptions workflowClientOptions; @@ -291,9 +322,11 @@ public static class Builder { private String namespace = "UnitTest"; private Map, WorkflowImplementationOptions> workflowTypes = new HashMap<>(); private Object[] activityImplementations = NO_ACTIVITIES; + private Object[] nexusServiceImplementations = NO_NEXUS_SERVICES; private boolean useExternalService = false; private String target = null; private boolean doNotStart = false; + private boolean doNotSetupNexusEndpoint = false; private long initialTimeMillis; // Default to TestEnvironmentOptions isUseTimeskipping private boolean useTimeskipping = @@ -364,6 +397,22 @@ public Builder registerWorkflowImplementationTypes( return this; } + /** + * Specify Nexus service implementations to register with the Temporal workerIf any Nexus + * services are registered with the worker, the extension will automatically create a Nexus + * Endpoint for the test and the endpoint will be set on the per-service options and default + * options in {@link WorkflowImplementationOptions} if none are provided. + * + *

    This can be disabled by setting {@link #setDoNotSetupNexusEndpoint(boolean)} to true. + * + * @see Worker#registerNexusServiceImplementation(Object...) + */ + @Experimental + public Builder setNexusServiceImplementation(Object... nexusServiceImplementations) { + this.nexusServiceImplementations = nexusServiceImplementations; + return this; + } + /** * Specify workflow implementation types to register with the Temporal worker. * @@ -429,6 +478,16 @@ public Builder setDoNotStart(boolean doNotStart) { return this; } + /** + * When set to true the {@link TestWorkflowEnvironment} will not automatically create a Nexus + * Endpoint. This is useful when you want to manually create a Nexus Endpoint for your test. + */ + @Experimental + public Builder setDoNotSetupNexusEndpoint(boolean doNotSetupNexusEndpoint) { + this.doNotSetupNexusEndpoint = doNotSetupNexusEndpoint; + return this; + } + /** * Set the initial time for the workflow virtual clock, milliseconds since epoch. * diff --git a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowRule.java b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowRule.java index b5d429382..f09e34ff7 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowRule.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowRule.java @@ -20,15 +20,19 @@ package io.temporal.testing; +import static io.temporal.testing.internal.TestServiceUtils.applyNexusServiceOptions; + import com.uber.m3.tally.Scope; import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.api.enums.v1.IndexedValueType; import io.temporal.api.history.v1.History; +import io.temporal.api.nexus.v1.Endpoint; import io.temporal.api.workflowservice.v1.WorkflowServiceGrpc; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowOptions; import io.temporal.client.WorkflowStub; +import io.temporal.common.Experimental; import io.temporal.common.SearchAttributeKey; import io.temporal.common.interceptors.WorkerInterceptor; import io.temporal.internal.common.env.DebugModeUtils; @@ -84,10 +88,12 @@ public class TestWorkflowRule implements TestRule { private final String namespace; private final boolean useExternalService; private final boolean doNotStart; + private final boolean doNotSetupNexusEndpoint; @Nullable private final Timeout globalTimeout; private final Class[] workflowTypes; private final Object[] activityImplementations; + private final Object[] nexusServiceImplementations; private final WorkflowServiceStubsOptions serviceStubsOptions; private final WorkflowClientOptions clientOptions; private final WorkerFactoryOptions workerFactoryOptions; @@ -100,6 +106,8 @@ public class TestWorkflowRule implements TestRule { @Nonnull private final Map searchAttributes; private String taskQueue; + private String nexusEndpointName; + private Endpoint nexusEndpoint; private final TestWorkflowEnvironment testEnvironment; private final TestWatcher watchman = new TestWatcher() { @@ -111,12 +119,17 @@ protected void failed(Throwable e, Description description) { private TestWorkflowRule(Builder builder) { this.doNotStart = builder.doNotStart; + this.doNotSetupNexusEndpoint = builder.doNotSetupNexusEndpoint; this.useExternalService = builder.useExternalService; this.namespace = (builder.namespace == null) ? RegisterTestNamespace.NAMESPACE : builder.namespace; this.workflowTypes = (builder.workflowTypes == null) ? new Class[0] : builder.workflowTypes; this.activityImplementations = (builder.activityImplementations == null) ? new Object[0] : builder.activityImplementations; + this.nexusServiceImplementations = + (builder.nexusServiceImplementations == null) + ? new Object[0] + : builder.nexusServiceImplementations; this.serviceStubsOptions = (builder.workflowServiceStubsOptions == null) ? WorkflowServiceStubsOptions.newBuilder().build() @@ -175,6 +188,7 @@ public static class Builder { private String target; private boolean useExternalService; private boolean doNotStart; + private boolean doNotSetupNexusEndpoint; private long initialTimeMillis; // Default to TestEnvironmentOptions isUseTimeskipping private boolean useTimeskipping = @@ -182,6 +196,7 @@ public static class Builder { private Class[] workflowTypes; private Object[] activityImplementations; + private Object[] nexusServiceImplementations; private WorkflowServiceStubsOptions workflowServiceStubsOptions; private WorkflowClientOptions workflowClientOptions; private WorkerFactoryOptions workerFactoryOptions; @@ -234,6 +249,22 @@ public Builder setWorkflowTypes( return this; } + /** + * Specify Nexus service implementations to register with the Temporal worker. If any Nexus + * services are registered with the worker, the rule will automatically create a Nexus Endpoint + * for the test and the endpoint will be set on the per-service options and default options in + * {@link WorkflowImplementationOptions} if none are provided. + * + *

    This can be disabled by setting {@link #setDoNotSetupNexusEndpoint(boolean)} to true. + * + * @see Worker#registerNexusServiceImplementation(Object...) + */ + @Experimental + public Builder setNexusServiceImplementation(Object... nexusServiceImplementations) { + this.nexusServiceImplementations = nexusServiceImplementations; + return this; + } + public Builder setActivityImplementations(Object... activityImplementations) { this.activityImplementations = activityImplementations; return this; @@ -313,6 +344,16 @@ public Builder setDoNotStart(boolean doNotStart) { return this; } + /** + * When set to true the {@link TestWorkflowEnvironment} will not automatically create a Nexus + * Endpoint. This is useful when you want to manually create a Nexus Endpoint for your test. + */ + @Experimental + public Builder setDoNotSetupNexusEndpoint(boolean doNotSetupNexusEndpoint) { + this.doNotSetupNexusEndpoint = doNotSetupNexusEndpoint; + return this; + } + /** * Add a search attribute to be registered on the Temporal Server. * @@ -394,19 +435,34 @@ public void evaluate() throws Throwable { private String init(Description description) { String testMethod = description.getMethodName(); String taskQueue = "WorkflowTest-" + testMethod + "-" + UUID.randomUUID(); + nexusEndpointName = String.format("WorkflowTestNexusEndpoint-%s", UUID.randomUUID()); Worker worker = testEnvironment.newWorker(taskQueue, workerOptions); + WorkflowImplementationOptions workflowImplementationOptions = + this.workflowImplementationOptions; + if (!doNotSetupNexusEndpoint) { + workflowImplementationOptions = + applyNexusServiceOptions( + workflowImplementationOptions, nexusServiceImplementations, nexusEndpointName); + } worker.registerWorkflowImplementationTypes(workflowImplementationOptions, workflowTypes); worker.registerActivitiesImplementations(activityImplementations); + worker.registerNexusServiceImplementation(nexusServiceImplementations); return taskQueue; } private void start() { + if (!doNotSetupNexusEndpoint && nexusServiceImplementations.length > 0) { + nexusEndpoint = testEnvironment.createNexusEndpoint(nexusEndpointName, taskQueue); + } if (!doNotStart) { testEnvironment.start(); } } protected void shutdown() { + if (nexusEndpoint != null && !testEnvironment.getOperatorServiceStubs().isShutdown()) { + testEnvironment.deleteNexusEndpoint(nexusEndpoint); + } testEnvironment.close(); } @@ -430,6 +486,13 @@ public String getTaskQueue() { return taskQueue; } + /** + * @return endpoint of the nexus service created for the test. + */ + public Endpoint getNexusEndpoint() { + return nexusEndpoint; + } + /** * @return client to the Temporal service used to start and query workflows. */ diff --git a/temporal-testing/src/main/java/io/temporal/testing/TimeLockingInterceptor.java b/temporal-testing/src/main/java/io/temporal/testing/TimeLockingInterceptor.java index 6d04a006f..d1e907ad2 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/TimeLockingInterceptor.java +++ b/temporal-testing/src/main/java/io/temporal/testing/TimeLockingInterceptor.java @@ -188,6 +188,11 @@ public Optional getOptions() { return next.getOptions(); } + @Override + public WorkflowStub newInstance(WorkflowOptions options) { + return new TimeLockingWorkflowStub(locker, next.newInstance(options)); + } + /** Unlocks time skipping before blocking calls and locks back after completion. */ private class TimeLockingFuture extends CompletableFuture { diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java b/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java index 7d1c18b55..bda485fdd 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java @@ -32,6 +32,7 @@ import io.temporal.api.enums.v1.IndexedValueType; import io.temporal.api.history.v1.History; import io.temporal.api.history.v1.HistoryEvent; +import io.temporal.api.nexus.v1.Endpoint; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowQueryException; @@ -162,6 +163,11 @@ public Builder setWorkflowTypes(Class... workflowTypes) { return this; } + public Builder setNexusServiceImplementation(Object... nexusServiceImplementations) { + testWorkflowRuleBuilder.setNexusServiceImplementation(nexusServiceImplementations); + return this; + } + public Builder setWorkflowTypes( WorkflowImplementationOptions implementationOptions, Class... workflowTypes) { testWorkflowRuleBuilder.setWorkflowTypes(implementationOptions, workflowTypes); @@ -258,6 +264,10 @@ public String getTaskQueue() { return testWorkflowRule.getTaskQueue(); } + public Endpoint getNexusEndpoint() { + return testWorkflowRule.getNexusEndpoint(); + } + public Worker getWorker() { return testWorkflowRule.getWorker(); } diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/TestServiceUtils.java b/temporal-testing/src/main/java/io/temporal/testing/internal/TestServiceUtils.java index fd54cea5d..2ef904332 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/TestServiceUtils.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/TestServiceUtils.java @@ -23,6 +23,7 @@ import static io.temporal.internal.common.InternalUtils.createNormalTaskQueue; import com.google.protobuf.ByteString; +import io.nexusrpc.handler.ServiceImplInstance; import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.api.common.v1.WorkflowType; import io.temporal.api.taskqueue.v1.StickyExecutionAttributes; @@ -35,13 +36,53 @@ import io.temporal.api.workflowservice.v1.StartWorkflowExecutionRequest; import io.temporal.internal.common.ProtobufTimeUtils; import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.WorkflowImplementationOptions; +import io.temporal.workflow.NexusServiceOptions; import java.time.Duration; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; public class TestServiceUtils { private TestServiceUtils() {} + public static WorkflowImplementationOptions applyNexusServiceOptions( + WorkflowImplementationOptions options, + Object[] nexusServiceImplementations, + String endpoint) { + Map newNexusServiceOptions = new HashMap<>(); + for (Object nexusService : nexusServiceImplementations) { + String serviceName = ServiceImplInstance.fromInstance(nexusService).getDefinition().getName(); + NexusServiceOptions serviceOptionWithEndpoint = + options.getNexusServiceOptions().get(serviceName); + if (serviceOptionWithEndpoint == null) { + serviceOptionWithEndpoint = NexusServiceOptions.newBuilder().build(); + } + serviceOptionWithEndpoint = + serviceOptionWithEndpoint.getEndpoint() == null + ? NexusServiceOptions.newBuilder(serviceOptionWithEndpoint) + .setEndpoint(endpoint) + .build() + : serviceOptionWithEndpoint; + newNexusServiceOptions.put(serviceName, serviceOptionWithEndpoint); + } + NexusServiceOptions defaultServiceOptions = + options.getDefaultNexusServiceOptions() == null + ? NexusServiceOptions.newBuilder().build() + : options.getDefaultNexusServiceOptions(); + if (defaultServiceOptions.getEndpoint() == null) { + options = + options.toBuilder() + .setDefaultNexusServiceOptions( + NexusServiceOptions.newBuilder(defaultServiceOptions) + .setEndpoint(endpoint) + .build()) + .build(); + } + return options.toBuilder().setNexusServiceOptions(newNexusServiceOptions).build(); + } + public static void startWorkflowExecution( String namespace, String taskqueueName, String workflowType, WorkflowServiceStubs service) throws Exception { diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/TracingWorkerInterceptor.java b/temporal-testing/src/main/java/io/temporal/testing/internal/TracingWorkerInterceptor.java index 6282b7d2a..88bba8b45 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/TracingWorkerInterceptor.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/TracingWorkerInterceptor.java @@ -185,6 +185,15 @@ public ChildWorkflowOutput executeChildWorkflow(ChildWorkflowInput inp return next.executeChildWorkflow(input); } + @Override + public ExecuteNexusOperationOutput executeNexusOperation( + ExecuteNexusOperationInput input) { + if (!WorkflowUnsafe.isReplaying()) { + trace.add("executeNexusOperation " + input.getOperation()); + } + return next.executeNexusOperation(input); + } + @Override public Random newRandom() { if (!WorkflowUnsafe.isReplaying()) { diff --git a/temporal-testing/src/test/java/io/temporal/testing/junit5/TestWorkflowExtensionTest.java b/temporal-testing/src/test/java/io/temporal/testing/junit5/TestWorkflowExtensionTest.java index d03d34737..f4741da38 100644 --- a/temporal-testing/src/test/java/io/temporal/testing/junit5/TestWorkflowExtensionTest.java +++ b/temporal-testing/src/test/java/io/temporal/testing/junit5/TestWorkflowExtensionTest.java @@ -25,6 +25,11 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.nexusrpc.Operation; +import io.nexusrpc.Service; +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; import io.temporal.activity.Activity; import io.temporal.activity.ActivityInfo; import io.temporal.activity.ActivityInterface; @@ -35,9 +40,7 @@ import io.temporal.testing.TestWorkflowExtension; import io.temporal.testing.WorkflowInitialTime; import io.temporal.worker.Worker; -import io.temporal.workflow.Workflow; -import io.temporal.workflow.WorkflowInterface; -import io.temporal.workflow.WorkflowMethod; +import io.temporal.workflow.*; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -55,9 +58,24 @@ public class TestWorkflowExtensionTest { TestWorkflowExtension.newBuilder() .registerWorkflowImplementationTypes(HelloWorkflowImpl.class) .setActivityImplementations(new HelloActivityImpl()) + .setNexusServiceImplementation(new TestNexusServiceImpl()) .setInitialTime(Instant.parse("2021-10-10T10:01:00Z")) .build(); + @Service + public interface TestNexusService { + @Operation + String operation(String input); + } + + @ServiceImpl(service = TestNexusService.class) + public static class TestNexusServiceImpl { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync((ctx, details, name) -> "Hello, " + name + "!"); + } + } + @ActivityInterface public interface HelloActivity { String buildGreeting(String name); @@ -88,9 +106,20 @@ public static class HelloWorkflowImpl implements HelloWorkflow { HelloActivity.class, ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofMinutes(1)).build()); + private final TestNexusService nexusService = + Workflow.newNexusServiceStub( + TestNexusService.class, + NexusServiceOptions.newBuilder() + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()) + .build()); + @Override public String sayHello(String name) { logger.info("Hello, {}", name); + nexusService.operation(name); Workflow.sleep(Duration.ofHours(1)); return helloActivity.buildGreeting(name); }