From 6cbec4dd97e43de06f471613b3a992f5b637d767 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Tue, 1 Sep 2026 11:26:00 +0200 Subject: [PATCH 1/6] feat!: unify the durable task client into dapr-sdk-workflows The durabletask-client module duplicated much of the workflows API across a module boundary, which forced an adapter layer and a second copy of a dozen types. Fold it into dapr-sdk-workflows and remove it. The absorbed types move from io.dapr.durabletask.* to io.dapr.workflows.task.*, split into client, worker, history, exception, serialization and internal subpackages. Twelve duplicate type-pairs collapse to one each. Most significantly TaskOrchestrationContext becomes WorkflowContext and is implemented directly by the replay executor, so the DefaultWorkflowContext adapter and the converter and wrapper classes it required are gone. Two behaviour changes come with the merge: - The runtime's default executor is a virtual-thread-per-task executor on Java 21+, resolved reflectively so the module still compiles at release 17 and never names a Java 21 API. WorkflowRuntime now tracks whether it created the executor and no longer shuts down one supplied by the caller, so handing it a framework-managed executor is safe. - WorkflowRuntimeStatus absorbs the protobuf mapping and gains STALLED, which the deleted converter rejected with an exception. isCausedBy maps the exception names that shipped under io.dapr.durabletask onto their current types, so a workflow started before the upgrade still resolves its failure type after it. The CI job that built durabletask-client now installs its reactor dependencies first: unlike the old module, the merged one depends on the sibling dapr-sdk and cannot be built standalone. BREAKING CHANGE: io.dapr:durabletask-client is removed and its classes ship in io.dapr:dapr-sdk-workflows. Types move from io.dapr.durabletask.* to io.dapr.workflows.task.*, seven types are renamed, and the four @Deprecated(forRemoval) DaprWorkflowClient methods together with WorkflowInstanceStatus are removed. See MIGRATION.md for the full type mapping and for what it means for workflows that are already running. Signed-off-by: Javier Aliaga --- .github/workflows/build.yml | 23 +- .../client/ObservationDaprWorkflowClient.java | 9 +- .../ObservationDaprWorkflowClientTest.java | 16 - durabletask-client/pom.xml | 240 ------- .../java/io/dapr/durabletask/Helpers.java | 77 -- .../OrchestrationRuntimeStatus.java | 125 ---- .../io/dapr/durabletask/RetryContext.java | 79 --- .../io/dapr/durabletask/RetryHandler.java | 31 - .../java/io/dapr/durabletask/RetryPolicy.java | 176 ----- .../java/io/dapr/durabletask/TaskOptions.java | 201 ------ .../durabletask/TaskOrchestrationContext.java | 641 ----------------- .../unittesting/DaprWorkflowExampleTest.java | 4 +- .../childworkflow/DemoChildWorkflow.java | 2 +- .../compensation/BookTripWorkflow.java | 2 +- .../faninout/DemoFanInOutWorkflow.java | 2 +- .../historypropagation/AuditActivity.java | 6 +- .../DemoFraudCheckChildWorkflow.java | 4 +- .../multiapp/App1Workflow.java | 2 +- .../multiapp/App2AuditActivity.java | 4 +- pom.xml | 3 +- sdk-bom/pom.xml | 5 - .../workflows/TestExecutionKeysWorkflow.java | 2 +- sdk-workflows/pom.xml | 213 +++++- .../io/dapr/workflows/WorkflowActivity.java | 4 +- .../workflows/WorkflowActivityContext.java | 2 +- .../io/dapr/workflows/WorkflowContext.java | 666 ++++++++++-------- .../dapr/workflows/WorkflowTaskOptions.java | 153 +++- .../workflows/WorkflowTaskRetryContext.java | 50 +- .../workflows/WorkflowTaskRetryPolicy.java | 57 +- .../workflows/client/DaprWorkflowClient.java | 106 +-- .../client/WorkflowFailureDetails.java | 52 -- .../client/WorkflowInstanceStatus.java | 144 ---- .../client/WorkflowRuntimeStatus.java | 82 ++- .../dapr/workflows/client/WorkflowState.java | 2 + .../internal/DefaultExecutorService.java | 65 ++ .../DefaultWorkflowActivityContext.java | 4 +- .../runtime/DefaultWorkflowContext.java | 337 --------- .../DefaultWorkflowFailureDetails.java | 81 --- .../DefaultWorkflowInstanceStatus.java | 212 ------ .../runtime/DefaultWorkflowState.java | 20 +- .../runtime/WorkflowActivityClassWrapper.java | 4 +- .../WorkflowActivityInstanceWrapper.java | 4 +- .../runtime/WorkflowClassWrapper.java | 4 +- .../runtime/WorkflowInstanceWrapper.java | 4 +- .../workflows/runtime/WorkflowRuntime.java | 30 +- .../runtime/WorkflowRuntimeBuilder.java | 15 +- .../WorkflowRuntimeStatusConverter.java | 58 -- .../runtime/WorkflowVersionWrapper.java | 2 +- .../workflows/task}/OrchestratorFunction.java | 6 +- .../java/io/dapr/workflows/task}/Task.java | 12 +- .../io/dapr/workflows/task}/TaskActivity.java | 7 +- .../workflows/task}/TaskActivityContext.java | 4 +- .../workflows/task}/TaskActivityFactory.java | 2 +- .../workflows/task}/TaskOrchestration.java | 17 +- .../task/client}/DurableTaskClient.java | 12 +- .../task/client}/DurableTaskGrpcClient.java | 8 +- .../client}/DurableTaskGrpcClientBuilder.java | 283 ++++---- .../NewOrchestrationInstanceOptions.java | 4 +- .../task/client}/OrchestrationMetadata.java | 33 +- .../task/client}/PurgeInstanceCriteria.java | 11 +- .../workflows/task/client}/PurgeResult.java | 4 +- .../CompositeTaskFailedException.java | 18 +- ...NonDeterministicOrchestratorException.java | 4 +- .../exception}/TaskCanceledException.java | 7 +- .../task/exception}/TaskFailedException.java | 25 +- .../VersionNotRegisteredException.java | 2 +- .../exception/WorkflowFailureDetails.java | 99 ++- .../task/history}/ActivityResult.java | 2 +- .../task/history}/ChildWorkflowResult.java | 2 +- .../history}/HistoryPropagationScope.java | 12 +- .../task/history}/PropagatedHistory.java | 13 +- .../history}/PropagatedHistoryException.java | 2 +- .../task/history}/WorkflowResult.java | 2 +- .../dapr/workflows/task/internal/Helpers.java | 124 ++++ .../task/internal}/TaskActivityExecutor.java | 9 +- .../internal}/TaskOrchestrationExecutor.java | 99 ++- .../internal}/TaskOrchestratorResult.java | 2 +- .../task/internal}/UuidGenerator.java | 2 +- .../task/internal}/WorkflowHistoryCache.java | 2 +- .../task/internal}/runner/ActivityRunner.java | 9 +- .../task/internal}/runner/DurableRunner.java | 3 +- .../internal/runner}/OrchestrationRunner.java | 12 +- .../internal}/runner/OrchestratorRunner.java | 8 +- .../ContinueAsNewInterruption.java | 8 +- .../OrchestratorBlockedException.java | 4 +- .../TaskOrchestrationFactories.java | 4 +- .../TaskOrchestrationFactory.java | 4 +- .../task/serialization}/DataConverter.java | 3 +- .../serialization}/JacksonDataConverter.java | 2 +- .../task/worker}/DurableTaskGrpcWorker.java | 14 +- .../worker}/DurableTaskGrpcWorkerBuilder.java | 8 +- .../workflows/DefaultWorkflowContextTest.java | 610 ---------------- .../WorkflowTaskOptionsMergedTest.java | 54 +- .../workflows/WorkflowTaskOptionsTest.java | 30 +- .../WorkflowTaskRetryPolicyTest.java | 145 ++++ .../java/io/dapr/workflows/WorkflowTest.java | 4 +- .../client/DaprWorkflowClientTest.java | 13 +- .../client/WorkflowInstanceStatusTest.java | 204 ------ .../client/WorkflowRuntimeStatusTest.java | 56 ++ .../workflows/client/WorkflowStateTest.java | 9 +- .../internal/DefaultExecutorServiceTest.java | 73 ++ .../DefaultWorkflowActivityContextTest.java | 2 +- .../WorkflowActivityClassWrapperTest.java | 2 +- .../WorkflowActivityInstanceWrapperTest.java | 2 +- .../runtime/WorkflowClassWrapperTest.java | 7 +- .../runtime/WorkflowInstanceWrapperTest.java | 3 +- .../runtime/WorkflowRuntimeBuilderTest.java | 12 +- .../WorkflowRuntimeStatusConverterTest.java | 71 -- .../runtime/WorkflowRuntimeTest.java | 30 +- .../runtime/WorkflowVersionWrapperTest.java | 2 +- .../task}/ActivityHistoryPropagationTest.java | 7 +- .../workflows/task}/DurableTaskClientIT.java | 158 +++-- .../DurableTaskGrpcClientScheduleTest.java | 5 +- .../task}/DurableTaskGrpcClientTlsTest.java | 2 +- .../DurableTaskGrpcClientTracingTest.java | 4 +- ...rableTaskGrpcWorkerChannelBackoffTest.java | 4 +- .../DurableTaskGrpcWorkerReconnectTest.java | 4 +- .../DurableTaskGrpcWorkerShutdownTest.java | 4 +- ...ableTaskGrpcWorkerStatefulHistoryTest.java | 4 +- .../dapr/workflows/task}/ErrorHandlingIT.java | 68 +- .../HistoryPropagationIntegrationTest.java | 29 +- .../workflows/task}/IntegrationTestBase.java | 6 +- .../workflows/task}/StatefulHistoryIT.java | 9 +- .../task}/SubOrchestrationCrossAppTest.java | 33 +- .../workflows/task}/WorkItemObserver.java | 2 +- .../exception/WorkflowFailureDetailsTest.java | 113 +++ .../history/HistoryPropagationScopeTest.java | 39 + .../internal}/AgentLoopEventDeliveryTest.java | 8 +- .../internal}/HistoryPropagationTest.java | 51 +- .../task/internal}/TimerOriginTest.java | 34 +- .../internal/WorkflowContextLoggerTest.java | 157 +++++ .../internal}/WorkflowHistoryCacheTest.java | 2 +- .../runner/OrchestratorRunnerHistoryTest.java | 6 +- .../orchestrator/CustomerWorkflow.java | 4 +- .../wfp/fanoutin/FanOutInWorkflow.java | 2 +- .../FraudCheckActivity.java | 2 +- .../FraudDetectionWorkflow.java | 6 +- .../ProcessPaymentWorkflow.java | 2 +- .../SettlePaymentActivity.java | 4 +- .../workflows/TestExecutionKeysWorkflow.java | 2 +- 140 files changed, 2629 insertions(+), 4404 deletions(-) delete mode 100644 durabletask-client/pom.xml delete mode 100644 durabletask-client/src/main/java/io/dapr/durabletask/Helpers.java delete mode 100644 durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java delete mode 100644 durabletask-client/src/main/java/io/dapr/durabletask/RetryContext.java delete mode 100644 durabletask-client/src/main/java/io/dapr/durabletask/RetryHandler.java delete mode 100644 durabletask-client/src/main/java/io/dapr/durabletask/RetryPolicy.java delete mode 100644 durabletask-client/src/main/java/io/dapr/durabletask/TaskOptions.java delete mode 100644 durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowFailureDetails.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstanceStatus.java create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowFailureDetails.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowInstanceStatus.java delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverter.java rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/OrchestratorFunction.java (93%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/Task.java (90%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/TaskActivity.java (94%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/TaskActivityContext.java (95%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/TaskActivityFactory.java (96%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/TaskOrchestration.java (85%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/client}/DurableTaskClient.java (97%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/client}/DurableTaskGrpcClient.java (98%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/client}/DurableTaskGrpcClientBuilder.java (96%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/client}/NewOrchestrationInstanceOptions.java (98%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/client}/OrchestrationMetadata.java (89%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/client}/PurgeInstanceCriteria.java (91%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/client}/PurgeResult.java (92%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/exception}/CompositeTaskFailedException.java (72%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/exception}/NonDeterministicOrchestratorException.java (84%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/exception}/TaskCanceledException.java (78%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/exception}/TaskFailedException.java (74%) rename {durabletask-client/src/main/java/io/dapr/durabletask/orchestration => sdk-workflows/src/main/java/io/dapr/workflows/task}/exception/VersionNotRegisteredException.java (92%) rename durabletask-client/src/main/java/io/dapr/durabletask/FailureDetails.java => sdk-workflows/src/main/java/io/dapr/workflows/task/exception/WorkflowFailureDetails.java (54%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/history}/ActivityResult.java (98%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/history}/ChildWorkflowResult.java (98%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/history}/HistoryPropagationScope.java (85%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/history}/PropagatedHistory.java (93%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/history}/PropagatedHistoryException.java (96%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/history}/WorkflowResult.java (99%) create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/task/internal/Helpers.java rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/TaskActivityExecutor.java (94%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/TaskOrchestrationExecutor.java (95%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/TaskOrchestratorResult.java (97%) rename {durabletask-client/src/main/java/io/dapr/durabletask/util => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/UuidGenerator.java (98%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/WorkflowHistoryCache.java (99%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/runner/ActivityRunner.java (95%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/runner/DurableRunner.java (98%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner}/OrchestrationRunner.java (93%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/internal}/runner/OrchestratorRunner.java (97%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/interruption/ContinueAsNewInterruption.java (85%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/interruption/OrchestratorBlockedException.java (94%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/orchestration/TaskOrchestrationFactories.java (97%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task}/orchestration/TaskOrchestrationFactory.java (91%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/serialization}/DataConverter.java (98%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/serialization}/JacksonDataConverter.java (97%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/worker}/DurableTaskGrpcWorker.java (96%) rename {durabletask-client/src/main/java/io/dapr/durabletask => sdk-workflows/src/main/java/io/dapr/workflows/task/worker}/DurableTaskGrpcWorkerBuilder.java (96%) delete mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java rename durabletask-client/src/test/java/io/dapr/durabletask/TaskOptionsTest.java => sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsMergedTest.java (64%) create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskRetryPolicyTest.java delete mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstanceStatusTest.java create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowRuntimeStatusTest.java create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java delete mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverterTest.java rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/ActivityHistoryPropagationTest.java (94%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskClientIT.java (91%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskGrpcClientScheduleTest.java (95%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskGrpcClientTlsTest.java (99%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskGrpcClientTracingTest.java (97%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskGrpcWorkerChannelBackoffTest.java (97%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskGrpcWorkerReconnectTest.java (97%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskGrpcWorkerShutdownTest.java (96%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/DurableTaskGrpcWorkerStatefulHistoryTest.java (98%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/ErrorHandlingIT.java (80%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/HistoryPropagationIntegrationTest.java (94%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/IntegrationTestBase.java (93%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/StatefulHistoryIT.java (94%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/SubOrchestrationCrossAppTest.java (93%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task}/WorkItemObserver.java (99%) create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/task/exception/WorkflowFailureDetailsTest.java create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/task/history/HistoryPropagationScopeTest.java rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task/internal}/AgentLoopEventDeliveryTest.java (97%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task/internal}/HistoryPropagationTest.java (95%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task/internal}/TimerOriginTest.java (95%) create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/task/internal/WorkflowContextLoggerTest.java rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task/internal}/WorkflowHistoryCacheTest.java (99%) rename {durabletask-client/src/test/java/io/dapr/durabletask => sdk-workflows/src/test/java/io/dapr/workflows/task/internal}/runner/OrchestratorRunnerHistoryTest.java (97%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index daa27e720d..3d0f4e6fe3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,10 +46,10 @@ jobs: name: report-dapr-java-sdk-actors-jdk${{ env.JDK_VER }} path: sdk-actors/target/jacoco-report/ - build-durabletask: - name: "Durable Task build & tests" + build-workflows: + name: "Workflows build & tests" runs-on: ubuntu-latest - timeout-minutes: 7 + timeout-minutes: 15 continue-on-error: false env: JDK_VER: 17 @@ -60,6 +60,13 @@ jobs: with: distribution: 'temurin' java-version: ${{ env.JDK_VER }} + + # sdk-workflows depends on the sibling `dapr-sdk` module, so `-pl sdk-workflows` cannot + # resolve it from a repository on a clean runner. Install the module and its upstream + # reactor dependencies first, as the `build` job does. + - name: Install sdk-workflows and its reactor dependencies + run: ./mvnw clean install -B -q -DskipTests -pl sdk-workflows -am + - name: Checkout Durable Task Sidecar uses: actions/checkout@v7 with: @@ -77,13 +84,15 @@ jobs: - name: Wait for 10 seconds run: sleep 10 - - name: Integration Tests For Durable Tasks + - name: Integration Tests For Workflows uses: nick-fields/retry@v4 with: max_attempts: 2 timeout_minutes: 5 retry_wait_seconds: 15 - command: ./mvnw -B -pl durabletask-client -Pintegration-tests dependency:copy-dependencies verify + # jacoco.skip: this profile skips surefire, so there is no unit-test coverage to + # measure here. The gate is enforced in the "Unit tests" job, which runs surefire. + command: ./mvnw -B -pl sdk-workflows -Pintegration-tests -Djacoco.skip=true dependency:copy-dependencies verify - name: Kill Durable Task Sidecar run: docker rm -f durabletask-sidecar || true @@ -127,7 +136,7 @@ jobs: max_attempts: 2 timeout_minutes: 25 retry_wait_seconds: 15 - command: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -pl !durabletask-client -Pintegration-tests dependency:copy-dependencies verify + command: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -pl !sdk-workflows -Pintegration-tests dependency:copy-dependencies verify - name: Upload failsafe test report for sdk-tests on failure if: ${{ failure() && steps.integration_tests.conclusion == 'failure' }} uses: actions/upload-artifact@v7 @@ -156,7 +165,7 @@ jobs: publish: runs-on: ubuntu-latest - needs: [ build, test, build-durabletask ] + needs: [ build, test, build-workflows ] timeout-minutes: 10 env: JDK_VER: 17 diff --git a/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClient.java b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClient.java index 4366857e75..86fdd19fee 100644 --- a/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClient.java +++ b/dapr-spring/dapr-spring-boot-observation/src/main/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClient.java @@ -39,11 +39,10 @@ /** * A {@link DaprWorkflowClient} subclass that creates Micrometer Observation spans (bridged to - * OpenTelemetry) for each non-deprecated method call. + * OpenTelemetry) for each method call. * *

Because this class extends {@link DaprWorkflowClient}, consumers can keep injecting - * {@code DaprWorkflowClient} without any code changes. Deprecated methods fall through to the - * parent implementation without any observation. + * {@code DaprWorkflowClient} without any code changes. * *

Trace propagation: an {@link OtelTracingClientInterceptor} is registered on the gRPC * channel. For each synchronous workflow RPC, the observation opens an OTel scope (via @@ -334,10 +333,6 @@ public boolean purgeWorkflow(String workflowInstanceId) { } } - // Deprecated methods (getInstanceState, waitForInstanceStart, waitForInstanceCompletion, - // purgeInstance) are intentionally not overridden — they fall through to the parent - // implementation without any observation. - // ------------------------------------------------------------------------- // gRPC interceptor: injects the current OTel span's traceparent into headers // ------------------------------------------------------------------------- diff --git a/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClientTest.java b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClientTest.java index ede8f49ce8..b2c2086ff8 100644 --- a/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClientTest.java +++ b/dapr-spring/dapr-spring-boot-observation/src/test/java/io/dapr/spring/observation/client/ObservationDaprWorkflowClientTest.java @@ -202,22 +202,6 @@ void purgeWorkflowCreatesSpan() { .hasError(); } - // ------------------------------------------------------------------------- - // Deprecated methods — must NOT create spans - // ------------------------------------------------------------------------- - - @Test - @DisplayName("Deprecated getInstanceState falls through to parent without creating a span") - @SuppressWarnings("deprecation") - void deprecatedGetInstanceStateDoesNotCreateSpan() { - // This will fail (no sidecar) but must not leave any observations in the registry - assertThatThrownBy(() -> client.getInstanceState("instance-1", false)) - .isInstanceOf(RuntimeException.class); - - // No spans should have been created for deprecated methods - TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation(); - } - // ------------------------------------------------------------------------- // Dummy workflow implementation for type-based tests // ------------------------------------------------------------------------- diff --git a/durabletask-client/pom.xml b/durabletask-client/pom.xml deleted file mode 100644 index 9d0e4e8aea..0000000000 --- a/durabletask-client/pom.xml +++ /dev/null @@ -1,240 +0,0 @@ - - - 4.0.0 - - io.dapr - dapr-sdk-parent - 1.19.0-SNAPSHOT - ../pom.xml - - - durabletask-client - jar - durabletask-client - Durable Task Client for Dapr Workflows - - - false - ${project.build.directory}/generated-sources - ${project.build.directory}/proto - - - - - javax.annotation - javax.annotation-api - provided - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-stub - - - io.grpc - grpc-netty - - - com.google.protobuf - protobuf-java - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - org.apache.commons - commons-lang3 - - - io.grpc - grpc-testing - test - - - org.junit.jupiter - junit-jupiter - test - - - org.testcontainers - testcontainers - test - - - io.micrometer - micrometer-observation - - - io.opentelemetry - opentelemetry-api - - - io.opentelemetry - opentelemetry-context - - - io.opentelemetry - opentelemetry-sdk - test - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${project.build.outputDirectory} - - - - com.googlecode.maven-download-plugin - download-maven-plugin - ${download-maven-plugin.version} - - - getOrchestratorServiceProto - initialize - - wget - - - true - ${durabletask.proto.baseurl}/orchestrator_service.proto - orchestrator_service.proto - ${protobuf.input.directory} - - - - getOrchestrationProto - initialize - - wget - - - true - ${durabletask.proto.baseurl}/orchestration.proto - orchestration.proto - ${protobuf.input.directory} - - - - getHistoryEventsProto - initialize - - wget - - - true - ${durabletask.proto.baseurl}/history_events.proto - history_events.proto - ${protobuf.input.directory} - - - - getOrchestratorActionsProto - initialize - - wget - - - true - ${durabletask.proto.baseurl}/orchestrator_actions.proto - orchestrator_actions.proto - ${protobuf.input.directory} - - - - getAttestationProto - initialize - - wget - - - true - ${durabletask.proto.baseurl}/attestation.proto - attestation.proto - ${protobuf.input.directory} - - - - - - org.xolstice.maven.plugins - protobuf-maven-plugin - ${protobuf-maven-plugin.version} - - com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} - grpc-java - io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} - ${protobuf.input.directory} - - - - - compile - compile-custom - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - true - - - - attach-javadocs - - jar - - - - - - com.github.spotbugs - spotbugs-maven-plugin - - - true - - - - - diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/Helpers.java b/durabletask-client/src/main/java/io/dapr/durabletask/Helpers.java deleted file mode 100644 index 265bb0ab06..0000000000 --- a/durabletask-client/src/main/java/io/dapr/durabletask/Helpers.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.time.Duration; - -final class Helpers { - static final Duration maxDuration = Duration.ofSeconds(Long.MAX_VALUE, 999999999L); - - static @Nonnull V throwIfArgumentNull(@Nullable V argValue, String argName) { - if (argValue == null) { - throw new IllegalArgumentException("The argument '" + argName + "' was null."); - } - - return argValue; - } - - static @Nonnull String throwIfArgumentNullOrWhiteSpace(String argValue, String argName) { - throwIfArgumentNull(argValue, argName); - if (argValue.trim().length() == 0) { - throw new IllegalArgumentException("The argument '" + argName + "' was empty or contained only whitespace."); - } - - return argValue; - } - - static void throwIfOrchestratorComplete(boolean isComplete) { - if (isComplete) { - throw new IllegalStateException("The orchestrator has already completed"); - } - } - - static boolean isInfiniteTimeout(Duration timeout) { - return timeout == null || timeout.isNegative() || timeout.equals(maxDuration); - } - - static double powExact(double base, double exponent) throws ArithmeticException { - if (base == 0.0) { - return 0.0; - } - - double result = Math.pow(base, exponent); - - if (result == Double.POSITIVE_INFINITY) { - throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY"); - } else if (result == Double.NEGATIVE_INFINITY) { - throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY"); - } else if (Double.compare(-0.0f, result) == 0) { - throw new ArithmeticException("Double overflow resulting in negative zero"); - } else if (Double.compare(+0.0f, result) == 0) { - throw new ArithmeticException("Double overflow resulting in positive zero"); - } - - return result; - } - - static boolean isNullOrEmpty(String s) { - return s == null || s.isEmpty(); - } - - // Cannot be instantiated - private Helpers() { - } -} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java b/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java deleted file mode 100644 index 3dde08ccb9..0000000000 --- a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -import io.dapr.durabletask.implementation.protobuf.Orchestration; - -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED; -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED; -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW; -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED; -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_PENDING; -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_RUNNING; -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_SUSPENDED; -import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED; - -/** - * Enum describing the runtime status of the orchestration. - */ -public enum OrchestrationRuntimeStatus { - /** - * The orchestration started running. - */ - RUNNING, - - /** - * The orchestration completed normally. - */ - COMPLETED, - - /** - * The orchestration is transitioning into a new instance. - * This status value is obsolete and exists only for compatibility reasons. - */ - CONTINUED_AS_NEW, - - /** - * The orchestration completed with an unhandled exception. - */ - FAILED, - - /** - * The orchestration canceled gracefully. - * The Canceled status is not currently used and exists only for compatibility reasons. - */ - CANCELED, - - /** - * The orchestration was abruptly terminated via a management API call. - */ - TERMINATED, - - /** - * The orchestration was scheduled but hasn't started running. - */ - PENDING, - - /** - * The orchestration is in a suspended state. - */ - SUSPENDED, - - /** - * The orchestration is in a stalled state. - */ - STALLED; - - static OrchestrationRuntimeStatus fromProtobuf(Orchestration.OrchestrationStatus status) { - switch (status) { - case ORCHESTRATION_STATUS_RUNNING: - return RUNNING; - case ORCHESTRATION_STATUS_COMPLETED: - return COMPLETED; - case ORCHESTRATION_STATUS_CONTINUED_AS_NEW: - return CONTINUED_AS_NEW; - case ORCHESTRATION_STATUS_FAILED: - return FAILED; - case ORCHESTRATION_STATUS_CANCELED: - return CANCELED; - case ORCHESTRATION_STATUS_TERMINATED: - return TERMINATED; - case ORCHESTRATION_STATUS_PENDING: - return PENDING; - case ORCHESTRATION_STATUS_SUSPENDED: - return SUSPENDED; - case ORCHESTRATION_STATUS_STALLED: - return STALLED; - default: - throw new IllegalArgumentException(String.format("Unknown status value: %s", status)); - } - } - - static Orchestration.OrchestrationStatus toProtobuf(OrchestrationRuntimeStatus status) { - switch (status) { - case RUNNING: - return ORCHESTRATION_STATUS_RUNNING; - case COMPLETED: - return ORCHESTRATION_STATUS_COMPLETED; - case CONTINUED_AS_NEW: - return ORCHESTRATION_STATUS_CONTINUED_AS_NEW; - case FAILED: - return ORCHESTRATION_STATUS_FAILED; - case CANCELED: - return ORCHESTRATION_STATUS_CANCELED; - case TERMINATED: - return ORCHESTRATION_STATUS_TERMINATED; - case PENDING: - return ORCHESTRATION_STATUS_PENDING; - case SUSPENDED: - return ORCHESTRATION_STATUS_SUSPENDED; - default: - throw new IllegalArgumentException(String.format("Unknown status value: %s", status)); - } - } -} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/RetryContext.java b/durabletask-client/src/main/java/io/dapr/durabletask/RetryContext.java deleted file mode 100644 index 620e02c7d3..0000000000 --- a/durabletask-client/src/main/java/io/dapr/durabletask/RetryContext.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -import java.time.Duration; - -/** - * Context data that's provided to {@link RetryHandler} implementations. - */ -public final class RetryContext { - private final TaskOrchestrationContext orchestrationContext; - private final int lastAttemptNumber; - private final FailureDetails lastFailure; - private final Duration totalRetryTime; - - RetryContext( - TaskOrchestrationContext orchestrationContext, - int lastAttemptNumber, - FailureDetails lastFailure, - Duration totalRetryTime) { - this.orchestrationContext = orchestrationContext; - this.lastAttemptNumber = lastAttemptNumber; - this.lastFailure = lastFailure; - this.totalRetryTime = totalRetryTime; - } - - /** - * Gets the context of the current orchestration. - * - *

The orchestration context can be used in retry handlers to schedule timers (via the - * {@link TaskOrchestrationContext#createTimer} methods) for implementing delays between retries. It can also be - * used to implement time-based retry logic by using the {@link TaskOrchestrationContext#getCurrentInstant} method. - *

- * - * @return the context of the parent orchestration - */ - public TaskOrchestrationContext getOrchestrationContext() { - return this.orchestrationContext; - } - - /** - * Gets the details of the previous task failure, including the exception type, message, and callstack. - * - * @return the details of the previous task failure - */ - public FailureDetails getLastFailure() { - return this.lastFailure; - } - - /** - * Gets the previous retry attempt number. This number starts at 1 and increments each time the retry handler - * is invoked for a particular task failure. - * - * @return the previous retry attempt number - */ - public int getLastAttemptNumber() { - return this.lastAttemptNumber; - } - - /** - * Gets the total amount of time spent in a retry loop for the current task. - * - * @return the total amount of time spent in a retry loop for the current task - */ - public Duration getTotalRetryTime() { - return this.totalRetryTime; - } -} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/RetryHandler.java b/durabletask-client/src/main/java/io/dapr/durabletask/RetryHandler.java deleted file mode 100644 index ad246a0c65..0000000000 --- a/durabletask-client/src/main/java/io/dapr/durabletask/RetryHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -/** - * Functional interface for implementing custom task retry handlers. - * - *

It's important to remember that retry handler code is an extension of the orchestrator code and must - * therefore comply with all the determinism requirements of orchestrator code.

- */ -@FunctionalInterface -public interface RetryHandler { - /** - * Invokes the retry handler logic and returns a value indicating whether to continue retrying. - * - * @param context retry context that's updated between each retry attempt - * @return {@code true} to continue retrying or {@code false} to stop retrying. - */ - boolean handle(RetryContext context); -} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/RetryPolicy.java b/durabletask-client/src/main/java/io/dapr/durabletask/RetryPolicy.java deleted file mode 100644 index 9efd912b1e..0000000000 --- a/durabletask-client/src/main/java/io/dapr/durabletask/RetryPolicy.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -import javax.annotation.Nullable; -import java.time.Duration; -import java.util.Objects; - -/** - * A declarative retry policy that can be configured for activity or sub-orchestration calls. - */ -public final class RetryPolicy { - - private int maxNumberOfAttempts; - private Duration firstRetryInterval; - private double backoffCoefficient = 1.0; - private Duration maxRetryInterval = Duration.ZERO; - private Duration retryTimeout = Duration.ZERO; - - /** - * Creates a new {@code RetryPolicy} object. - * - * @param maxNumberOfAttempts the maximum number of task invocation attempts; must be 1 or greater - * @param firstRetryInterval the amount of time to delay between the first and second attempt - * @throws IllegalArgumentException if {@code maxNumberOfAttempts} is zero or negative - */ - public RetryPolicy(int maxNumberOfAttempts, Duration firstRetryInterval) { - this.setMaxNumberOfAttempts(maxNumberOfAttempts); - this.setFirstRetryInterval(firstRetryInterval); - } - - /** - * Sets the maximum number of task invocation attempts; must be 1 or greater. - * - *

This value represents the number of times to attempt to execute the task. It does not represent - * the maximum number of times to retry the task. This is why the number must be 1 or greater.

- * - * @param maxNumberOfAttempts the maximum number of attempts; must be 1 or greater - * @return this retry policy object - * @throws IllegalArgumentException if {@code maxNumberOfAttempts} is zero or negative - */ - public RetryPolicy setMaxNumberOfAttempts(int maxNumberOfAttempts) { - if (maxNumberOfAttempts <= 0) { - throw new IllegalArgumentException("The value for maxNumberOfAttempts must be greater than zero."); - } - this.maxNumberOfAttempts = maxNumberOfAttempts; - return this; - } - - /** - * Sets the amount of time to delay between the first and second attempt. - * - * @param firstRetryInterval the amount of time to delay between the first and second attempt - * @return this retry policy object - * @throws IllegalArgumentException if {@code firstRetryInterval} is {@code null}, zero, or negative. - */ - public RetryPolicy setFirstRetryInterval(Duration firstRetryInterval) { - if (firstRetryInterval == null) { - throw new IllegalArgumentException("firstRetryInterval cannot be null."); - } - if (firstRetryInterval.isZero() || firstRetryInterval.isNegative()) { - throw new IllegalArgumentException("The value for firstRetryInterval must be greater than zero."); - } - this.firstRetryInterval = firstRetryInterval; - return this; - } - - /** - * Sets the exponential backoff coefficient used to determine the delay between subsequent retries. - * Must be 1.0 or greater. - * - *

To avoid extremely long delays between retries, consider also specifying a maximum retry interval using the - * {@link #setMaxRetryInterval} method.

- * - * @param backoffCoefficient the exponential backoff coefficient - * @return this retry policy object - * @throws IllegalArgumentException if {@code backoffCoefficient} is less than 1.0 - */ - public RetryPolicy setBackoffCoefficient(double backoffCoefficient) { - if (backoffCoefficient < 1.0) { - throw new IllegalArgumentException("The value for backoffCoefficient must be greater or equal to 1.0."); - } - this.backoffCoefficient = backoffCoefficient; - return this; - } - - /** - * Sets the maximum time to delay between attempts. - * - *

It's recommended to set a maximum retry interval whenever using a backoff coefficient that's greater than the - * default of 1.0.

- * - * @param maxRetryInterval the maximum time to delay between attempts or {@code null} to remove the maximum retry - * interval - * @return this retry policy object - */ - public RetryPolicy setMaxRetryInterval(@Nullable Duration maxRetryInterval) { - if (maxRetryInterval != null && maxRetryInterval.compareTo(this.firstRetryInterval) < 0) { - throw new IllegalArgumentException("The value for maxRetryInterval must be greater than or equal to the value " - + "for firstRetryInterval."); - } - this.maxRetryInterval = maxRetryInterval; - return this; - } - - /** - * Sets the overall timeout for retries, regardless of the retry count. - * - * @param retryTimeout the overall timeout for retries - * @return this retry policy object - */ - public RetryPolicy setRetryTimeout(Duration retryTimeout) { - if (retryTimeout == null || retryTimeout.compareTo(this.firstRetryInterval) < 0) { - throw new IllegalArgumentException("The value for retryTimeout cannot be null and must be greater than or equal " - + "to the value for firstRetryInterval."); - } - this.retryTimeout = retryTimeout; - return this; - } - - /** - * Gets the configured maximum number of task invocation attempts. - * - * @return the configured maximum number of task invocation attempts. - */ - public int getMaxNumberOfAttempts() { - return this.maxNumberOfAttempts; - } - - /** - * Gets the configured amount of time to delay between the first and second attempt. - * - * @return the configured amount of time to delay between the first and second attempt - */ - public Duration getFirstRetryInterval() { - return this.firstRetryInterval; - } - - /** - * Gets the configured exponential backoff coefficient used to determine the delay between subsequent retries. - * - * @return the configured exponential backoff coefficient used to determine the delay between subsequent retries - */ - public double getBackoffCoefficient() { - return this.backoffCoefficient; - } - - /** - * Gets the configured maximum time to delay between attempts. - * - * @return the configured maximum time to delay between attempts - */ - public Duration getMaxRetryInterval() { - return this.maxRetryInterval; - } - - /** - * Gets the configured overall timeout for retries. - * - * @return the configured overall timeout for retries - */ - public Duration getRetryTimeout() { - return this.retryTimeout; - } -} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOptions.java b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOptions.java deleted file mode 100644 index 0c4772626e..0000000000 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOptions.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2026 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -/** - * Options that can be used to control the behavior of orchestrator and activity task execution. - */ -public final class TaskOptions { - private final RetryPolicy retryPolicy; - private final RetryHandler retryHandler; - private final String appID; - private final HistoryPropagationScope historyPropagationScope; - - private TaskOptions(RetryPolicy retryPolicy, RetryHandler retryHandler, String appID, - HistoryPropagationScope historyPropagationScope) { - this.retryPolicy = retryPolicy; - this.retryHandler = retryHandler; - this.appID = appID; - this.historyPropagationScope = historyPropagationScope; - } - - /** - * Creates a new builder for {@code TaskOptions}. - * - * @return a new builder instance - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Creates a new {@code TaskOptions} object with default values. - * - * @return a new TaskOptions instance with no configuration - */ - public static TaskOptions create() { - return new Builder().build(); - } - - /** - * Creates a new {@code TaskOptions} object from a {@link RetryPolicy}. - * - * @param retryPolicy the retry policy to use in the new {@code TaskOptions} object. - * @return a new TaskOptions instance with the specified retry policy - */ - public static TaskOptions withRetryPolicy(RetryPolicy retryPolicy) { - return new Builder().retryPolicy(retryPolicy).build(); - } - - /** - * Creates a new {@code TaskOptions} object from a {@link RetryHandler}. - * - * @param retryHandler the retry handler to use in the new {@code TaskOptions} object. - * @return a new TaskOptions instance with the specified retry handler - */ - public static TaskOptions withRetryHandler(RetryHandler retryHandler) { - return new Builder().retryHandler(retryHandler).build(); - } - - /** - * Creates a new {@code TaskOptions} object with the specified app ID. - * - * @param appID the app ID to use for cross-app workflow routing - * @return a new TaskOptions instance with the specified app ID - */ - public static TaskOptions withAppID(String appID) { - return new Builder().appID(appID).build(); - } - - boolean hasRetryPolicy() { - return this.retryPolicy != null; - } - - /** - * Gets the configured {@link RetryPolicy} value or {@code null} if none was configured. - * - * @return the configured retry policy - */ - public RetryPolicy getRetryPolicy() { - return this.retryPolicy; - } - - boolean hasRetryHandler() { - return this.retryHandler != null; - } - - /** - * Gets the configured {@link RetryHandler} value or {@code null} if none was configured. - * - * @return the configured retry handler. - */ - public RetryHandler getRetryHandler() { - return this.retryHandler; - } - - /** - * Gets the configured app ID value or {@code null} if none was configured. - * - * @return the configured app ID - */ - public String getAppID() { - return this.appID; - } - - boolean hasAppID() { - return this.appID != null && !this.appID.isEmpty(); - } - - /** - * Gets the configured {@link HistoryPropagationScope} value or {@code null} if none was configured. - * - * @return the configured history propagation scope - */ - public HistoryPropagationScope getHistoryPropagationScope() { - return this.historyPropagationScope; - } - - boolean hasHistoryPropagationScope() { - return this.historyPropagationScope != null - && this.historyPropagationScope != HistoryPropagationScope.NONE; - } - - /** - * Builder for creating {@code TaskOptions} instances. - */ - public static final class Builder { - private RetryPolicy retryPolicy; - private RetryHandler retryHandler; - private String appID; - private HistoryPropagationScope historyPropagationScope; - - private Builder() { - // Private constructor -enforces using TaskOptions.builder() - } - - /** - * Sets the retry policy for the task options. - * - * @param retryPolicy the retry policy to use - * @return this builder instance for method chaining - */ - public Builder retryPolicy(RetryPolicy retryPolicy) { - this.retryPolicy = retryPolicy; - return this; - } - - /** - * Sets the retry handler for the task options. - * - * @param retryHandler the retry handler to use - * @return this builder instance for method chaining - */ - public Builder retryHandler(RetryHandler retryHandler) { - this.retryHandler = retryHandler; - return this; - } - - /** - * Sets the app ID for cross-app workflow routing. - * - * @param appID the app ID to use - * @return this builder instance for method chaining - */ - public Builder appID(String appID) { - this.appID = appID; - return this; - } - - /** - * Sets the history propagation scope for the task. - * - * @param scope the propagation scope to use - * @return this builder instance for method chaining - */ - public Builder historyPropagationScope(HistoryPropagationScope scope) { - this.historyPropagationScope = scope; - return this; - } - - /** - * Builds a new {@code TaskOptions} instance with the configured values. - * - * @return a new TaskOptions instance - */ - public TaskOptions build() { - return new TaskOptions(this.retryPolicy, this.retryHandler, this.appID, - this.historyPropagationScope); - } - } -} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java deleted file mode 100644 index 5efe04258f..0000000000 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java +++ /dev/null @@ -1,641 +0,0 @@ -/* - * Copyright 2026 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -import javax.annotation.Nullable; -import java.time.Duration; -import java.time.Instant; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -/** - * Used by orchestrators to perform actions such as scheduling tasks, durable timers, waiting for external events, - * and for getting basic information about the current orchestration. - */ -public interface TaskOrchestrationContext { - /** - * Gets the name of the current task orchestration. - * - * @return the name of the current task orchestration - */ - String getName(); - - /** - * Gets the deserialized input of the current task orchestration. - * - * @param targetType the {@link Class} object associated with {@code V} - * @param the expected type of the orchestrator input - * @return the deserialized input as an object of type {@code V} or {@code null} if no input was provided. - */ - V getInput(Class targetType); - - /** - * Gets the unique ID of the current orchestration instance. - * - * @return the unique ID of the current orchestration instance - */ - String getInstanceId(); - - /** - * Gets the app ID of the current orchestration instance, if available. - * This is used for cross-app workflow routing. - * - * @return the app ID of the current orchestration instance, or null if not available - */ - String getAppId(); - - /** - * Gets the current orchestration time in UTC. - * - * @return the current orchestration time in UTC - */ - Instant getCurrentInstant(); - - /** - * Gets a value indicating whether the orchestrator is currently replaying a previous execution. - * - *

Orchestrator functions are "replayed" after being unloaded from memory to reconstruct local variable state. - * During a replay, previously executed tasks will be completed automatically with previously seen values - * that are stored in the orchestration history. One the orchestrator reaches the point in the orchestrator - * where it's no longer replaying existing history, this method will return {@code false}.

- * - *

You can use this method if you have logic that needs to run only when not replaying. For example, - * certain types of application logging may become too noisy when duplicated as part of replay. The - * application code could check to see whether the function is being replayed and then issue the log statements - * when this value is {@code false}.

- * - * @return {@code true} if the orchestrator is replaying, otherwise {@code false} - */ - boolean getIsReplaying(); - - /** - * Returns a new {@code Task} that is completed when all tasks in {@code tasks} completes. - * See {@link #allOf(Task[])} for more detailed information. - * - * @param tasks the list of {@code Task} objects - * @param the return type of the {@code Task} objects - * @return a new {@code Task} that is completed when any of the given {@code Task}s complete - * @see #allOf(Task[]) - */ - Task> allOf(List> tasks); - - // TODO: Update the description of allOf to be more specific about the exception behavior. - - // https://github.io.dapr.durabletask-java/issues/54 - - /** - * Returns a new {@code Task} that is completed when all the given {@code Task}s complete. If any of the given - * {@code Task}s complete with an exception, the returned {@code Task} will also complete with - * an {@link CompositeTaskFailedException} containing details of the first encountered failure. - * The value of the returned {@code Task} is an ordered list of - * the return values of the given tasks. If no tasks are provided, returns a {@code Task} completed with value - * {@code null}. - * - *

This method is useful for awaiting the completion of a set of independent tasks before continuing to the next - * step in the orchestration, as in the following example:

- *
{@code
-   * Task t1 = ctx.callActivity("MyActivity", String.class);
-   * Task t2 = ctx.callActivity("MyActivity", String.class);
-   * Task t3 = ctx.callActivity("MyActivity", String.class);
-   *
-   * List orderedResults = ctx.allOf(t1, t2, t3).await();
-   * }
- * - *

Exceptions in any of the given tasks results in an unchecked {@link CompositeTaskFailedException}. - * This exception can be inspected to obtain failure details of individual {@link Task}s.

- *
{@code
-   * try {
-   *     List orderedResults = ctx.allOf(t1, t2, t3).await();
-   * } catch (CompositeTaskFailedException e) {
-   *     List exceptions = e.getExceptions()
-   * }
-   * }
- * - * @param tasks the {@code Task}s - * @param the return type of the {@code Task} objects - * @return the values of the completed {@code Task} objects in the same order as the source list - */ - default Task> allOf(Task... tasks) { - return this.allOf(Arrays.asList(tasks)); - } - - /** - * Returns a new {@code Task} that is completed when any of the tasks in {@code tasks} completes. - * See {@link #anyOf(Task[])} for more detailed information. - * - * @param tasks the list of {@code Task} objects - * @return a new {@code Task} that is completed when any of the given {@code Task}s complete - * @see #anyOf(Task[]) - */ - Task> anyOf(List> tasks); - - /** - * Returns a new {@code Task} that is completed when any of the given {@code Task}s complete. The value of the - * new {@code Task} is a reference to the completed {@code Task} object. If no tasks are provided, returns a - * {@code Task} that never completes. - * - *

This method is useful for waiting on multiple concurrent tasks and performing a task-specific operation when the - * first task completes, as in the following example:

- *
{@code
-   * Task event1 = ctx.waitForExternalEvent("Event1");
-   * Task event2 = ctx.waitForExternalEvent("Event2");
-   * Task event3 = ctx.waitForExternalEvent("Event3");
-   *
-   * Task winner = ctx.anyOf(event1, event2, event3).await();
-   * if (winner == event1) {
-   *     // ...
-   * } else if (winner == event2) {
-   *     // ...
-   * } else if (winner == event3) {
-   *     // ...
-   * }
-   * }
- * - *

The {@code anyOf} method can also be used for implementing long-running timeouts, as in the following example: - *

- *
{@code
-   * Task activityTask = ctx.callActivity("SlowActivity");
-   * Task timeoutTask = ctx.createTimer(Duration.ofMinutes(30));
-   *
-   * Task winner = ctx.anyOf(activityTask, timeoutTask).await();
-   * if (winner == activityTask) {
-   *     // completion case
-   * } else {
-   *     // timeout case
-   * }
-   * }
- * - * @param tasks the list of {@code Task} objects - * @return a new {@code Task} that is completed when any of the given {@code Task}s complete - */ - default Task> anyOf(Task... tasks) { - return this.anyOf(Arrays.asList(tasks)); - } - - /** - * Creates a durable timer that expires after the specified delay. - * - *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, - * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, - * it may be visible in framework logs and the stored history state. - * - * @param name of the timer - * @param delay the amount of time before the timer should expire - * @return a new {@code Task} that completes after the specified delay - */ - Task createTimer(String name, Duration delay); - - /** - * Creates a durable timer that expires after the specified delay. - * - *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, - * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, - * it may be visible in framework logs and the stored history state.

- * - * @param delay the amount of time before the timer should expire - * @return a new {@code Task} that completes after the specified delay - */ - Task createTimer(Duration delay); - - /** - * Creates a durable timer that expires after the specified timestamp with specific zone. - * - *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, - * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, - * it may be visible in framework logs and the stored history state.

- * - * @param zonedDateTime timestamp with specific zone when the timer should expire - * @return a new {@code Task} that completes after the specified delay - */ - Task createTimer(ZonedDateTime zonedDateTime); - - /** - * Creates a durable timer that expires after the specified timestamp with specific zone. - * - *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, - * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, - * it may be visible in framework logs and the stored history state. - * - * @param name for the timer - * @param zonedDateTime timestamp with specific zone when the timer should expire - * @return a new {@code Task} that completes after the specified delay - */ - Task createTimer(String name, ZonedDateTime zonedDateTime); - - /** - * Transitions the orchestration into the {@link OrchestrationRuntimeStatus#COMPLETED} state with the given output. - * - * @param output the serializable output of the completed orchestration - */ - void complete(Object output); - - /** - * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} - * that completes when the activity completes. If the activity completes successfully, the returned {@code Task}'s - * value will be the activity's output. If the activity fails, the returned {@code Task} will complete exceptionally - * with a {@link TaskFailedException}. - * - *

Activities are the basic unit of work in a durable task orchestration. Unlike orchestrators, which are not - * allowed to do any I/O or call non-deterministic APIs, activities have no implementation restrictions.

- * - *

An activity may execute in the local machine or a remote machine. The exact behavior depends on the underlying - * storage provider, which is responsible for distributing tasks across machines. In general, you should never make - * any assumptions about where an activity will run. You should also assume at-least-once execution guarantees for - * activities, meaning that an activity may be executed twice if, for example, there is a process failure before - * the activities result is saved into storage.

- * - *

Both the inputs and outputs of activities are serialized and stored in durable storage. It's highly recommended - * to not include any sensitive data in activity inputs or outputs. It's also recommended to not use large payloads - * for activity inputs and outputs, which can result in expensive serialization and network utilization. For data - * that cannot be cheaply or safely persisted to storage, it's recommended to instead pass references - * (for example, a URL to a storage blog) to the data and have activities fetch the data directly as part of their - * implementation.

- * - * @param name the name of the activity to call - * @param input the serializable input to pass to the activity - * @param options additional options that control the execution and processing of the activity - * @param returnType the expected class type of the activity output - * @param the expected type of the activity output - * @return a new {@link Task} that completes when the activity completes or fails - */ - Task callActivity(String name, Object input, TaskOptions options, Class returnType); - - /** - * Asynchronously invokes an activity by name and returns a new {@link Task} that completes when the activity - * completes. See {@link #callActivity(String, Object, TaskOptions, Class)} for a complete description. - * - * @param name the name of the activity to call - * @return a new {@link Task} that completes when the activity completes or fails - * @see #callActivity(String, Object, TaskOptions, Class) - */ - default Task callActivity(String name) { - return this.callActivity(name, Void.class); - } - - /** - * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} - * that completes when the activity completes. See {@link #callActivity(String, Object, TaskOptions, Class)} for a - * complete description. - * - * @param name the name of the activity to call - * @param input the serializable input to pass to the activity - * @return a new {@link Task} that completes when the activity completes or fails - */ - default Task callActivity(String name, Object input) { - return this.callActivity(name, input, null, Void.class); - } - - /** - * Asynchronously invokes an activity by name and returns a new {@link Task} that completes when the activity - * completes. If the activity completes successfully, the returned {@code Task}'s value will be the activity's - * output. See {@link #callActivity(String, Object, TaskOptions, Class)} for a complete description. - * - * @param name the name of the activity to call - * @param returnType the expected class type of the activity output - * @param the expected type of the activity output - * @return a new {@link Task} that completes when the activity completes or fails - */ - default Task callActivity(String name, Class returnType) { - return this.callActivity(name, null, null, returnType); - } - - /** - * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} - * that completes when the activity completes.If the activity completes successfully, the returned {@code Task}'s - * value will be the activity's output. See {@link #callActivity(String, Object, TaskOptions, Class)} for a - * complete description. - * - * @param name the name of the activity to call - * @param input the serializable input to pass to the activity - * @param returnType the expected class type of the activity output - * @param the expected type of the activity output - * @return a new {@link Task} that completes when the activity completes or fails - */ - default Task callActivity(String name, Object input, Class returnType) { - return this.callActivity(name, input, null, returnType); - } - - /** - * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} - * that completes when the activity completes. See {@link #callActivity(String, Object, TaskOptions, Class)} for a - * complete description. - * - * @param name the name of the activity to call - * @param input the serializable input to pass to the activity - * @param options additional options that control the execution and processing of the activity - * @return a new {@link Task} that completes when the activity completes or fails - */ - default Task callActivity(String name, Object input, TaskOptions options) { - return this.callActivity(name, input, options, Void.class); - } - - /** - * Restarts the orchestration with a new input and clears its history. See {@link #continueAsNew(Object, boolean)} - * for a full description. - * - * @param input the serializable input data to re-initialize the instance with - */ - default void continueAsNew(Object input) { - this.continueAsNew(input, true); - } - - /** - * Restarts the orchestration with a new input and clears its history. - * - *

This method is primarily designed for eternal orchestrations, which are orchestrations that - * may not ever complete. It works by restarting the orchestration, providing it with a new input, - * and truncating the existing orchestration history. It allows an orchestration to continue - * running indefinitely without having its history grow unbounded. The benefits of periodically - * truncating history include decreased memory usage, decreased storage volumes, and shorter orchestrator - * replays when rebuilding state.

- * - *

The results of any incomplete tasks will be discarded when an orchestrator calls {@code continueAsNew}. - * For example, if a timer is scheduled and then {@code continueAsNew} is called before the timer fires, the timer - * event will be discarded. The only exception to this is external events. By default, if an external event is - * received by an orchestration but not yet processed, the event is saved in the orchestration state unit it is - * received by a call to {@link #waitForExternalEvent}. These events will remain in memory - * even after an orchestrator restarts using {@code continueAsNew}. This behavior can be disabled by specifying - * {@code false} for the {@code preserveUnprocessedEvents} parameter value.

- * - *

Orchestrator implementations should complete immediately after calling the{@code continueAsNew} method.

- * - * @param input the serializable input data to re-initialize the instance with - * @param preserveUnprocessedEvents {@code true} to push unprocessed external events into the new orchestration - * history, otherwise {@code false} - */ - void continueAsNew(Object input, boolean preserveUnprocessedEvents); - - /** - * Check if the given patch name can be applied to the orchestration. - * - * @param patchName The name of the patch to check. - * @return True if the given patch name can be applied to the orchestration, False otherwise. - */ - - boolean isPatched(String patchName); - - /** - * Create a new Uuid that is safe for replay within an orchestration or operation. - * - *

The default implementation of this method creates a name-based Uuid - * using the algorithm from RFC 4122 §4.3. The name input used to generate - * this value is a combination of the orchestration instance ID and an - * internally managed sequence number. - *

- * - * @return a deterministic Uuid - */ - default UUID newUuid() { - throw new RuntimeException("No implementation found."); - } - - /** - * Sends an external event to another orchestration instance. - * - * @param instanceID the unique ID of the receiving orchestration instance. - * @param eventName the name of the event to send - */ - default void sendEvent(String instanceID, String eventName) { - this.sendEvent(instanceID, eventName, null); - } - - /** - * Sends an external event to another orchestration instance. - * - * @param instanceId the unique ID of the receiving orchestration instance. - * @param eventName the name of the event to send - * @param eventData the payload of the event to send - */ - void sendEvent(String instanceId, String eventName, Object eventData); - - /** - * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes - * when the sub-orchestration completes. - * - *

See {@link #callSubOrchestrator(String, Object, String, TaskOptions, Class)} for a full description.

- * - * @param name the name of the orchestrator to invoke - * @return a new {@link Task} that completes when the sub-orchestration completes or fails - * @see #callSubOrchestrator(String, Object, String, TaskOptions, Class) - */ - default Task callSubOrchestrator(String name) { - return this.callSubOrchestrator(name, null); - } - - /** - * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes - * when the sub-orchestration completes. - * - *

See {@link #callSubOrchestrator(String, Object, String, TaskOptions, Class)} for a full description.

- * - * @param name the name of the orchestrator to invoke - * @param input the serializable input to send to the sub-orchestration - * @return a new {@link Task} that completes when the sub-orchestration completes or fails - */ - default Task callSubOrchestrator(String name, Object input) { - return this.callSubOrchestrator(name, input, Void.class); - } - - /** - * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes - * when the sub-orchestration completes. - * - *

See {@link #callSubOrchestrator(String, Object, String, TaskOptions, Class)} for a full description.

- * - * @param name the name of the orchestrator to invoke - * @param input the serializable input to send to the sub-orchestration - * @param returnType the expected class type of the sub-orchestration output - * @param the expected type of the sub-orchestration output - * @return a new {@link Task} that completes when the sub-orchestration completes or fails - */ - default Task callSubOrchestrator(String name, Object input, Class returnType) { - return this.callSubOrchestrator(name, input, null, returnType); - } - - /** - * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes - * when the sub-orchestration completes. - * - *

See {@link #callSubOrchestrator(String, Object, String, TaskOptions, Class)} for a full description.

- * - * @param name the name of the orchestrator to invoke - * @param input the serializable input to send to the sub-orchestration - * @param instanceID the unique ID of the sub-orchestration - * @param returnType the expected class type of the sub-orchestration output - * @param the expected type of the sub-orchestration output - * @return a new {@link Task} that completes when the sub-orchestration completes or fails - */ - default Task callSubOrchestrator(String name, Object input, String instanceID, Class returnType) { - return this.callSubOrchestrator(name, input, instanceID, null, returnType); - } - - /** - * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes - * when the sub-orchestration completes. - * - *

See {@link #callSubOrchestrator(String, Object, String, TaskOptions, Class)} for a full description.

- * - * @param name the name of the orchestrator to invoke - * @param input the serializable input to send to the sub-orchestration - * @param instanceID the unique ID of the sub-orchestration - * @param options additional options that control the execution and processing of the activity - * @return a new {@link Task} that completes when the sub-orchestration completes or fails - */ - default Task callSubOrchestrator(String name, Object input, String instanceID, TaskOptions options) { - return this.callSubOrchestrator(name, input, instanceID, options, Void.class); - } - - /** - * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes - * when the sub-orchestration completes. If the sub-orchestration completes successfully, the returned - * {@code Task}'s value will be the activity's output. If the sub-orchestration fails, the returned {@code Task} - * will complete exceptionally with a {@link TaskFailedException}. - * - *

A sub-orchestration has its own instance ID, history, and status that is independent of the parent orchestrator - * that started it. There are many advantages to breaking down large orchestrations into sub-orchestrations:

- *
    - *
  • - * Splitting large orchestrations into a series of smaller sub-orchestrations can make code more maintainable. - *
  • - *
  • - * Distributing orchestration logic across multiple compute nodes concurrently is useful if - * orchestration logic otherwise needs to coordinate a lot of tasks. - *
  • - *
  • - * Memory usage and CPU overhead can be reduced by keeping the history of parent orchestrations smaller. - *
  • - *
- * - *

The disadvantage is that there is overhead associated with starting a sub-orchestration and processing its - * output. This is typically only an issue for very small orchestrations.

- * - *

Because sub-orchestrations are independent of their parents, terminating a parent orchestration does not affect - * any sub-orchestrations. Sub-orchestrations must be terminated independently using their unique instance ID, - * which is specified using the {@code instanceID} parameter.

- * - * @param name the name of the orchestrator to invoke - * @param input the serializable input to send to the sub-orchestration - * @param instanceID the unique ID of the sub-orchestration - * @param options additional options that control the execution and processing of the activity - * @param returnType the expected class type of the sub-orchestration output - * @param the expected type of the sub-orchestration output - * @return a new {@link Task} that completes when the sub-orchestration completes or fails - */ - Task callSubOrchestrator( - String name, - @Nullable Object input, - @Nullable String instanceID, - @Nullable TaskOptions options, - Class returnType); - - /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received or is canceled when {@code timeout} expires. - * - *

External clients can raise events to a waiting orchestration instance using the - * {@link DurableTaskClient#raiseEvent} method.

- * - *

If the current orchestration is not yet waiting for an event named {@code name}, then the event will be saved in - * the orchestration instance state and dispatched immediately when this method is called. This event saving occurs - * even if the current orchestrator cancels the wait operation before the event is received.

- * - *

Orchestrators can wait for the same event name multiple times, so waiting for multiple events with the same name - * is allowed. Each external event received by an orchestrator will complete just one task returned by this method. - *

- * - * @param name the case-insensitive name of the event to wait for - * @param timeout the amount of time to wait before canceling the returned {@code Task} - * @param dataType the expected class type of the event data payload - * @param the expected type of the event data payload - * @return a new {@link Task} that completes when the external event is received or when {@code timeout} expires - * @throws TaskCanceledException if the specified {@code timeout} value expires before the event is received - */ - Task waitForExternalEvent(String name, Duration timeout, Class dataType) throws TaskCanceledException; - - /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received or is canceled when {@code timeout} expires. - * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description.

- * - * @param name the case-insensitive name of the event to wait for - * @param timeout the amount of time to wait before canceling the returned {@code Task} - * @return a new {@link Task} that completes when the external event is received or when {@code timeout} expires - * @throws TaskCanceledException if the specified {@code timeout} value expires before the event is received - */ - default Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException { - return this.waitForExternalEvent(name, timeout, Void.class); - } - - /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received. - * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description.

- * - * @param name the case-insensitive name of the event to wait for - * @return a new {@link Task} that completes when the external event is received - */ - default Task waitForExternalEvent(String name) { - return this.waitForExternalEvent(name, Void.class); - } - - /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received. - * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description.

- * - * @param name the case-insensitive name of the event to wait for - * @param dataType the expected class type of the event data payload - * @param the expected type of the event data payload - * @return a new {@link Task} that completes when the external event is received - */ - default Task waitForExternalEvent(String name, Class dataType) { - try { - return this.waitForExternalEvent(name, null, dataType); - } catch (TaskCanceledException e) { - // This should never happen because of the max duration - throw new RuntimeException("An unexpected exception was throw while waiting for an external event.", e); - } - } - - /** - * Assigns a custom status value to the current orchestration. - * - *

The {@code customStatus} value is serialized and stored in orchestration state and will be made available to the - * orchestration status query APIs, such as {@link DurableTaskClient#getInstanceMetadata}. The serialized value - * must not exceed 16 KB of UTF-16 encoded text.

- * - *

Use {@link #clearCustomStatus()} to remove the custom status value from the orchestration state.

- * - * @param customStatus A serializable value to assign as the custom status value. - */ - void setCustomStatus(Object customStatus); - - /** - * Clears the orchestration's custom status. - */ - void clearCustomStatus(); - - /** - * Gets the propagated history from a parent workflow, if any was propagated. - * - * @return an Optional containing the propagated history, or empty if none was propagated - */ - Optional getPropagatedHistory(); -} diff --git a/examples/src/main/java/io/dapr/examples/unittesting/DaprWorkflowExampleTest.java b/examples/src/main/java/io/dapr/examples/unittesting/DaprWorkflowExampleTest.java index b8ce0ef67c..2f5f00aab3 100644 --- a/examples/src/main/java/io/dapr/examples/unittesting/DaprWorkflowExampleTest.java +++ b/examples/src/main/java/io/dapr/examples/unittesting/DaprWorkflowExampleTest.java @@ -13,8 +13,8 @@ package io.dapr.examples.unittesting; -import io.dapr.durabletask.Task; -import io.dapr.durabletask.TaskCanceledException; +import io.dapr.workflows.task.Task; +import io.dapr.workflows.task.exception.TaskCanceledException; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowContext; import io.dapr.workflows.WorkflowStub; diff --git a/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java index 1d4efbdfe7..dd89a253af 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/childworkflow/DemoChildWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.examples.workflows.childworkflow; -import io.dapr.durabletask.interruption.OrchestratorBlockedException; +import io.dapr.workflows.task.interruption.OrchestratorBlockedException; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java index f375363edd..4b4f9d7e6d 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.examples.workflows.compensation; -import io.dapr.durabletask.TaskFailedException; +import io.dapr.workflows.task.exception.TaskFailedException; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; diff --git a/examples/src/main/java/io/dapr/examples/workflows/faninout/DemoFanInOutWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/faninout/DemoFanInOutWorkflow.java index 611b1cac67..f45c245951 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/faninout/DemoFanInOutWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/faninout/DemoFanInOutWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.examples.workflows.faninout; -import io.dapr.durabletask.Task; +import io.dapr.workflows.task.Task; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; diff --git a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/AuditActivity.java b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/AuditActivity.java index a1f3816d64..9d03697dba 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/AuditActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/AuditActivity.java @@ -13,9 +13,9 @@ package io.dapr.examples.workflows.historypropagation; -import io.dapr.durabletask.ChildWorkflowResult; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.WorkflowResult; +import io.dapr.workflows.task.history.ChildWorkflowResult; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.WorkflowResult; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.WorkflowActivityContext; diff --git a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/DemoFraudCheckChildWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/DemoFraudCheckChildWorkflow.java index e0be46a53c..06356406ab 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/DemoFraudCheckChildWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/DemoFraudCheckChildWorkflow.java @@ -13,8 +13,8 @@ package io.dapr.examples.workflows.historypropagation; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.WorkflowResult; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.WorkflowResult; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; diff --git a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App1Workflow.java b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App1Workflow.java index a4142afc60..92a5fdeeab 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App1Workflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App1Workflow.java @@ -13,7 +13,7 @@ package io.dapr.examples.workflows.historypropagation.multiapp; -import io.dapr.durabletask.HistoryPropagationScope; +import io.dapr.workflows.task.history.HistoryPropagationScope; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; diff --git a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App2AuditActivity.java b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App2AuditActivity.java index 65810a26bb..ce48fe5e76 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App2AuditActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/historypropagation/multiapp/App2AuditActivity.java @@ -13,8 +13,8 @@ package io.dapr.examples.workflows.historypropagation.multiapp; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.WorkflowResult; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.WorkflowResult; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.WorkflowActivityContext; diff --git a/pom.xml b/pom.xml index cb5ae7de29..2c45bb79b6 100644 --- a/pom.xml +++ b/pom.xml @@ -752,7 +752,6 @@ examples testcontainers-dapr - durabletask-client @@ -762,7 +761,7 @@ sdk-tests spring-boot-examples spring-boot-sdk-tests - durabletask-client + sdk-workflows diff --git a/sdk-bom/pom.xml b/sdk-bom/pom.xml index f3aae9bcc9..6990e82aad 100644 --- a/sdk-bom/pom.xml +++ b/sdk-bom/pom.xml @@ -129,11 +129,6 @@ testcontainers-dapr ${dapr.sdk.version} - - io.dapr - durabletask-client - ${dapr.sdk.version} - diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestExecutionKeysWorkflow.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestExecutionKeysWorkflow.java index 65eb1047c4..4a43223ec5 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestExecutionKeysWorkflow.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestExecutionKeysWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.it.testcontainers.workflows; -import io.dapr.durabletask.Task; +import io.dapr.workflows.task.Task; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index 274c989424..5d1842f6b9 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -1,7 +1,7 @@ - + + 4.0.0 @@ -16,12 +16,84 @@ dapr-sdk-workflows SDK for Workflows on Dapr + + false + ${project.build.directory}/generated-sources + ${project.build.directory}/proto + + 60% + + io.dapr dapr-sdk ${project.parent.version} + + javax.annotation + javax.annotation-api + provided + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + io.grpc + grpc-netty + + + com.google.protobuf + protobuf-java + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.apache.commons + commons-lang3 + + + io.micrometer + micrometer-observation + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-context + + + + org.junit.jupiter + junit-jupiter + test + org.mockito mockito-core @@ -33,22 +105,18 @@ test - org.junit.jupiter - junit-jupiter + io.grpc + grpc-testing test - io.dapr - durabletask-client - ${project.parent.version} + org.testcontainers + testcontainers + test io.opentelemetry - opentelemetry-api - - - io.grpc - grpc-testing + opentelemetry-sdk test @@ -59,13 +127,130 @@ org.sonatype.plugins nexus-staging-maven-plugin + + org.apache.maven.plugins + maven-failsafe-plugin + + ${project.build.outputDirectory} + + + + com.googlecode.maven-download-plugin + download-maven-plugin + ${download-maven-plugin.version} + + + getOrchestratorServiceProto + initialize + + wget + + + true + ${durabletask.proto.baseurl}/orchestrator_service.proto + orchestrator_service.proto + ${protobuf.input.directory} + + + + getOrchestrationProto + initialize + + wget + + + true + ${durabletask.proto.baseurl}/orchestration.proto + orchestration.proto + ${protobuf.input.directory} + + + + getHistoryEventsProto + initialize + + wget + + + true + ${durabletask.proto.baseurl}/history_events.proto + history_events.proto + ${protobuf.input.directory} + + + + getOrchestratorActionsProto + initialize + + wget + + + true + ${durabletask.proto.baseurl}/orchestrator_actions.proto + orchestrator_actions.proto + ${protobuf.input.directory} + + + + getAttestationProto + initialize + + wget + + + true + ${durabletask.proto.baseurl}/attestation.proto + attestation.proto + ${protobuf.input.directory} + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-maven-plugin.version} + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + ${protobuf.input.directory} + + + + + compile + compile-custom + + + + org.apache.maven.plugins maven-source-plugin + + + attach-sources + + jar-no-fork + + + org.apache.maven.plugins maven-javadoc-plugin + + true + + + + attach-javadocs + + jar + + + org.jacoco diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivity.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivity.java index 5f8c45292a..73ee839745 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivity.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivity.java @@ -29,8 +29,8 @@ *

Because activities only guarantee at least once execution, it's recommended that activity logic be implemented as * idempotent whenever possible. * - *

Activities are scheduled by orchestrators using one of the {@link io.dapr.workflows.WorkflowContext#callActivity} - * method overloads. + *

Activities are scheduled by orchestrators using one of the + * {@link io.dapr.workflows.WorkflowContext#callActivity} method overloads. */ public interface WorkflowActivity { /** diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java index 91d691525c..da72212821 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java @@ -13,7 +13,7 @@ package io.dapr.workflows; -import io.dapr.durabletask.PropagatedHistory; +import io.dapr.workflows.task.history.PropagatedHistory; import org.slf4j.Logger; import java.util.Optional; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java index fd7c8a6d92..a9f0fb13b1 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Dapr Authors + * Copyright 2026 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -13,14 +13,17 @@ package io.dapr.workflows; -import io.dapr.durabletask.CompositeTaskFailedException; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.Task; -import io.dapr.durabletask.TaskCanceledException; -import io.dapr.durabletask.TaskFailedException; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.task.Task; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.exception.CompositeTaskFailedException; +import io.dapr.workflows.task.exception.TaskCanceledException; +import io.dapr.workflows.task.exception.TaskFailedException; +import io.dapr.workflows.task.history.PropagatedHistory; import org.slf4j.Logger; import javax.annotation.Nullable; + import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; @@ -30,34 +33,51 @@ import java.util.UUID; /** - * Context object used by workflow implementations to perform actions such as scheduling activities, - * durable timers, waiting for external events, and for getting basic information about the current - * workflow instance. + * Used by orchestrators to perform actions such as scheduling tasks, durable timers, waiting for external events, + * and for getting basic information about the current orchestration. */ public interface WorkflowContext { - /** - * Get a logger only when {@code isReplaying} is false. - * Otherwise, return a NOP (no operation) logger. + * Gets a logger for this workflow. + * + *

The returned logger is a no-op logger while the workflow is replaying, so that log + * statements in workflow code are emitted once rather than on every replay. * - * @return Logger + * @return a logger that is silenced during replay */ Logger getLogger(); /** - * Gets the name of the current workflow. + * Gets the name of the current task orchestration. * - * @return the name of the current workflow + * @return the name of the current task orchestration */ String getName(); /** - * Gets the instance ID of the current workflow. + * Gets the deserialized input of the current task orchestration. + * + * @param targetType the {@link Class} object associated with {@code V} + * @param the expected type of the orchestrator input + * @return the deserialized input as an object of type {@code V} or {@code null} if no input was provided. + */ + V getInput(Class targetType); + + /** + * Gets the unique ID of the current orchestration instance. * - * @return the instance ID of the current workflow + * @return the unique ID of the current orchestration instance */ String getInstanceId(); + /** + * Gets the app ID of the current orchestration instance, if available. + * This is used for cross-app workflow routing. + * + * @return the app ID of the current orchestration instance, or null if not available + */ + String getAppId(); + /** * Gets the current orchestration time in UTC. * @@ -66,77 +86,182 @@ public interface WorkflowContext { Instant getCurrentInstant(); /** - * Completes the current workflow. + * Gets a value indicating whether the orchestrator is currently replaying a previous execution. * - * @param output the serializable output of the completed Workflow. + *

Orchestrator functions are "replayed" after being unloaded from memory to reconstruct local variable state. + * During a replay, previously executed tasks will be completed automatically with previously seen values + * that are stored in the orchestration history. One the orchestrator reaches the point in the orchestrator + * where it's no longer replaying existing history, this method will return {@code false}.

+ * + *

You can use this method if you have logic that needs to run only when not replaying. For example, + * certain types of application logging may become too noisy when duplicated as part of replay. The + * application code could check to see whether the function is being replayed and then issue the log statements + * when this value is {@code false}.

+ * + * @return {@code true} if the orchestrator is replaying, otherwise {@code false} */ - void complete(Object output); + boolean isReplaying(); /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received or is canceled when {@code timeout} expires. + * Returns a new {@code Task} that is completed when all tasks in {@code tasks} completes. + * See {@link #allOf(Task[])} for more detailed information. * - *

If the current orchestration is not yet waiting for an event named {@code name}, then the event will be saved in - * the orchestration instance state and dispatched immediately when this method is called. This event saving occurs - * even if the current orchestrator cancels the wait operation before the event is received. + * @param tasks the list of {@code Task} objects + * @param the return type of the {@code Task} objects + * @return a new {@code Task} that is completed when any of the given {@code Task}s complete + * @see #allOf(Task[]) + */ + Task> allOf(List> tasks); + + // TODO: Update the description of allOf to be more specific about the exception behavior. + + // https://github.io.dapr.workflows.task-java/issues/54 + + /** + * Returns a new {@code Task} that is completed when all the given {@code Task}s complete. If any of the given + * {@code Task}s complete with an exception, the returned {@code Task} will also complete with + * an {@link CompositeTaskFailedException} containing details of the first encountered failure. + * The value of the returned {@code Task} is an ordered list of + * the return values of the given tasks. If no tasks are provided, returns a {@code Task} completed with value + * {@code null}. * - *

Orchestrators can wait for the same event name multiple times, so waiting for multiple events with the same name - * is allowed. Each external event received by an orchestrator will complete just one task returned by this method. + *

This method is useful for awaiting the completion of a set of independent tasks before continuing to the next + * step in the orchestration, as in the following example:

+ *
{@code
+   * Task t1 = ctx.callActivity("MyActivity", String.class);
+   * Task t2 = ctx.callActivity("MyActivity", String.class);
+   * Task t3 = ctx.callActivity("MyActivity", String.class);
    *
-   * @param name     the case-insensitive name of the event to wait for
-   * @param timeout  the amount of time to wait before canceling the returned {@code Task}
-   * @param dataType the expected class type of the event data payload
-   * @param       the expected type of the event data payload
-   * @return a new {@link Task} that completes when the external event is received or when {@code timeout} expires
-   * @throws TaskCanceledException if the specified {@code timeout} value expires before the event is received
+   * List orderedResults = ctx.allOf(t1, t2, t3).await();
+   * }
+ * + *

Exceptions in any of the given tasks results in an unchecked {@link CompositeTaskFailedException}. + * This exception can be inspected to obtain failure details of individual {@link Task}s.

+ *
{@code
+   * try {
+   *     List orderedResults = ctx.allOf(t1, t2, t3).await();
+   * } catch (CompositeTaskFailedException e) {
+   *     List exceptions = e.getExceptions()
+   * }
+   * }
+ * + * @param tasks the {@code Task}s + * @param the return type of the {@code Task} objects + * @return the values of the completed {@code Task} objects in the same order as the source list */ - Task waitForExternalEvent(String name, Duration timeout, Class dataType) throws TaskCanceledException; + default Task> allOf(Task... tasks) { + return this.allOf(Arrays.asList(tasks)); + } /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received or is canceled when {@code timeout} expires. + * Returns a new {@code Task} that is completed when any of the tasks in {@code tasks} completes. + * See {@link #anyOf(Task[])} for more detailed information. * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description. + * @param tasks the list of {@code Task} objects + * @return a new {@code Task} that is completed when any of the given {@code Task}s complete + * @see #anyOf(Task[]) + */ + Task> anyOf(List> tasks); + + /** + * Returns a new {@code Task} that is completed when any of the given {@code Task}s complete. The value of the + * new {@code Task} is a reference to the completed {@code Task} object. If no tasks are provided, returns a + * {@code Task} that never completes. * - * @param name the case-insensitive name of the event to wait for - * @param timeout the amount of time to wait before canceling the returned {@code Task} - * @param the expected type of the event data payload - * @return a new {@link Task} that completes when the external event is received or when {@code timeout} expires - * @throws TaskCanceledException if the specified {@code timeout} value expires before the event is received + *

This method is useful for waiting on multiple concurrent tasks and performing a task-specific operation when the + * first task completes, as in the following example:

+ *
{@code
+   * Task event1 = ctx.waitForExternalEvent("Event1");
+   * Task event2 = ctx.waitForExternalEvent("Event2");
+   * Task event3 = ctx.waitForExternalEvent("Event3");
+   *
+   * Task winner = ctx.anyOf(event1, event2, event3).await();
+   * if (winner == event1) {
+   *     // ...
+   * } else if (winner == event2) {
+   *     // ...
+   * } else if (winner == event3) {
+   *     // ...
+   * }
+   * }
+ * + *

The {@code anyOf} method can also be used for implementing long-running timeouts, as in the following example: + *

+ *
{@code
+   * Task activityTask = ctx.callActivity("SlowActivity");
+   * Task timeoutTask = ctx.createTimer(Duration.ofMinutes(30));
+   *
+   * Task winner = ctx.anyOf(activityTask, timeoutTask).await();
+   * if (winner == activityTask) {
+   *     // completion case
+   * } else {
+   *     // timeout case
+   * }
+   * }
+ * + * @param tasks the list of {@code Task} objects + * @return a new {@code Task} that is completed when any of the given {@code Task}s complete */ - Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException; + default Task> anyOf(Task... tasks) { + return this.anyOf(Arrays.asList(tasks)); + } /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received. + * Creates a durable timer that expires after the specified delay. * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description. + *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, + * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, + * it may be visible in framework logs and the stored history state. * - * @param name the case-insensitive name of the event to wait for - * @param the expected type of the event data payload - * @return a new {@link Task} that completes when the external event is received + * @param name of the timer + * @param delay the amount of time before the timer should expire + * @return a new {@code Task} that completes after the specified delay */ - Task waitForExternalEvent(String name) throws TaskCanceledException; + Task createTimer(String name, Duration delay); /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is - * received. + * Creates a durable timer that expires after the specified delay. * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description. + *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, + * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, + * it may be visible in framework logs and the stored history state.

* - * @param name the case-insensitive name of the event to wait for - * @param dataType the expected class type of the event data payload - * @param the expected type of the event data payload - * @return a new {@link Task} that completes when the external event is received + * @param delay the amount of time before the timer should expire + * @return a new {@code Task} that completes after the specified delay */ - default Task waitForExternalEvent(String name, Class dataType) { - try { - return this.waitForExternalEvent(name, null, dataType); - } catch (TaskCanceledException e) { - // This should never happen because of the max duration - throw new RuntimeException("An unexpected exception was throw while waiting for an external event.", e); - } - } + Task createTimer(Duration delay); + + /** + * Creates a durable timer that expires after the specified timestamp with specific zone. + * + *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, + * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, + * it may be visible in framework logs and the stored history state.

+ * + * @param zonedDateTime timestamp with specific zone when the timer should expire + * @return a new {@code Task} that completes after the specified delay + */ + Task createTimer(ZonedDateTime zonedDateTime); + + /** + * Creates a durable timer that expires after the specified timestamp with specific zone. + * + *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, + * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, + * it may be visible in framework logs and the stored history state. + * + * @param name for the timer + * @param zonedDateTime timestamp with specific zone when the timer should expire + * @return a new {@code Task} that completes after the specified delay + */ + Task createTimer(String name, ZonedDateTime zonedDateTime); + + /** + * Transitions the orchestration into the {@link WorkflowRuntimeStatus#COMPLETED} state with the given output. + * + * @param output the serializable output of the completed orchestration + */ + void complete(Object output); /** * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} @@ -144,6 +269,22 @@ default Task waitForExternalEvent(String name, Class dataType) { * value will be the activity's output. If the activity fails, the returned {@code Task} will complete exceptionally * with a {@link TaskFailedException}. * + *

Activities are the basic unit of work in a durable task orchestration. Unlike orchestrators, which are not + * allowed to do any I/O or call non-deterministic APIs, activities have no implementation restrictions.

+ * + *

An activity may execute in the local machine or a remote machine. The exact behavior depends on the underlying + * storage provider, which is responsible for distributing tasks across machines. In general, you should never make + * any assumptions about where an activity will run. You should also assume at-least-once execution guarantees for + * activities, meaning that an activity may be executed twice if, for example, there is a process failure before + * the activities result is saved into storage.

+ * + *

Both the inputs and outputs of activities are serialized and stored in durable storage. It's highly recommended + * to not include any sensitive data in activity inputs or outputs. It's also recommended to not use large payloads + * for activity inputs and outputs, which can result in expensive serialization and network utilization. For data + * that cannot be cheaply or safely persisted to storage, it's recommended to instead pass references + * (for example, a URL to a storage blog) to the data and have activities fetch the data directly as part of their + * implementation.

+ * * @param name the name of the activity to call * @param input the serializable input to pass to the activity * @param options additional options that control the execution and processing of the activity @@ -162,13 +303,14 @@ default Task waitForExternalEvent(String name, Class dataType) { * @see #callActivity(String, Object, WorkflowTaskOptions, Class) */ default Task callActivity(String name) { - return this.callActivity(name, null, null, Void.class); + return this.callActivity(name, Void.class); } /** * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} * that completes when the activity completes. - * See {@link #callActivity(String, Object, WorkflowTaskOptions, Class)} for a complete description. + * See {@link #callActivity(String, Object, WorkflowTaskOptions, Class)} for a + * complete description. * * @param name the name of the activity to call * @param input the serializable input to pass to the activity @@ -195,8 +337,8 @@ default Task callActivity(String name, Class returnType) { /** * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} * that completes when the activity completes.If the activity completes successfully, the returned {@code Task}'s - * value will be the activity's output. - * See {@link #callActivity(String, Object, WorkflowTaskOptions, Class)} for a complete description. + * value will be the activity's output. See {@link #callActivity(String, Object, WorkflowTaskOptions, Class)} for a + * complete description. * * @param name the name of the activity to call * @param input the serializable input to pass to the activity @@ -211,7 +353,8 @@ default Task callActivity(String name, Object input, Class returnType) /** * Asynchronously invokes an activity by name and with the specified input value and returns a new {@link Task} * that completes when the activity completes. - * See {@link #callActivity(String, Object, WorkflowTaskOptions, Class)} for a complete description. + * See {@link #callActivity(String, Object, WorkflowTaskOptions, Class)} for a + * complete description. * * @param name the name of the activity to call * @param input the serializable input to pass to the activity @@ -223,149 +366,92 @@ default Task callActivity(String name, Object input, WorkflowTaskOptions o } /** - * Gets a value indicating whether the workflow is currently replaying a previous execution. - * - *

Workflow functions are "replayed" after being unloaded from memory to reconstruct local variable state. - * During a replay, previously executed tasks will be completed automatically with previously seen values - * that are stored in the workflow history. Once the workflow reaches the point where it's no longer - * replaying existing history, this method will return {@code false}. - * - *

You can use this method if you have logic that needs to run only when not replaying. For example, - * certain types of application logging may become too noisy when duplicated as part of replay. The - * application code could check to see whether the function is being replayed and then issue the log statements - * when this value is {@code false}. + * Restarts the orchestration with a new input and clears its history. See {@link #continueAsNew(Object, boolean)} + * for a full description. * - * @return {@code true} if the workflow is replaying, otherwise {@code false} + * @param input the serializable input data to re-initialize the instance with */ - boolean isReplaying(); + default void continueAsNew(Object input) { + this.continueAsNew(input, true); + } /** - * Returns a new {@code Task} that is completed when all the given {@code Task}s complete. If any of the given - * {@code Task}s complete with an exception, the returned {@code Task} will also complete with an - * {@link CompositeTaskFailedException} containing details of the first encountered failure. - * The value of the returned {@code Task} is an ordered list of the return values of the given tasks. - * If no tasks are provided, returns a {@code Task} completed with value - * {@code null}. + * Restarts the orchestration with a new input and clears its history. * - *

This method is useful for awaiting the completion of a set of independent tasks before continuing to the next - * step in the orchestration, as in the following example: - *

{@code
-   * Task t1 = ctx.callActivity("MyActivity", String.class);
-   * Task t2 = ctx.callActivity("MyActivity", String.class);
-   * Task t3 = ctx.callActivity("MyActivity", String.class);
+   * 

This method is primarily designed for eternal orchestrations, which are orchestrations that + * may not ever complete. It works by restarting the orchestration, providing it with a new input, + * and truncating the existing orchestration history. It allows an orchestration to continue + * running indefinitely without having its history grow unbounded. The benefits of periodically + * truncating history include decreased memory usage, decreased storage volumes, and shorter orchestrator + * replays when rebuilding state.

* - * List orderedResults = ctx.allOf(List.of(t1, t2, t3)).await(); - * }
+ *

The results of any incomplete tasks will be discarded when an orchestrator calls {@code continueAsNew}. + * For example, if a timer is scheduled and then {@code continueAsNew} is called before the timer fires, the timer + * event will be discarded. The only exception to this is external events. By default, if an external event is + * received by an orchestration but not yet processed, the event is saved in the orchestration state unit it is + * received by a call to {@link #waitForExternalEvent}. These events will remain in memory + * even after an orchestrator restarts using {@code continueAsNew}. This behavior can be disabled by specifying + * {@code false} for the {@code preserveUnprocessedEvents} parameter value.

* - *

Exceptions in any of the given tasks results in an unchecked {@link CompositeTaskFailedException}. - * This exception can be inspected to obtain failure details of individual {@link Task}s. - *

{@code
-   * try {
-   *     List orderedResults = ctx.allOf(List.of(t1, t2, t3)).await();
-   * } catch (CompositeTaskFailedException e) {
-   *     List exceptions = e.getExceptions()
-   * }
-   * }
+ *

Orchestrator implementations should complete immediately after calling the{@code continueAsNew} method.

* - * @param tasks the list of {@code Task} objects - * @param the return type of the {@code Task} objects - * @return the values of the completed {@code Task} objects in the same order as the source list - * @throws CompositeTaskFailedException if the specified {@code timeout} value expires before the event is received + * @param input the serializable input data to re-initialize the instance with + * @param preserveUnprocessedEvents {@code true} to push unprocessed external events into the new orchestration + * history, otherwise {@code false} */ - Task> allOf(List> tasks) throws CompositeTaskFailedException; + void continueAsNew(Object input, boolean preserveUnprocessedEvents); /** - * Returns a new {@code Task} that is completed when any of the tasks in {@code tasks} completes. - * See {@link #anyOf(Task[])} for more detailed information. + * Check if the given patch name can be applied to the orchestration. * - * @param tasks the list of {@code Task} objects - * @return a new {@code Task} that is completed when any of the given {@code Task}s complete - * @see #anyOf(Task[]) + * @param patchName The name of the patch to check. + * @return True if the given patch name can be applied to the orchestration, False otherwise. */ - Task> anyOf(List> tasks); + boolean isPatched(String patchName); + /** - * Returns a new {@code Task} that is completed when any of the given {@code Task}s complete. The value of the - * new {@code Task} is a reference to the completed {@code Task} object. If no tasks are provided, returns a - * {@code Task} that never completes. - * - *

This method is useful for waiting on multiple concurrent tasks and performing a task-specific operation when the - * first task completes, as in the following example: - *

{@code
-   * Task event1 = ctx.waitForExternalEvent("Event1");
-   * Task event2 = ctx.waitForExternalEvent("Event2");
-   * Task event3 = ctx.waitForExternalEvent("Event3");
+   * Create a new Uuid that is safe for replay within an orchestration or operation.
    *
-   * Task winner = ctx.anyOf(event1, event2, event3).await();
-   * if (winner == event1) {
-   *     // ...
-   * } else if (winner == event2) {
-   *     // ...
-   * } else if (winner == event3) {
-   *     // ...
-   * }
-   * }
- * The {@code anyOf} method can also be used for implementing long-running timeouts, as in the following example: - *
{@code
-   * Task activityTask = ctx.callActivity("SlowActivity");
-   * Task timeoutTask = ctx.createTimer(Duration.ofMinutes(30));
-   *
-   * Task winner = ctx.anyOf(activityTask, timeoutTask).await();
-   * if (winner == activityTask) {
-   *     // completion case
-   * } else {
-   *     // timeout case
-   * }
-   * }
+ *

The default implementation of this method creates a name-based Uuid + * using the algorithm from RFC 4122 §4.3. The name input used to generate + * this value is a combination of the orchestration instance ID and an + * internally managed sequence number. + *

* - * @param tasks the list of {@code Task} objects - * @return a new {@code Task} that is completed when any of the given {@code Task}s complete + * @return a deterministic Uuid */ - default Task> anyOf(Task... tasks) { - return this.anyOf(Arrays.asList(tasks)); + default UUID newUuid() { + throw new RuntimeException("No implementation found."); } /** - * Creates a durable timer that expires after the specified delay. - * - *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, - * internally-managed durable timers. The orchestration code doesn't need to be aware of this behavior. However, - * it may be visible in framework logs and the stored history state. - * - * @param duration the amount of time before the timer should expire - * @return a new {@code Task} that completes after the specified delay - */ - Task createTimer(Duration duration); - - /** - * Creates a durable timer that expires after the specified timestamp with specific zone. - * - *

Specifying a long delay (for example, a delay of a few days or more) may result in the creation of multiple, - * internally-managed timers. The workflow code doesn't need to be aware of this behavior. However, - * it may be visible in framework logs and the stored history state. + * Sends an external event to another orchestration instance. * - * @param zonedDateTime timestamp with specific zone when the timer should expire - * @return a new {@code Task} that completes after the specified delay + * @param instanceID the unique ID of the receiving orchestration instance. + * @param eventName the name of the event to send */ - Task createTimer(ZonedDateTime zonedDateTime); + default void sendEvent(String instanceID, String eventName) { + this.sendEvent(instanceID, eventName, null); + } /** - * Gets the deserialized input of the current task orchestration. + * Sends an external event to another orchestration instance. * - * @param targetType the {@link Class} object associated with {@code V} - * @param the expected type of the workflow input - * @return the deserialized input as an object of type {@code V} or {@code null} if no input was provided. + * @param instanceId the unique ID of the receiving orchestration instance. + * @param eventName the name of the event to send + * @param eventData the payload of the event to send */ - V getInput(Class targetType); + void sendEvent(String instanceId, String eventName, Object eventData); /** - * Asynchronously invokes another workflow as a child-workflow and returns a {@link Task} that completes - * when the child-workflow completes. + * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes + * when the sub-orchestration completes. * - *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description. + *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description.

* - * @param name the name of the workflow to invoke - * @return a new {@link Task} that completes when the child-workflow completes or fails + * @param name the name of the orchestrator to invoke + * @return a new {@link Task} that completes when the sub-orchestration completes or fails * @see #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class) */ default Task callChildWorkflow(String name) { @@ -373,79 +459,79 @@ default Task callChildWorkflow(String name) { } /** - * Asynchronously invokes another workflow as a child-workflow and returns a {@link Task} that completes - * when the child-workflow completes. + * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes + * when the sub-orchestration completes. * - *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description. + *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description.

* - * @param name the name of the workflow to invoke - * @param input the serializable input to send to the child-workflow - * @return a new {@link Task} that completes when the child-workflow completes or fails + * @param name the name of the orchestrator to invoke + * @param input the serializable input to send to the sub-orchestration + * @return a new {@link Task} that completes when the sub-orchestration completes or fails */ default Task callChildWorkflow(String name, Object input) { return this.callChildWorkflow(name, input, Void.class); } /** - * Asynchronously invokes another workflow as a child-workflow and returns a {@link Task} that completes - * when the child-workflow completes. + * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes + * when the sub-orchestration completes. * - *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description. + *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description.

* - * @param name the name of the workflow to invoke - * @param input the serializable input to send to the child-workflow - * @param returnType the expected class type of the child-workflow output - * @param the expected type of the child-workflow output - * @return a new {@link Task} that completes when the child-workflow completes or fails + * @param name the name of the orchestrator to invoke + * @param input the serializable input to send to the sub-orchestration + * @param returnType the expected class type of the sub-orchestration output + * @param the expected type of the sub-orchestration output + * @return a new {@link Task} that completes when the sub-orchestration completes or fails */ default Task callChildWorkflow(String name, Object input, Class returnType) { return this.callChildWorkflow(name, input, null, returnType); } /** - * Asynchronously invokes another workflow as a child-workflow and returns a {@link Task} that completes - * when the child-workflow completes. + * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes + * when the sub-orchestration completes. * - *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description. + *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description.

* - * @param name the name of the workflow to invoke - * @param input the serializable input to send to the child-workflow - * @param instanceID the unique ID of the child-workflow - * @param returnType the expected class type of the child-workflow output - * @param the expected type of the child-workflow output - * @return a new {@link Task} that completes when the child-workflow completes or fails + * @param name the name of the orchestrator to invoke + * @param input the serializable input to send to the sub-orchestration + * @param instanceID the unique ID of the sub-orchestration + * @param returnType the expected class type of the sub-orchestration output + * @param the expected type of the sub-orchestration output + * @return a new {@link Task} that completes when the sub-orchestration completes or fails */ default Task callChildWorkflow(String name, Object input, String instanceID, Class returnType) { return this.callChildWorkflow(name, input, instanceID, null, returnType); } /** - * Asynchronously invokes another workflow as a child-workflow and returns a {@link Task} that completes - * when the child-workflow completes. + * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes + * when the sub-orchestration completes. * - *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description. + *

See {@link #callChildWorkflow(String, Object, String, WorkflowTaskOptions, Class)} for a full description.

* - * @param name the name of the workflow to invoke - * @param input the serializable input to send to the child-workflow - * @param instanceID the unique ID of the child-workflow + * @param name the name of the orchestrator to invoke + * @param input the serializable input to send to the sub-orchestration + * @param instanceID the unique ID of the sub-orchestration * @param options additional options that control the execution and processing of the activity - * @return a new {@link Task} that completes when the child-workflow completes or fails + * @return a new {@link Task} that completes when the sub-orchestration completes or fails */ default Task callChildWorkflow(String name, Object input, String instanceID, WorkflowTaskOptions options) { return this.callChildWorkflow(name, input, instanceID, options, Void.class); } /** - * Asynchronously invokes another workflow as a child-workflow and returns a {@link Task} that completes - * when the child-workflow completes. If the child-workflow completes successfully, the returned - * {@code Task}'s value will be the activity's output. If the child-workflow fails, the returned {@code Task} + * Asynchronously invokes another orchestrator as a sub-orchestration and returns a {@link Task} that completes + * when the sub-orchestration completes. If the sub-orchestration completes successfully, the returned + * {@code Task}'s value will be the activity's output. If the sub-orchestration fails, the returned {@code Task} * will complete exceptionally with a {@link TaskFailedException}. * - *

A child-workflow has its own instance ID, history, and status that is independent of the parent workflow - * that started it. There are many advantages to breaking down large orchestrations into child-workflows: + *

A sub-orchestration has its own instance ID, history, and status that is independent of the parent orchestrator + * that started it. There are many advantages to breaking down large orchestrations into sub-orchestrations:

*
    *
  • - * Splitting large orchestrations into a series of smaller child-workflows can make code more maintainable. + * Splitting large orchestrations into a series of smaller sub-orchestrations can make code more maintainable. *
  • *
  • * Distributing orchestration logic across multiple compute nodes concurrently is useful if @@ -455,92 +541,118 @@ default Task callChildWorkflow(String name, Object input, String instanceI * Memory usage and CPU overhead can be reduced by keeping the history of parent orchestrations smaller. *
  • *
- * The disadvantage is that there is overhead associated with starting a child-workflow and processing its - * output. This is typically only an issue for very small orchestrations. * - *

Because child-workflows are independent of their parents, terminating a parent orchestration does not affect - * any child-workflows. child-workflows must be terminated independently using their unique instance ID, - * which is specified using the {@code instanceID} parameter + *

The disadvantage is that there is overhead associated with starting a sub-orchestration and processing its + * output. This is typically only an issue for very small orchestrations.

+ * + *

Because sub-orchestrations are independent of their parents, terminating a parent orchestration does not affect + * any sub-orchestrations. Sub-orchestrations must be terminated independently using their unique instance ID, + * which is specified using the {@code instanceID} parameter.

* - * @param name the name of the workflow to invoke - * @param input the serializable input to send to the child-workflow - * @param instanceID the unique ID of the child-workflow + * @param name the name of the orchestrator to invoke + * @param input the serializable input to send to the sub-orchestration + * @param instanceID the unique ID of the sub-orchestration * @param options additional options that control the execution and processing of the activity - * @param returnType the expected class type of the child-workflow output - * @param the expected type of the child-workflow output - * @return a new {@link Task} that completes when the child-workflow completes or fails + * @param returnType the expected class type of the sub-orchestration output + * @param the expected type of the sub-orchestration output + * @return a new {@link Task} that completes when the sub-orchestration completes or fails */ - Task callChildWorkflow(String name, - @Nullable Object input, - @Nullable String instanceID, - @Nullable WorkflowTaskOptions options, - Class returnType); + Task callChildWorkflow( + String name, + @Nullable Object input, + @Nullable String instanceID, + @Nullable WorkflowTaskOptions options, + Class returnType); /** - * Restarts the orchestration with a new input and clears its history. See {@link #continueAsNew(Object, boolean)} - * for a full description. + * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is + * received or is canceled when {@code timeout} expires. * - * @param input the serializable input data to re-initialize the instance with + *

External clients can raise events to a waiting orchestration instance using the + * {@link DurableTaskClient#raiseEvent} method.

+ * + *

If the current orchestration is not yet waiting for an event named {@code name}, then the event will be saved in + * the orchestration instance state and dispatched immediately when this method is called. This event saving occurs + * even if the current orchestrator cancels the wait operation before the event is received.

+ * + *

Orchestrators can wait for the same event name multiple times, so waiting for multiple events with the same name + * is allowed. Each external event received by an orchestrator will complete just one task returned by this method. + *

+ * + * @param name the case-insensitive name of the event to wait for + * @param timeout the amount of time to wait before canceling the returned {@code Task} + * @param dataType the expected class type of the event data payload + * @param the expected type of the event data payload + * @return a new {@link Task} that completes when the external event is received or when {@code timeout} expires + * @throws TaskCanceledException if the specified {@code timeout} value expires before the event is received */ - default void continueAsNew(Object input) { - this.continueAsNew(input, true); - } + Task waitForExternalEvent(String name, Duration timeout, Class dataType) throws TaskCanceledException; /** - * Restarts the orchestration with a new input and clears its history. + * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is + * received or is canceled when {@code timeout} expires. * - *

This method is primarily designed for eternal orchestrations, which are orchestrations that - * may not ever complete. It works by restarting the orchestration, providing it with a new input, - * and truncating the existing orchestration history. It allows an orchestration to continue - * running indefinitely without having its history grow unbounded. The benefits of periodically - * truncating history include decreased memory usage, decreased storage volumes, and shorter orchestrator - * replays when rebuilding state. + *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description.

* - *

The results of any incomplete tasks will be discarded when an orchestrator calls {@code continueAsNew}. - * For example, if a timer is scheduled and then {@code continueAsNew} is called before the timer fires, the timer - * event will be discarded. The only exception to this is external events. By default, if an external event is - * received by an orchestration but not yet processed, the event is saved in the orchestration state unit it is - * received by a call to {@link #waitForExternalEvent}. These events will remain in memory - * even after an orchestrator restarts using {@code continueAsNew}. This behavior can be disabled by specifying - * {@code false} for the {@code preserveUnprocessedEvents} parameter value. + * @param name the case-insensitive name of the event to wait for + * @param timeout the amount of time to wait before canceling the returned {@code Task} + * @return a new {@link Task} that completes when the external event is received or when {@code timeout} expires + * @throws TaskCanceledException if the specified {@code timeout} value expires before the event is received + */ + default Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException { + return this.waitForExternalEvent(name, timeout, Void.class); + } + + /** + * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is + * received. * - *

Orchestrator implementations should complete immediately after calling the{@code continueAsNew} method. + *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description.

* - * @param input the serializable input data to re-initialize the instance with - * @param preserveUnprocessedEvents {@code true} to push unprocessed external events into the new orchestration - * history, otherwise {@code false} + * @param name the case-insensitive name of the event to wait for + * @return a new {@link Task} that completes when the external event is received */ - void continueAsNew(Object input, boolean preserveUnprocessedEvents); + default Task waitForExternalEvent(String name) { + return this.waitForExternalEvent(name, Void.class); + } /** - * Create a new UUID that is safe for replay within a workflow. + * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is + * received. * - *

- * The default implementation of this method creates a name-based UUID - * using the algorithm from RFC 4122 §4.3. The name input used to generate - * this value is a combination of the workflow instance ID and an - * internally managed sequence number. - *

- * @return a deterministic UUID + *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description.

+ * + * @param name the case-insensitive name of the event to wait for + * @param dataType the expected class type of the event data payload + * @param the expected type of the event data payload + * @return a new {@link Task} that completes when the external event is received */ - default UUID newUuid() { - throw new RuntimeException("No implementation found."); + default Task waitForExternalEvent(String name, Class dataType) { + try { + return this.waitForExternalEvent(name, null, dataType); + } catch (TaskCanceledException e) { + // This should never happen because of the max duration + throw new RuntimeException("An unexpected exception was throw while waiting for an external event.", e); + } } /** - * Set a custom status to a workflow execution. + * Assigns a custom status value to the current orchestration. + * + *

The {@code customStatus} value is serialized and stored in orchestration state and will be made available to the + * orchestration status query APIs, such as {@link DurableTaskClient#getInstanceMetadata}. The serialized value + * must not exceed 16 KB of UTF-16 encoded text.

+ * + *

Use {@link #clearCustomStatus()} to remove the custom status value from the orchestration state.

* - * @param status to be set to the current execution + * @param customStatus A serializable value to assign as the custom status value. */ - void setCustomStatus(Object status); + void setCustomStatus(Object customStatus); /** - * Checks if the patch has been applied. - * - * @param patchName the patch name to check - * @return true if already applied + * Clears the orchestration's custom status. */ - boolean isPatched(String patchName); + void clearCustomStatus(); /** * Gets the propagated history from a parent workflow, if any was propagated. diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java index 52cabd34d6..8b0f954155 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java @@ -13,7 +13,7 @@ package io.dapr.workflows; -import io.dapr.durabletask.HistoryPropagationScope; +import io.dapr.workflows.task.history.HistoryPropagationScope; public class WorkflowTaskOptions { @@ -78,6 +78,43 @@ public WorkflowTaskOptions(WorkflowTaskRetryHandler retryHandler, String appId) this(null, retryHandler, appId, null); } + /** + * Indicates whether a retry policy was configured. + * + * @return true when a retry policy is set. + */ + public boolean hasRetryPolicy() { + return this.retryPolicy != null; + } + + /** + * Indicates whether a retry handler was configured. + * + * @return true when a retry handler is set. + */ + public boolean hasRetryHandler() { + return this.retryHandler != null; + } + + /** + * Indicates whether a non-empty target app ID was configured. + * + * @return true when an app ID is set and not empty. + */ + public boolean hasAppID() { + return this.appId != null && !this.appId.isEmpty(); + } + + /** + * Indicates whether a history propagation scope other than NONE was configured. + * + * @return true when history propagation is requested. + */ + public boolean hasHistoryPropagationScope() { + return this.historyPropagationScope != null + && this.historyPropagationScope != HistoryPropagationScope.NONE; + } + public WorkflowTaskRetryPolicy getRetryPolicy() { return retryPolicy; } @@ -124,4 +161,118 @@ public static WorkflowTaskOptions propagateLineage() { public static WorkflowTaskOptions propagateOwnHistory() { return withHistoryPropagation(HistoryPropagationScope.OWN_HISTORY); } + + /** + * Creates a builder for {@link WorkflowTaskOptions}. + * + * @return a new builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates an empty {@link WorkflowTaskOptions}. + * + * @return a new options instance with nothing configured. + */ + public static WorkflowTaskOptions create() { + return new WorkflowTaskOptions(null, null, null, null); + } + + /** + * Creates a {@link WorkflowTaskOptions} from a retry policy. + * + * @param retryPolicy the retry policy. + * @return a new options instance. + */ + public static WorkflowTaskOptions withRetryPolicy(WorkflowTaskRetryPolicy retryPolicy) { + return new WorkflowTaskOptions(retryPolicy, null, null, null); + } + + /** + * Creates a {@link WorkflowTaskOptions} from a retry handler. + * + * @param retryHandler the retry handler. + * @return a new options instance. + */ + public static WorkflowTaskOptions withRetryHandler(WorkflowTaskRetryHandler retryHandler) { + return new WorkflowTaskOptions(null, retryHandler, null, null); + } + + /** + * Creates a {@link WorkflowTaskOptions} targeting another app. + * + * @param appId the ID of the app to call into. + * @return a new options instance. + */ + public static WorkflowTaskOptions withAppID(String appId) { + return new WorkflowTaskOptions(null, null, appId, null); + } + + /** + * Builder for {@link WorkflowTaskOptions}. + */ + public static final class Builder { + private WorkflowTaskRetryPolicy retryPolicy; + private WorkflowTaskRetryHandler retryHandler; + private String appId; + private HistoryPropagationScope historyPropagationScope; + + private Builder() { + } + + /** + * Sets the retry policy. + * + * @param retryPolicy the retry policy. + * @return this builder. + */ + public Builder retryPolicy(WorkflowTaskRetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + return this; + } + + /** + * Sets the retry handler. + * + * @param retryHandler the retry handler. + * @return this builder. + */ + public Builder retryHandler(WorkflowTaskRetryHandler retryHandler) { + this.retryHandler = retryHandler; + return this; + } + + /** + * Sets the target app ID for cross-app calls. + * + * @param appId the app ID. + * @return this builder. + */ + public Builder appID(String appId) { + this.appId = appId; + return this; + } + + /** + * Sets the history propagation scope. + * + * @param scope the scope. + * @return this builder. + */ + public Builder historyPropagationScope(HistoryPropagationScope scope) { + this.historyPropagationScope = scope; + return this; + } + + /** + * Builds the options. + * + * @return a new {@link WorkflowTaskOptions}. + */ + public WorkflowTaskOptions build() { + return new WorkflowTaskOptions(this.retryPolicy, this.retryHandler, this.appId, this.historyPropagationScope); + } + } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryContext.java index 15fa3fd505..c8a1730a5b 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryContext.java @@ -13,48 +13,53 @@ package io.dapr.workflows; -import io.dapr.workflows.client.WorkflowFailureDetails; -import io.dapr.workflows.runtime.DefaultWorkflowContext; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; import java.time.Duration; -public class WorkflowTaskRetryContext { - - private final DefaultWorkflowContext workflowContext; +/** + * Context data that's provided to {@link WorkflowTaskRetryHandler} implementations. + */ +public final class WorkflowTaskRetryContext { + private final WorkflowContext orchestrationContext; private final int lastAttemptNumber; private final WorkflowFailureDetails lastFailure; private final Duration totalRetryTime; /** - * Constructor for WorkflowTaskRetryContext. + * Creates a retry context. + * + *

Public so the workflow executor in a sibling package can reach it; not intended for + * application code. * - * @param workflowContext The workflow context - * @param lastAttemptNumber The number of the previous attempt - * @param lastFailure The failure details from the most recent failure - * @param totalRetryTime The amount of time spent retrying + * @param orchestrationContext the workflow context of the failing task. + * @param lastAttemptNumber the number of the attempt that just failed. + * @param lastFailure details of the last failure. + * @param totalRetryTime how long retries have been running for. */ public WorkflowTaskRetryContext( - DefaultWorkflowContext workflowContext, - int lastAttemptNumber, - WorkflowFailureDetails lastFailure, - Duration totalRetryTime) { - this.workflowContext = workflowContext; + WorkflowContext orchestrationContext, + int lastAttemptNumber, + WorkflowFailureDetails lastFailure, + Duration totalRetryTime) { + this.orchestrationContext = orchestrationContext; this.lastAttemptNumber = lastAttemptNumber; this.lastFailure = lastFailure; this.totalRetryTime = totalRetryTime; } /** - * Gets the context of the current workflow. + * Gets the context of the current orchestration. * - *

The workflow context can be used in retry handlers to schedule timers (via the - * {@link DefaultWorkflowContext#createTimer} methods) for implementing delays between retries. It can also be - * used to implement time-based retry logic by using the {@link DefaultWorkflowContext#getCurrentInstant} method. + *

The orchestration context can be used in retry handlers to schedule timers (via the + * {@link WorkflowContext#createTimer} methods) for implementing delays between retries. It can also be + * used to implement time-based retry logic by using the {@link WorkflowContext#getCurrentInstant} method. + *

* - * @return the context of the parent workflow + * @return the context of the parent orchestration */ - public DefaultWorkflowContext getWorkflowContext() { - return this.workflowContext; + public WorkflowContext getWorkflowContext() { + return this.orchestrationContext; } /** @@ -84,5 +89,4 @@ public int getLastAttemptNumber() { public Duration getTotalRetryTime() { return this.totalRetryTime; } - } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryPolicy.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryPolicy.java index b0e72f917b..899c718330 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryPolicy.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskRetryPolicy.java @@ -40,11 +40,64 @@ public WorkflowTaskRetryPolicy( Duration maxRetryInterval, Duration retryTimeout ) { + // Validation lives here rather than only on the Builder because the durable task client's + // RetryPolicy validated in its setters, and the adapter that used to sit between the two types + // routed every policy through them. With the adapter gone, a policy built through these + // constructors would otherwise reach the executor unchecked and fail late - as an NPE part-way + // through a replay, or as silently wrong retry timing. + if (maxNumberOfAttempts == null) { + throw new IllegalArgumentException("maxNumberOfAttempts cannot be null."); + } + if (maxNumberOfAttempts <= 0) { + throw new IllegalArgumentException("The value for maxNumberOfAttempts must be greater than zero."); + } + if (firstRetryInterval == null) { + throw new IllegalArgumentException("firstRetryInterval cannot be null."); + } + if (firstRetryInterval.isZero() || firstRetryInterval.isNegative()) { + throw new IllegalArgumentException("The value for firstRetryInterval must be greater than zero."); + } + if (backoffCoefficient == null) { + throw new IllegalArgumentException("backoffCoefficient cannot be null."); + } + if (backoffCoefficient < 1.0) { + throw new IllegalArgumentException("The value for backoffCoefficient must be greater or equal to 1.0."); + } + + // Range-check the RAW arguments, before the null coercion below. Only null means "unset"; an + // explicitly supplied Duration.ZERO is a real value, and v1 rejected it because + // ZERO < firstRetryInterval. Checking after coercion would silently accept an explicit ZERO and + // would also disagree with the Builder, which still rejects it. + if (maxRetryInterval != null && maxRetryInterval.compareTo(firstRetryInterval) < 0) { + throw new IllegalArgumentException("The value for maxRetryInterval must be greater than or equal to the value " + + "for firstRetryInterval."); + } + if (retryTimeout != null && retryTimeout.compareTo(firstRetryInterval) < 0) { + throw new IllegalArgumentException("The value for retryTimeout must be greater than or equal to the value " + + "for firstRetryInterval."); + } + this.maxNumberOfAttempts = maxNumberOfAttempts; this.firstRetryInterval = firstRetryInterval; this.backoffCoefficient = backoffCoefficient; - this.maxRetryInterval = maxRetryInterval; - this.retryTimeout = retryTimeout; + // The durable task client's RetryPolicy defaulted these to ZERO, and the adapter that used to + // sit between the two types translated "unset" into that default. With the adapter gone, the + // coercion has to live here: a null reaching the executor suppresses the retry timer entirely. + this.maxRetryInterval = maxRetryInterval == null ? Duration.ZERO : maxRetryInterval; + this.retryTimeout = retryTimeout == null ? Duration.ZERO : retryTimeout; + } + + /** + * Creates a retry policy with the two required settings, leaving the rest at their defaults. + * + *

Carried over from the durable task client's RetryPolicy, whose two-argument constructor + * this replaces. + * + * @param maxNumberOfAttempts Maximum number of attempts to retry the workflow. + * @param firstRetryInterval Interval to wait before the first retry. + */ + public WorkflowTaskRetryPolicy(int maxNumberOfAttempts, Duration firstRetryInterval) { + this(maxNumberOfAttempts, firstRetryInterval, 1.0, null, null); } public int getMaxNumberOfAttempts() { diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java index 3880c770e0..66832ad035 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java @@ -14,16 +14,15 @@ package io.dapr.workflows.client; import io.dapr.config.Properties; -import io.dapr.durabletask.DurableTaskClient; -import io.dapr.durabletask.DurableTaskGrpcClientBuilder; -import io.dapr.durabletask.NewOrchestrationInstanceOptions; -import io.dapr.durabletask.OrchestrationMetadata; -import io.dapr.durabletask.PurgeResult; import io.dapr.utils.NetworkUtils; import io.dapr.workflows.Workflow; import io.dapr.workflows.internal.ApiTokenClientInterceptor; -import io.dapr.workflows.runtime.DefaultWorkflowInstanceStatus; import io.dapr.workflows.runtime.DefaultWorkflowState; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.client.DurableTaskGrpcClientBuilder; +import io.dapr.workflows.task.client.NewOrchestrationInstanceOptions; +import io.dapr.workflows.task.client.OrchestrationMetadata; +import io.dapr.workflows.task.client.PurgeResult; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannel; import io.grpc.Status; @@ -246,23 +245,6 @@ public void terminateWorkflow(String workflowInstanceId, @Nullable Object output this.innerClient.terminate(workflowInstanceId, output); } - /** - * Fetches workflow instance metadata from the configured durable store. - * - * @param instanceId the unique ID of the workflow instance to fetch - * @param getInputsAndOutputs true to fetch the workflow instance's - * inputs, outputs, and custom status, or false to omit them - * @return a metadata record that describes the workflow instance and it execution status, or a default instance - * @deprecated Use {@link #getWorkflowState(String, boolean)} instead. - */ - @Nullable - @Deprecated(forRemoval = true) - public WorkflowInstanceStatus getInstanceState(String instanceId, boolean getInputsAndOutputs) { - OrchestrationMetadata metadata = this.innerClient.getInstanceMetadata(instanceId, getInputsAndOutputs); - - return metadata == null ? null : new DefaultWorkflowInstanceStatus(metadata); - } - /** * Fetches workflow instance metadata from the configured durable store. * @@ -278,35 +260,6 @@ public WorkflowState getWorkflowState(String instanceId, boolean getInputsAndOut return metadata == null ? null : new DefaultWorkflowState(metadata); } - /** - * Waits for an workflow to start running and returns an - * {@link WorkflowInstanceStatus} object that contains metadata about the started - * instance and optionally its input, output, and custom status payloads. - * - *

A "started" workflow instance is any instance not in the Pending state. - * - *

If an workflow instance is already running when this method is called, - * the method will return immediately. - * - * @param instanceId the unique ID of the workflow instance to wait for - * @param timeout the amount of time to wait for the workflow instance to start - * @param getInputsAndOutputs true to fetch the workflow instance's - * inputs, outputs, and custom status, or false to omit them - * @return the workflow instance metadata or null if no such instance is found - * @throws TimeoutException when the workflow instance is not started within the specified amount of time - * @deprecated Use {@link #waitForWorkflowStart(String, Duration, boolean)} instead. - */ - @Deprecated(forRemoval = true) - @Nullable - public WorkflowInstanceStatus waitForInstanceStart(String instanceId, Duration timeout, boolean getInputsAndOutputs) - throws TimeoutException { - - OrchestrationMetadata metadata = this.innerClient.waitForInstanceStart(instanceId, timeout, getInputsAndOutputs); - - return metadata == null ? null : new DefaultWorkflowInstanceStatus(metadata); - } - - /** * Waits for a workflow to start running and returns an * {@link WorkflowState} object that contains metadata about the started @@ -333,37 +286,6 @@ public WorkflowState waitForWorkflowStart(String instanceId, Duration timeout, b return metadata == null ? null : new DefaultWorkflowState(metadata); } - /** - * Waits for an workflow to complete and returns an {@link WorkflowInstanceStatus} object that contains - * metadata about the completed instance. - * - *

A "completed" workflow instance is any instance in one of the terminal states. For example, the - * Completed, Failed, or Terminated states. - * - *

Workflows are long-running and could take hours, days, or months before completing. - * Workflows can also be eternal, in which case they'll never complete unless terminated. - * In such cases, this call may block indefinitely, so care must be taken to ensure appropriate timeouts are used. - * If an workflow instance is already complete when this method is called, the method will return immediately. - * - * @param instanceId the unique ID of the workflow instance to wait for - * @param timeout the amount of time to wait for the workflow instance to complete - * @param getInputsAndOutputs true to fetch the workflow instance's inputs, outputs, and custom - * status, or false to omit them - * @return the workflow instance metadata or null if no such instance is found - * @throws TimeoutException when the workflow instance is not completed within the specified amount of time - * @deprecated Use {@link #waitForWorkflowCompletion(String, Duration, boolean)} instead. - */ - @Nullable - @Deprecated(forRemoval = true) - public WorkflowInstanceStatus waitForInstanceCompletion(String instanceId, Duration timeout, - boolean getInputsAndOutputs) throws TimeoutException { - - OrchestrationMetadata metadata = this.innerClient.waitForInstanceCompletion(instanceId, timeout, - getInputsAndOutputs); - return metadata == null ? null : new DefaultWorkflowInstanceStatus(metadata); - } - - /** * Waits for an workflow to complete and returns an {@link WorkflowState} object that contains * metadata about the completed instance. @@ -403,24 +325,6 @@ public void raiseEvent(String workflowInstanceId, String eventName, Object event this.innerClient.raiseEvent(workflowInstanceId, eventName, eventPayload); } - /** - * Purges workflow instance state from the workflow state store. - * - * @param workflowInstanceId The unique ID of the workflow instance to purge. - * @return Return true if the workflow state was found and purged successfully otherwise false. - * @deprecated Use {@link #purgeWorkflow(String)} instead. - */ - @Deprecated(forRemoval = true) - public boolean purgeInstance(String workflowInstanceId) { - PurgeResult result = this.innerClient.purgeInstance(workflowInstanceId); - - if (result != null) { - return result.getDeletedInstanceCount() > 0; - } - - return false; - } - /** * Purges workflow instance state from the workflow state store. * diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowFailureDetails.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowFailureDetails.java deleted file mode 100644 index 1adf1fe005..0000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowFailureDetails.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.client; - -/** - * Represents a workflow failure details. - */ -public interface WorkflowFailureDetails { - - /** - * Gets the error type, which is the namespace-qualified exception type name. - * - * @return the error type, which is the namespace-qualified exception type name - */ - String getErrorType(); - - /** - * Gets the error message. - * - * @return the error message - */ - String getErrorMessage(); - - /** - * Gets the stack trace. - * - * @return the stack trace - */ - String getStackTrace(); - - /** - * Checks whether the failure was caused by the provided exception class. - * - * @param exceptionClass the exception class to check - * @return {@code true} if the failure was caused by the provided exception class - */ - default boolean isCausedBy(Class exceptionClass) { - throw new UnsupportedOperationException("This method is not implemented"); - } - -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstanceStatus.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstanceStatus.java deleted file mode 100644 index bdcd0087f5..0000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstanceStatus.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.client; - -import javax.annotation.Nullable; - -import java.time.Instant; - -/** - * Represents a snapshot of a workflow instance's current state, including - * metadata. - * @deprecated Use {@link WorkflowState} instead. - */ -@Deprecated(forRemoval = true) -public interface WorkflowInstanceStatus { - - /** - * Gets the name of the workflow. - * - * @return the name of the workflow - */ - String getName(); - - /** - * Gets the unique ID of the workflow instance. - * - * @return the unique ID of the workflow instance - */ - String getInstanceId(); - - /** - * Gets the current runtime status of the workflow instance at the time this - * object was fetched. - * - * @return the current runtime status of the workflow instance at the time this object was fetched - */ - WorkflowRuntimeStatus getRuntimeStatus(); - - /** - * Gets the workflow instance's creation time in UTC. - * - * @return the workflow instance's creation time in UTC - */ - Instant getCreatedAt(); - - /** - * Gets the workflow instance's last updated time in UTC. - * - * @return the workflow instance's last updated time in UTC - */ - Instant getLastUpdatedAt(); - - /** - * Gets the workflow instance's serialized input, if any, as a string value. - * - * @return the workflow instance's serialized input or {@code null} - */ - String getSerializedInput(); - - /** - * Gets the workflow instance's serialized output, if any, as a string value. - * - * @return the workflow instance's serialized output or {@code null} - */ - String getSerializedOutput(); - - /** - * Gets the failure details, if any, for the failed workflow instance. - * - *

This method returns data only if the workflow is in the - * {@link WorkflowFailureDetails} failureDetails, - * and only if this instance metadata was fetched with the option to include - * output data. - * - * @return the failure details of the failed workflow instance or {@code null} - */ - @Nullable - WorkflowFailureDetails getFailureDetails(); - - /** - * Gets a value indicating whether the workflow instance was running at the time - * this object was fetched. - * - * @return {@code true} if the workflow existed and was in a running state otherwise {@code false} - */ - boolean isRunning(); - - /** - * Gets a value indicating whether the workflow instance was completed at the - * time this object was fetched. - * - *

A workflow instance is considered completed when its runtime status value is - * {@link WorkflowRuntimeStatus#COMPLETED}, - * {@link WorkflowRuntimeStatus#FAILED}, or - * {@link WorkflowRuntimeStatus#TERMINATED}. - * - * @return {@code true} if the workflow was in a terminal state; otherwise {@code false} - */ - boolean isCompleted(); - - /** - * Deserializes the workflow's input into an object of the specified type. - * - *

Deserialization is performed using the DataConverter that was - * configured on the DurableTaskClient object that created this workflow - * metadata object. - * - * @param type the class associated with the type to deserialize the input data - * into - * @param the type to deserialize the input data into - * @return the deserialized input value - * @throws IllegalStateException if the metadata was fetched without the option - * to read inputs and outputs - */ - T readInputAs(Class type); - - /** - * Deserializes the workflow's output into an object of the specified type. - * - *

Deserialization is performed using the DataConverter that was - * configured on the DurableTaskClient - * object that created this workflow metadata object. - * - * @param type the class associated with the type to deserialize the output data - * into - * @param the type to deserialize the output data into - * @return the deserialized input value - * @throws IllegalStateException if the metadata was fetched without the option - * to read inputs and outputs - */ - T readOutputAs(Class type); - -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowRuntimeStatus.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowRuntimeStatus.java index 5721c8ad38..f12767cb1b 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowRuntimeStatus.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowRuntimeStatus.java @@ -13,6 +13,18 @@ package io.dapr.workflows.client; +import io.dapr.durabletask.implementation.protobuf.Orchestration; + +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_CANCELED; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_PENDING; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_RUNNING; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_STALLED; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_SUSPENDED; +import static io.dapr.durabletask.implementation.protobuf.Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED; + /** * Enum describing the runtime status of a workflow. */ @@ -55,6 +67,74 @@ public enum WorkflowRuntimeStatus { /** * The workflow was suspended. */ - SUSPENDED + SUSPENDED, + + /** + * The workflow is in a stalled state. + */ + STALLED; + /** + * Maps a protobuf workflow status onto this enum. + * + * @param status the protobuf status to convert. + * @return the corresponding {@link WorkflowRuntimeStatus}. + * @throws IllegalArgumentException if the status is unknown. + */ + public static WorkflowRuntimeStatus fromProtobuf(Orchestration.OrchestrationStatus status) { + switch (status) { + case ORCHESTRATION_STATUS_RUNNING: + return RUNNING; + case ORCHESTRATION_STATUS_COMPLETED: + return COMPLETED; + case ORCHESTRATION_STATUS_CONTINUED_AS_NEW: + return CONTINUED_AS_NEW; + case ORCHESTRATION_STATUS_FAILED: + return FAILED; + case ORCHESTRATION_STATUS_CANCELED: + return CANCELED; + case ORCHESTRATION_STATUS_TERMINATED: + return TERMINATED; + case ORCHESTRATION_STATUS_PENDING: + return PENDING; + case ORCHESTRATION_STATUS_SUSPENDED: + return SUSPENDED; + case ORCHESTRATION_STATUS_STALLED: + return STALLED; + default: + throw new IllegalArgumentException(String.format("Unknown status value: %s", status)); + } + } + + /** + * Maps this enum onto its protobuf workflow status. + * + * @param status the status to convert. + * @return the corresponding protobuf status. + * @throws IllegalArgumentException if the status is unknown. + */ + public static Orchestration.OrchestrationStatus toProtobuf(WorkflowRuntimeStatus status) { + switch (status) { + case RUNNING: + return ORCHESTRATION_STATUS_RUNNING; + case COMPLETED: + return ORCHESTRATION_STATUS_COMPLETED; + case CONTINUED_AS_NEW: + return ORCHESTRATION_STATUS_CONTINUED_AS_NEW; + case FAILED: + return ORCHESTRATION_STATUS_FAILED; + case CANCELED: + return ORCHESTRATION_STATUS_CANCELED; + case TERMINATED: + return ORCHESTRATION_STATUS_TERMINATED; + case PENDING: + return ORCHESTRATION_STATUS_PENDING; + case SUSPENDED: + return ORCHESTRATION_STATUS_SUSPENDED; + case STALLED: + return ORCHESTRATION_STATUS_STALLED; + default: + throw new IllegalArgumentException(String.format("Unknown status value: %s", status)); + } + } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowState.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowState.java index 282d1d73ee..8febd031ce 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowState.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowState.java @@ -13,6 +13,8 @@ package io.dapr.workflows.client; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; + import javax.annotation.Nullable; import java.time.Instant; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java b/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java new file mode 100644 index 0000000000..3b9a7f74e4 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.internal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Builds the executor the workflow runtime falls back to when the caller supplies none. + * + *

This module is compiled for Java 17, so it cannot reference + * {@code Executors.newVirtualThreadPerTaskExecutor()} at compile time. On Java 21 and later + * that factory is resolved reflectively; on Java 17 through 20 a cached thread pool is used, + * which is what the workflow runtime has always used. + * + *

Callers that want explicit control should pass their own executor to + * {@code WorkflowRuntimeBuilder.withExecutorService(...)} instead of relying on this default. + * An executor supplied that way is never shut down by the runtime. + */ +public final class DefaultExecutorService { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultExecutorService.class); + + private static final int VIRTUAL_THREADS_SINCE = 21; + + private DefaultExecutorService() { + } + + /** + * Creates the default executor for the current runtime. + * + * @return a virtual-thread-per-task executor on Java 21+, otherwise a cached thread pool. + */ + public static ExecutorService create() { + if (Runtime.version().feature() >= VIRTUAL_THREADS_SINCE) { + try { + Method factory = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); + ExecutorService executorService = (ExecutorService) factory.invoke(null); + LOGGER.info("Using a virtual thread per task executor for workflow and activity execution"); + return executorService; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + LOGGER.warn("Virtual threads are unavailable on this Java {} runtime, " + + "falling back to a cached thread pool", Runtime.version().feature(), ex); + } + } + + return Executors.newCachedThreadPool(); + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java index 4763e90527..11c697d173 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java @@ -13,9 +13,9 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.TaskActivityContext; import io.dapr.workflows.WorkflowActivityContext; +import io.dapr.workflows.task.TaskActivityContext; +import io.dapr.workflows.task.history.PropagatedHistory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java deleted file mode 100644 index 1ee6591969..0000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.runtime; - -import io.dapr.durabletask.CompositeTaskFailedException; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.RetryHandler; -import io.dapr.durabletask.RetryPolicy; -import io.dapr.durabletask.Task; -import io.dapr.durabletask.TaskCanceledException; -import io.dapr.durabletask.TaskOptions; -import io.dapr.durabletask.TaskOrchestrationContext; -import io.dapr.workflows.WorkflowContext; -import io.dapr.workflows.WorkflowTaskOptions; -import io.dapr.workflows.WorkflowTaskRetryContext; -import io.dapr.workflows.WorkflowTaskRetryHandler; -import io.dapr.workflows.WorkflowTaskRetryPolicy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.helpers.NOPLogger; - -import javax.annotation.Nullable; - -import java.time.Duration; -import java.time.Instant; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -public class DefaultWorkflowContext implements WorkflowContext { - private final TaskOrchestrationContext innerContext; - private final Logger logger; - - /** - * Constructor for DefaultWorkflowContext. - * - * @param context TaskOrchestrationContext - * @throws IllegalArgumentException if context is null - */ - public DefaultWorkflowContext(TaskOrchestrationContext context) throws IllegalArgumentException { - this(context, WorkflowContext.class); - } - - /** - * Constructor for DefaultWorkflowContext. - * - * @param context TaskOrchestrationContext - * @param clazz Class to use for logger - * @throws IllegalArgumentException if context is null - */ - public DefaultWorkflowContext(TaskOrchestrationContext context, Class clazz) throws IllegalArgumentException { - this(context, LoggerFactory.getLogger(clazz)); - } - - /** - * Constructor for DefaultWorkflowContext. - * - * @param context TaskOrchestrationContext - * @param logger Logger - * @throws IllegalArgumentException if context or logger is null - */ - public DefaultWorkflowContext(TaskOrchestrationContext context, Logger logger) - throws IllegalArgumentException { - if (context == null) { - throw new IllegalArgumentException("Context cannot be null"); - } - - if (logger == null) { - throw new IllegalArgumentException("Logger cannot be null"); - } - - this.innerContext = context; - this.logger = logger; - } - - /** - * {@inheritDoc} - */ - public Logger getLogger() { - if (this.innerContext.getIsReplaying()) { - return NOPLogger.NOP_LOGGER; - } - return this.logger; - } - - /** - * {@inheritDoc} - */ - public String getName() { - return this.innerContext.getName(); - } - - /** - * {@inheritDoc} - */ - public String getInstanceId() { - return this.innerContext.getInstanceId(); - } - - /** - * {@inheritDoc} - */ - public Instant getCurrentInstant() { - return this.innerContext.getCurrentInstant(); - } - - /** - * {@inheritDoc} - */ - public void complete(Object output) { - this.innerContext.complete(output); - } - - /** - * {@inheritDoc} - */ - @Override - public Task waitForExternalEvent(String name, Duration timeout, Class dataType) - throws TaskCanceledException { - return this.innerContext.waitForExternalEvent(name, timeout, dataType); - } - - /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} - * that completes when the event is - * received or is canceled when {@code timeout} expires. - * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full - * description. - * - * @param name the case-insensitive name of the event to wait for - * @param timeout the amount of time to wait before canceling the returned - * {@code Task} - * @return a new {@link Task} that completes when the external event is received - * or when {@code timeout} expires - * @throws TaskCanceledException if the specified {@code timeout} value expires - * before the event is received - */ - @Override - public Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException { - return this.innerContext.waitForExternalEvent(name, timeout, Void.class); - } - - /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} - * that completes when the event is - * received. - * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full - * description. - * - * @param name the case-insensitive name of the event to wait for - * @return a new {@link Task} that completes when the external event is received - */ - @Override - public Task waitForExternalEvent(String name) throws TaskCanceledException { - return this.innerContext.waitForExternalEvent(name, null, Void.class); - } - - @Override - public boolean isReplaying() { - return this.innerContext.getIsReplaying(); - } - - /** - * {@inheritDoc} - */ - public Task callActivity(String name, Object input, WorkflowTaskOptions options, Class returnType) { - TaskOptions taskOptions = toTaskOptions(options); - - return this.innerContext.callActivity(name, input, taskOptions, returnType); - } - - /** - * {@inheritDoc} - */ - public Task> allOf(List> tasks) throws CompositeTaskFailedException { - return this.innerContext.allOf(tasks); - } - - /** - * {@inheritDoc} - */ - public Task> anyOf(List> tasks) { - return this.innerContext.anyOf(tasks); - } - - /** - * {@inheritDoc} - */ - public Task createTimer(Duration duration) { - return this.innerContext.createTimer(duration); - } - - @Override - public Task createTimer(ZonedDateTime zonedDateTime) { - return this.innerContext.createTimer(zonedDateTime); - } - - /** - * {@inheritDoc} - */ - public T getInput(Class targetType) { - return this.innerContext.getInput(targetType); - } - - /** - * {@inheritDoc} - */ - @Override - public Task callChildWorkflow(String name, @Nullable Object input, @Nullable String instanceID, - @Nullable WorkflowTaskOptions options, Class returnType) { - TaskOptions taskOptions = toTaskOptions(options); - - return this.innerContext.callSubOrchestrator(name, input, instanceID, taskOptions, returnType); - } - - /** - * {@inheritDoc} - */ - @Override - public void continueAsNew(Object input) { - this.innerContext.continueAsNew(input); - } - - /** - * {@inheritDoc} - */ - @Override - public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { - this.innerContext.continueAsNew(input, preserveUnprocessedEvents); - } - - /** - * {@inheritDoc} - */ - @Override - public UUID newUuid() { - return this.innerContext.newUuid(); - } - - private TaskOptions toTaskOptions(WorkflowTaskOptions options) { - if (options == null) { - return null; - } - - RetryPolicy retryPolicy = toRetryPolicy(options.getRetryPolicy()); - RetryHandler retryHandler = toRetryHandler(options.getRetryHandler()); - - return TaskOptions.builder() - .retryPolicy(retryPolicy) - .retryHandler(retryHandler) - .appID(options.getAppId()) - .historyPropagationScope(options.getHistoryPropagationScope()) - .build(); - } - - /** - * Converts a {@link WorkflowTaskRetryPolicy} to a {@link RetryPolicy}. - * - * @param workflowTaskRetryPolicy The {@link WorkflowTaskRetryPolicy} being converted - * @return A {@link RetryPolicy} - */ - private RetryPolicy toRetryPolicy(WorkflowTaskRetryPolicy workflowTaskRetryPolicy) { - if (workflowTaskRetryPolicy == null) { - return null; - } - - RetryPolicy retryPolicy = new RetryPolicy( - workflowTaskRetryPolicy.getMaxNumberOfAttempts(), - workflowTaskRetryPolicy.getFirstRetryInterval() - ); - - retryPolicy.setBackoffCoefficient(workflowTaskRetryPolicy.getBackoffCoefficient()); - if (workflowTaskRetryPolicy.getMaxRetryInterval() != null) { - retryPolicy.setMaxRetryInterval(workflowTaskRetryPolicy.getMaxRetryInterval()); - } - if (workflowTaskRetryPolicy.getRetryTimeout() != null) { - retryPolicy.setRetryTimeout(workflowTaskRetryPolicy.getRetryTimeout()); - } - - return retryPolicy; - } - - /** - * Converts a {@link WorkflowTaskRetryHandler} to a {@link RetryHandler}. - * - * @param workflowTaskRetryHandler The {@link WorkflowTaskRetryHandler} being converted - * @return A {@link RetryHandler} - */ - private RetryHandler toRetryHandler(WorkflowTaskRetryHandler workflowTaskRetryHandler) { - if (workflowTaskRetryHandler == null) { - return null; - } - - return retryContext -> { - WorkflowTaskRetryContext workflowRetryContext = new WorkflowTaskRetryContext( - this, - retryContext.getLastAttemptNumber(), - new DefaultWorkflowFailureDetails(retryContext.getLastFailure()), - retryContext.getTotalRetryTime() - ); - - return workflowTaskRetryHandler.handle(workflowRetryContext); - }; - } - - /** - * Set custom status to a workflow execution. - * - * @param status to set to the execution - */ - public void setCustomStatus(Object status) { - innerContext.setCustomStatus(status); - } - - public boolean isPatched(String patchName) { - return this.innerContext.isPatched(patchName); - } - - @Override - public Optional getPropagatedHistory() { - return this.innerContext.getPropagatedHistory(); - } -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowFailureDetails.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowFailureDetails.java deleted file mode 100644 index ee6e0d8020..0000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowFailureDetails.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.runtime; - -import io.dapr.durabletask.FailureDetails; -import io.dapr.workflows.client.WorkflowFailureDetails; - -/** - * Represents a workflow failure details. - */ -public class DefaultWorkflowFailureDetails implements WorkflowFailureDetails { - - private final FailureDetails workflowFailureDetails; - - /** - * Class constructor. - * - * @param failureDetails failure Details - */ - public DefaultWorkflowFailureDetails(FailureDetails failureDetails) { - this.workflowFailureDetails = failureDetails; - } - - /** - * Gets the error type, which is the namespace-qualified exception type name. - * - * @return the error type, which is the namespace-qualified exception type name - */ - @Override - public String getErrorType() { - return workflowFailureDetails.getErrorType(); - } - - /** - * Gets the error message. - * - * @return the error message - */ - @Override - public String getErrorMessage() { - return workflowFailureDetails.getErrorMessage(); - } - - /** - * Gets the stack trace. - * - * @return the stack trace - */ - @Override - public String getStackTrace() { - return workflowFailureDetails.getStackTrace(); - } - - /** - * Checks whether the failure was caused by the provided exception class. - * - * @param exceptionClass the exception class to check - * @return {@code true} if the failure was caused by the provided exception class - */ - @Override - public boolean isCausedBy(Class exceptionClass) { - return workflowFailureDetails.isCausedBy(exceptionClass); - } - - @Override - public String toString() { - return workflowFailureDetails.toString(); - } - -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowInstanceStatus.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowInstanceStatus.java deleted file mode 100644 index 2c63dc9451..0000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowInstanceStatus.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.runtime; - -import io.dapr.durabletask.FailureDetails; -import io.dapr.durabletask.OrchestrationMetadata; -import io.dapr.durabletask.OrchestrationRuntimeStatus; -import io.dapr.workflows.client.WorkflowFailureDetails; -import io.dapr.workflows.client.WorkflowInstanceStatus; -import io.dapr.workflows.client.WorkflowRuntimeStatus; - -import javax.annotation.Nullable; - -import java.time.Instant; - -/** - * Represents a snapshot of a workflow instance's current state, including - * metadata. - * @deprecated Use {@link DefaultWorkflowState} instead. - */ -@Deprecated(forRemoval = true) -public class DefaultWorkflowInstanceStatus implements WorkflowInstanceStatus { - - private final OrchestrationMetadata orchestrationMetadata; - - @Nullable - private final WorkflowFailureDetails failureDetails; - - /** - * Class constructor. - * - * @param orchestrationMetadata Durable task orchestration metadata - */ - public DefaultWorkflowInstanceStatus(OrchestrationMetadata orchestrationMetadata) { - if (orchestrationMetadata == null) { - throw new IllegalArgumentException("OrchestrationMetadata cannot be null"); - } - this.orchestrationMetadata = orchestrationMetadata; - - FailureDetails details = orchestrationMetadata.getFailureDetails(); - - if (details != null) { - this.failureDetails = new DefaultWorkflowFailureDetails(details); - } else { - this.failureDetails = null; - } - } - - /** - * Gets the name of the workflow. - * - * @return the name of the workflow - */ - public String getName() { - return orchestrationMetadata.getName(); - } - - /** - * Gets the unique ID of the workflow instance. - * - * @return the unique ID of the workflow instance - */ - public String getInstanceId() { - return orchestrationMetadata.getInstanceId(); - } - - /** - * Gets the current runtime status of the workflow instance at the time this - * object was fetched. - * - * @return the current runtime status of the workflow instance at the time this object was fetched - */ - public WorkflowRuntimeStatus getRuntimeStatus() { - OrchestrationRuntimeStatus status = orchestrationMetadata.getRuntimeStatus(); - - return WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(status); - } - - /** - * Gets the workflow instance's creation time in UTC. - * - * @return the workflow instance's creation time in UTC - */ - public Instant getCreatedAt() { - return orchestrationMetadata.getCreatedAt(); - } - - /** - * Gets the workflow instance's last updated time in UTC. - * - * @return the workflow instance's last updated time in UTC - */ - public Instant getLastUpdatedAt() { - return orchestrationMetadata.getLastUpdatedAt(); - } - - /** - * Gets the workflow instance's serialized input, if any, as a string value. - * - * @return the workflow instance's serialized input or {@code null} - */ - public String getSerializedInput() { - return orchestrationMetadata.getSerializedInput(); - } - - /** - * Gets the workflow instance's serialized output, if any, as a string value. - * - * @return the workflow instance's serialized output or {@code null} - */ - public String getSerializedOutput() { - return orchestrationMetadata.getSerializedOutput(); - } - - /** - * Gets the failure details, if any, for the failed workflow instance. - * - *

This method returns data only if the workflow is in the - * {@link OrchestrationRuntimeStatus#FAILED} state, - * and only if this instance metadata was fetched with the option to include - * output data. - * - * @return the failure details of the failed workflow instance or {@code null} - */ - @Nullable - public WorkflowFailureDetails getFailureDetails() { - return this.failureDetails; - } - - /** - * Gets a value indicating whether the workflow instance was running at the time - * this object was fetched. - * - * @return {@code true} if the workflow existed and was in a running state otherwise {@code false} - */ - public boolean isRunning() { - return orchestrationMetadata.isRunning(); - } - - /** - * Gets a value indicating whether the workflow instance was completed at the - * time this object was fetched. - * - *

A workflow instance is considered completed when its runtime status value is - * {@link WorkflowRuntimeStatus#COMPLETED}, - * {@link WorkflowRuntimeStatus#FAILED}, or - * {@link WorkflowRuntimeStatus#TERMINATED}. - * - * @return {@code true} if the workflow was in a terminal state; otherwise {@code false} - */ - public boolean isCompleted() { - return orchestrationMetadata.isCompleted(); - } - - /** - * Deserializes the workflow's input into an object of the specified type. - * - *

Deserialization is performed using the DataConverter that was - * configured on the DurableTaskClient object that created this workflow - * metadata object. - * - * @param type the class associated with the type to deserialize the input data - * into - * @param the type to deserialize the input data into - * @return the deserialized input value - * @throws IllegalStateException if the metadata was fetched without the option - * to read inputs and outputs - */ - public T readInputAs(Class type) { - return orchestrationMetadata.readInputAs(type); - } - - /** - * Deserializes the workflow's output into an object of the specified type. - * - *

Deserialization is performed using the DataConverter that was - * configured on the DurableTaskClient - * object that created this workflow metadata object. - * - * @param type the class associated with the type to deserialize the output data - * into - * @param the type to deserialize the output data into - * @return the deserialized input value - * @throws IllegalStateException if the metadata was fetched without the option - * to read inputs and outputs - */ - public T readOutputAs(Class type) { - return orchestrationMetadata.readOutputAs(type); - } - - /** - * Generates a user-friendly string representation of the current metadata - * object. - * - * @return a user-friendly string representation of the current metadata object - */ - public String toString() { - return orchestrationMetadata.toString(); - } - -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowState.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowState.java index 78420d4c81..615cb64350 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowState.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowState.java @@ -13,12 +13,10 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.FailureDetails; -import io.dapr.durabletask.OrchestrationMetadata; -import io.dapr.durabletask.OrchestrationRuntimeStatus; -import io.dapr.workflows.client.WorkflowFailureDetails; import io.dapr.workflows.client.WorkflowRuntimeStatus; import io.dapr.workflows.client.WorkflowState; +import io.dapr.workflows.task.client.OrchestrationMetadata; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; import javax.annotation.Nullable; @@ -46,13 +44,7 @@ public DefaultWorkflowState(OrchestrationMetadata orchestrationMetadata) { } this.orchestrationMetadata = orchestrationMetadata; - FailureDetails details = orchestrationMetadata.getFailureDetails(); - - if (details != null) { - this.failureDetails = new DefaultWorkflowFailureDetails(details); - } else { - this.failureDetails = null; - } + this.failureDetails = orchestrationMetadata.getFailureDetails(); } /** @@ -80,9 +72,7 @@ public String getWorkflowId() { * @return the current runtime status of the workflow instance at the time this object was fetched */ public WorkflowRuntimeStatus getRuntimeStatus() { - OrchestrationRuntimeStatus status = orchestrationMetadata.getRuntimeStatus(); - - return WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(status); + return orchestrationMetadata.getRuntimeStatus(); } /** @@ -125,7 +115,7 @@ public String getSerializedOutput() { * Gets the failure details, if any, for the failed workflow instance. * *

This method returns data only if the workflow is in the - * {@link OrchestrationRuntimeStatus#FAILED} state, + * {@link WorkflowRuntimeStatus#FAILED} state, * and only if this instance metadata was fetched with the option to include * output data. * diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapper.java index 43bd2ca203..43601138bb 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapper.java @@ -13,9 +13,9 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskActivity; -import io.dapr.durabletask.TaskActivityFactory; import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.task.TaskActivity; +import io.dapr.workflows.task.TaskActivityFactory; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java index 09d1a9d6ca..696fc14823 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapper.java @@ -13,9 +13,9 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskActivity; -import io.dapr.durabletask.TaskActivityFactory; import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.task.TaskActivity; +import io.dapr.workflows.task.TaskActivityFactory; /** * Wrapper for Durable Task Framework task activity factory. diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java index 8ac3789f98..6471008528 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowClassWrapper.java @@ -13,8 +13,8 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskOrchestration; import io.dapr.workflows.Workflow; +import io.dapr.workflows.task.TaskOrchestration; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -70,7 +70,7 @@ public TaskOrchestration create() { ); } - workflow.run(new DefaultWorkflowContext(ctx, workflow.getClass())); + workflow.run(ctx); }; } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java index 4dbd766246..773c2c30e3 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowInstanceWrapper.java @@ -13,8 +13,8 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskOrchestration; import io.dapr.workflows.Workflow; +import io.dapr.workflows.task.TaskOrchestration; /** * Wrapper for Durable Task Framework orchestration factory. @@ -41,6 +41,6 @@ public String getName() { @Override public TaskOrchestration create() { - return ctx -> workflow.run(new DefaultWorkflowContext(ctx, workflow.getClass())); + return ctx -> workflow.run(ctx); } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntime.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntime.java index 5f7221f6ff..28fb74dd3a 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntime.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntime.java @@ -13,8 +13,8 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.DurableTaskGrpcWorker; import io.dapr.workflows.internal.GrpcChannelKeepalive; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; import io.grpc.ManagedChannel; import javax.annotation.Nullable; @@ -31,6 +31,7 @@ public class WorkflowRuntime implements AutoCloseable { private final ManagedChannel managedChannel; private final ExecutorService executorService; private final GrpcChannelKeepalive keepalive; + private final boolean ownsExecutorService; /** * Constructor. @@ -59,10 +60,33 @@ public WorkflowRuntime(DurableTaskGrpcWorker worker, ManagedChannel managedChannel, ExecutorService executorService, @Nullable GrpcChannelKeepalive keepalive) { + this(worker, managedChannel, executorService, keepalive, true); + } + + /** + * Constructor. + * + * @param worker grpcWorker processing activities. + * @param managedChannel grpc channel. + * @param executorService executor service responsible for running the threads. + * @param keepalive application-level keepalive on the worker's channel, started with + * {@link #start()} and stopped on {@link #close()}. May be null when + * no keepalive is wanted. + * @param ownsExecutorService whether this runtime created the executor and is therefore + * responsible for shutting it down on {@link #close()}. Pass + * false for an executor owned by the caller or by a framework + * such as Spring, which must outlive this runtime. + */ + public WorkflowRuntime(DurableTaskGrpcWorker worker, + ManagedChannel managedChannel, + ExecutorService executorService, + @Nullable GrpcChannelKeepalive keepalive, + boolean ownsExecutorService) { this.worker = worker; this.managedChannel = managedChannel; this.executorService = executorService; this.keepalive = keepalive; + this.ownsExecutorService = ownsExecutorService; } /** @@ -114,6 +138,10 @@ private void closeSideCarChannel() { } private void shutDownWorkerPool() { + if (!this.ownsExecutorService) { + return; + } + this.executorService.shutdown(); try { if (!this.executorService.awaitTermination(60, TimeUnit.SECONDS)) { diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java index dc298e9cf4..14c28d1b37 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java @@ -14,14 +14,15 @@ package io.dapr.workflows.runtime; import io.dapr.config.Properties; -import io.dapr.durabletask.DurableTaskGrpcWorkerBuilder; -import io.dapr.durabletask.TaskActivityFactory; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; import io.dapr.utils.NetworkUtils; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.internal.ApiTokenClientInterceptor; +import io.dapr.workflows.internal.DefaultExecutorService; import io.dapr.workflows.internal.GrpcChannelKeepalive; +import io.dapr.workflows.task.TaskActivityFactory; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannel; import org.apache.commons.lang3.StringUtils; @@ -33,7 +34,6 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; public class WorkflowRuntimeBuilder { private ClientInterceptor workflowApiTokenInterceptor; @@ -84,7 +84,10 @@ private WorkflowRuntimeBuilder(Properties properties, Logger logger) { public WorkflowRuntime build() { if (instance == null) { synchronized (WorkflowRuntime.class) { - this.executorService = this.executorService == null ? Executors.newCachedThreadPool() : this.executorService; + boolean ownsExecutorService = this.executorService == null; + if (ownsExecutorService) { + this.executorService = DefaultExecutorService.create(); + } if (instance == null) { GrpcChannelKeepalive keepalive = null; if (this.properties.getValue(Properties.WORKFLOWS_RUNTIME_APP_KEEP_ALIVE_ENABLED)) { @@ -93,7 +96,7 @@ public WorkflowRuntime build() { } instance = new WorkflowRuntime( this.builder.withExecutorService(this.executorService).build(), - this.managedChannel, this.executorService, keepalive); + this.managedChannel, this.executorService, keepalive, ownsExecutorService); } } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverter.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverter.java deleted file mode 100644 index 2900916aaf..0000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverter.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.runtime; - -import io.dapr.durabletask.OrchestrationRuntimeStatus; -import io.dapr.workflows.client.WorkflowRuntimeStatus; - -public class WorkflowRuntimeStatusConverter { - - private WorkflowRuntimeStatusConverter() { - } - - /** - * Converts an OrchestrationRuntimeStatus to a WorkflowRuntimeStatus. - * - * @param status the OrchestrationRuntimeStatus to convert - * @return the corresponding WorkflowRuntimeStatus - * @throws IllegalArgumentException if the status is null or unknown - */ - public static WorkflowRuntimeStatus fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus status) { - if (status == null) { - throw new IllegalArgumentException("status cannot be null"); - } - - switch (status) { - case RUNNING: - return WorkflowRuntimeStatus.RUNNING; - case COMPLETED: - return WorkflowRuntimeStatus.COMPLETED; - case CONTINUED_AS_NEW: - return WorkflowRuntimeStatus.CONTINUED_AS_NEW; - case FAILED: - return WorkflowRuntimeStatus.FAILED; - case CANCELED: - return WorkflowRuntimeStatus.CANCELED; - case TERMINATED: - return WorkflowRuntimeStatus.TERMINATED; - case PENDING: - return WorkflowRuntimeStatus.PENDING; - case SUSPENDED: - return WorkflowRuntimeStatus.SUSPENDED; - default: - throw new IllegalArgumentException(String.format("Unknown status value: %s", status)); - } - } - -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowVersionWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowVersionWrapper.java index 4683ebc4dd..846788bfd5 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowVersionWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowVersionWrapper.java @@ -13,7 +13,7 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; public abstract class WorkflowVersionWrapper implements TaskOrchestrationFactory { private final String versionName; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestratorFunction.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/OrchestratorFunction.java similarity index 93% rename from durabletask-client/src/main/java/io/dapr/durabletask/OrchestratorFunction.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/OrchestratorFunction.java index a4d2f2f087..ce76c7bbe8 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestratorFunction.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/OrchestratorFunction.java @@ -11,7 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; + +import io.dapr.workflows.WorkflowContext; /** * Functional interface for inline orchestrator functions. @@ -34,5 +36,5 @@ public interface OrchestratorFunction { * execution * @return the serializable output of the orchestrator function */ - R apply(TaskOrchestrationContext ctx); + R apply(WorkflowContext ctx); } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/Task.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/Task.java similarity index 90% rename from durabletask-client/src/main/java/io/dapr/durabletask/Task.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/Task.java index de2f13e871..2ba058d9ad 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/Task.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/Task.java @@ -11,9 +11,11 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; -import io.dapr.durabletask.interruption.OrchestratorBlockedException; +import io.dapr.workflows.WorkflowContext; +import io.dapr.workflows.task.exception.TaskFailedException; +import io.dapr.workflows.task.interruption.OrchestratorBlockedException; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -22,7 +24,7 @@ /** * Represents an asynchronous operation in a durable orchestration. * - *

{@code Task} instances are created by methods on the {@link TaskOrchestrationContext} class, which is available + *

{@code Task} instances are created by methods on the {@link WorkflowContext} class, which is available * in {@link TaskOrchestration} implementations. For example, scheduling an activity will return a task.

*
  * Task{@literal <}int{@literal >} activityTask = ctx.callActivity("MyActivity", someInput, int.class);
@@ -41,9 +43,9 @@
  * @param  the return type of the task
  */
 public abstract class Task {
-  final CompletableFuture future;
+  public final CompletableFuture future;
 
-  Task(CompletableFuture future) {
+  public Task(CompletableFuture future) {
     this.future = future;
   }
 
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivity.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivity.java
similarity index 94%
rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskActivity.java
rename to sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivity.java
index 27e4291e95..7ba6aed8b9 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivity.java
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivity.java
@@ -11,7 +11,10 @@
 limitations under the License.
 */
 
-package io.dapr.durabletask;
+package io.dapr.workflows.task;
+
+import io.dapr.workflows.WorkflowContext;
+
 
 /**
  * Common interface for task activity implementations.
@@ -29,7 +32,7 @@
  * 

Because activities only guarantee at least once execution, it's recommended that activity logic be implemented as * idempotent whenever possible.

* - *

Activities are scheduled by orchestrators using one of the {@link TaskOrchestrationContext#callActivity} method + *

Activities are scheduled by orchestrators using one of the {@link WorkflowContext#callActivity} method * overloads.

*/ @FunctionalInterface diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivityContext.java similarity index 95% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityContext.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivityContext.java index d8b658ed1c..b69da106da 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivityContext.java @@ -11,7 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; + +import io.dapr.workflows.task.history.PropagatedHistory; import java.util.Optional; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityFactory.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivityFactory.java similarity index 96% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityFactory.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivityFactory.java index e3ef45a95b..efea45fc63 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityFactory.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskActivityFactory.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; /** * Factory interface for producing {@link TaskActivity} implementations. diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestration.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskOrchestration.java similarity index 85% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestration.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/TaskOrchestration.java index 8935313779..6c4c24728a 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestration.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/TaskOrchestration.java @@ -11,7 +11,10 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; + +import io.dapr.workflows.WorkflowContext; +import io.dapr.workflows.task.client.DurableTaskClient; /** * Common interface for task orchestrator implementations. @@ -22,7 +25,7 @@ * *

Orchestrators can be scheduled using the {@link DurableTaskClient#scheduleNewOrchestrationInstance} method * overloads. Orchestrators can also invoke child orchestrators using the - * {@link TaskOrchestrationContext#callSubOrchestrator} method overloads.

+ * {@link WorkflowContext#callChildWorkflow} method overloads.

* *

Orchestrators may be replayed multiple times to rebuild their local state after being reloaded into memory. * Orchestrator code must therefore be deterministic to ensure no unexpected side effects from execution @@ -31,8 +34,8 @@ *

  • * An orchestrator must not generate random numbers or random UUIDs, get the current date, read environment * variables, or do anything else that might result in a different value if the code is replayed in the future. - * Activities and built-in methods on the {@link TaskOrchestrationContext} parameter, like - * {@link TaskOrchestrationContext#getCurrentInstant()}, can be used to work around these restrictions. + * Activities and built-in methods on the {@link WorkflowContext} parameter, like + * {@link WorkflowContext#getCurrentInstant()}, can be used to work around these restrictions. *
  • *
  • * Orchestrator logic must be executed on the orchestrator thread. Creating new threads or scheduling callbacks @@ -40,11 +43,11 @@ *
  • *
  • * Avoid infinite loops as they could cause the application to run out of memory. Instead, ensure that loops are - * bounded or use {@link TaskOrchestrationContext#continueAsNew} to restart an orchestrator with a new input. + * bounded or use {@link WorkflowContext#continueAsNew} to restart an orchestrator with a new input. *
  • *
  • * Avoid logging directly in the orchestrator code because log messages will be duplicated on each replay. - * Instead, check the value of the {@link TaskOrchestrationContext#getIsReplaying} method and write log messages + * Instead, check the value of the {@link WorkflowContext#isReplaying} method and write log messages * only when it is {@code false}. *
  • * @@ -78,5 +81,5 @@ public interface TaskOrchestration { * @param ctx provides access to methods for scheduling durable tasks and getting information about the current * orchestration instance. */ - void run(TaskOrchestrationContext ctx); + void run(WorkflowContext ctx); } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskClient.java similarity index 97% rename from durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskClient.java index b08ffcc53c..4852882017 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskClient.java @@ -11,7 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.client; + +import io.dapr.workflows.WorkflowContext; import javax.annotation.Nullable; @@ -96,10 +98,10 @@ public abstract String scheduleNewOrchestrationInstance( * Sends an event notification message to a waiting orchestration instance. * *

    In order to handle the event, the target orchestration instance must be waiting for an event named - * eventName using the {@link TaskOrchestrationContext#waitForExternalEvent(String)} method. + * eventName using the {@link WorkflowContext#waitForExternalEvent(String)} method. * If the target orchestration instance is not yet waiting for an event named eventName, * then the event will be saved in the orchestration instance state and dispatched immediately when the - * orchestrator calls {@link TaskOrchestrationContext#waitForExternalEvent(String)}. This event saving occurs even + * orchestrator calls {@link WorkflowContext#waitForExternalEvent(String)}. This event saving occurs even * if the orchestrator has canceled its wait operation before the event was received.

    * *

    Raised events for a completed or non-existent orchestration instance will be silently discarded.

    @@ -115,10 +117,10 @@ public void raiseEvent(String instanceId, String eventName) { * Sends an event notification message with a payload to a waiting orchestration instance. * *

    In order to handle the event, the target orchestration instance must be waiting for an event named - * eventName using the {@link TaskOrchestrationContext#waitForExternalEvent(String)} method. + * eventName using the {@link WorkflowContext#waitForExternalEvent(String)} method. * If the target orchestration instance is not yet waiting for an event named eventName, * then the event will be saved in the orchestration instance state and dispatched immediately when the - * orchestrator calls {@link TaskOrchestrationContext#waitForExternalEvent(String)}. This event saving occurs even + * orchestrator calls {@link WorkflowContext#waitForExternalEvent(String)}. This event saving occurs even * if the orchestrator has canceled its wait operation before the event was received.

    * *

    Raised events for a completed or non-existent orchestration instance will be silently discarded.

    diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskGrpcClient.java similarity index 98% rename from durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskGrpcClient.java index f258525915..34da72ac82 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskGrpcClient.java @@ -11,13 +11,17 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.client; import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.task.internal.Helpers; +import io.dapr.workflows.task.serialization.DataConverter; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import io.grpc.Channel; import io.grpc.ChannelCredentials; import io.grpc.Grpc; @@ -388,7 +392,7 @@ public PurgeResult purgeInstances(PurgeInstanceCriteria purgeInstanceCriteria) t builder.setCreatedTimeTo(DataConverter.getTimestampFromInstant(createdTimeTo))); purgeInstanceCriteria.getRuntimeStatusList().forEach(runtimeStatus -> Optional.ofNullable(runtimeStatus).ifPresent(status -> - builder.addRuntimeStatus(OrchestrationRuntimeStatus.toProtobuf(status)))); + builder.addRuntimeStatus(WorkflowRuntimeStatus.toProtobuf(status)))); Duration timeout = purgeInstanceCriteria.getTimeout(); if (timeout == null || timeout.isNegative() || timeout.isZero()) { diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClientBuilder.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskGrpcClientBuilder.java similarity index 96% rename from durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClientBuilder.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskGrpcClientBuilder.java index b934fc9713..7196f9b329 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClientBuilder.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/DurableTaskGrpcClientBuilder.java @@ -1,141 +1,142 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.durabletask; - -import io.grpc.Channel; -import io.opentelemetry.api.trace.Tracer; - -/** - * Builder class for constructing new {@link DurableTaskClient} objects that communicate with a sidecar process - * over gRPC. - */ -public final class DurableTaskGrpcClientBuilder { - DataConverter dataConverter; - int port; - Channel channel; - String tlsCaPath; - String tlsCertPath; - String tlsKeyPath; - boolean insecure; - Tracer tracer; - - /** - * Sets the {@link DataConverter} to use for converting serializable data payloads. - * - * @param dataConverter the {@link DataConverter} to use for converting serializable data payloads - * @return this builder object - */ - public DurableTaskGrpcClientBuilder dataConverter(DataConverter dataConverter) { - this.dataConverter = dataConverter; - return this; - } - - /** - * Sets the gRPC channel to use for communicating with the sidecar process. - * - *

    This builder method allows you to provide your own gRPC channel for communicating with the Durable Task sidecar - * endpoint. Channels provided using this method won't be closed when the client is closed. - * Rather, the caller remains responsible for shutting down the channel after disposing the client.

    - * - *

    If not specified, a gRPC channel will be created automatically for each constructed - * {@link DurableTaskClient}.

    - * - * @param channel the gRPC channel to use - * @return this builder object - */ - public DurableTaskGrpcClientBuilder grpcChannel(Channel channel) { - this.channel = channel; - return this; - } - - /** - * Sets the Tracer object to be used by DurableTaskClient to emit traces. - * - * @param tracer to be used by the DurableTaskClient - * @return this builder object - */ - public DurableTaskGrpcClientBuilder tracer(Tracer tracer) { - this.tracer = tracer; - return this; - } - - /** - * Sets the gRPC endpoint port to connect to. If not specified, the default Durable Task port number will be used. - * - * @param port the gRPC endpoint port to connect to - * @return this builder object - */ - public DurableTaskGrpcClientBuilder port(int port) { - this.port = port; - return this; - } - - /** - * Sets the path to the TLS CA certificate file for server authentication. - * If not set, the system's default CA certificates will be used. - * - * @param tlsCaPath path to the TLS CA certificate file - * @return this builder object - */ - public DurableTaskGrpcClientBuilder tlsCaPath(String tlsCaPath) { - this.tlsCaPath = tlsCaPath; - return this; - } - - /** - * Sets the path to the TLS client certificate file for client authentication. - * This is used for mTLS (mutual TLS) connections. - * - * @param tlsCertPath path to the TLS client certificate file - * @return this builder object - */ - public DurableTaskGrpcClientBuilder tlsCertPath(String tlsCertPath) { - this.tlsCertPath = tlsCertPath; - return this; - } - - /** - * Sets the path to the TLS client key file for client authentication. - * This is used for mTLS (mutual TLS) connections. - * - * @param tlsKeyPath path to the TLS client key file - * @return this builder object - */ - public DurableTaskGrpcClientBuilder tlsKeyPath(String tlsKeyPath) { - this.tlsKeyPath = tlsKeyPath; - return this; - } - - /** - * Sets whether to use insecure (plaintext) mode for gRPC communication. - * When set to true, TLS will be disabled and communication will be unencrypted. - * This should only be used for development/testing. - * - * @param insecure whether to use insecure mode - * @return this builder object - */ - public DurableTaskGrpcClientBuilder insecure(boolean insecure) { - this.insecure = insecure; - return this; - } - - /** - * Initializes a new {@link DurableTaskClient} object with the settings specified in the current builder object. - * - * @return a new {@link DurableTaskClient} object - */ - public DurableTaskClient build() { - return new DurableTaskGrpcClient(this); - } -} +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.task.client; + +import io.dapr.workflows.task.serialization.DataConverter; +import io.grpc.Channel; +import io.opentelemetry.api.trace.Tracer; + +/** + * Builder class for constructing new {@link DurableTaskClient} objects that communicate with a sidecar process + * over gRPC. + */ +public final class DurableTaskGrpcClientBuilder { + DataConverter dataConverter; + int port; + Channel channel; + String tlsCaPath; + String tlsCertPath; + String tlsKeyPath; + boolean insecure; + Tracer tracer; + + /** + * Sets the {@link DataConverter} to use for converting serializable data payloads. + * + * @param dataConverter the {@link DataConverter} to use for converting serializable data payloads + * @return this builder object + */ + public DurableTaskGrpcClientBuilder dataConverter(DataConverter dataConverter) { + this.dataConverter = dataConverter; + return this; + } + + /** + * Sets the gRPC channel to use for communicating with the sidecar process. + * + *

    This builder method allows you to provide your own gRPC channel for communicating with the Durable Task sidecar + * endpoint. Channels provided using this method won't be closed when the client is closed. + * Rather, the caller remains responsible for shutting down the channel after disposing the client.

    + * + *

    If not specified, a gRPC channel will be created automatically for each constructed + * {@link DurableTaskClient}.

    + * + * @param channel the gRPC channel to use + * @return this builder object + */ + public DurableTaskGrpcClientBuilder grpcChannel(Channel channel) { + this.channel = channel; + return this; + } + + /** + * Sets the Tracer object to be used by DurableTaskClient to emit traces. + * + * @param tracer to be used by the DurableTaskClient + * @return this builder object + */ + public DurableTaskGrpcClientBuilder tracer(Tracer tracer) { + this.tracer = tracer; + return this; + } + + /** + * Sets the gRPC endpoint port to connect to. If not specified, the default Durable Task port number will be used. + * + * @param port the gRPC endpoint port to connect to + * @return this builder object + */ + public DurableTaskGrpcClientBuilder port(int port) { + this.port = port; + return this; + } + + /** + * Sets the path to the TLS CA certificate file for server authentication. + * If not set, the system's default CA certificates will be used. + * + * @param tlsCaPath path to the TLS CA certificate file + * @return this builder object + */ + public DurableTaskGrpcClientBuilder tlsCaPath(String tlsCaPath) { + this.tlsCaPath = tlsCaPath; + return this; + } + + /** + * Sets the path to the TLS client certificate file for client authentication. + * This is used for mTLS (mutual TLS) connections. + * + * @param tlsCertPath path to the TLS client certificate file + * @return this builder object + */ + public DurableTaskGrpcClientBuilder tlsCertPath(String tlsCertPath) { + this.tlsCertPath = tlsCertPath; + return this; + } + + /** + * Sets the path to the TLS client key file for client authentication. + * This is used for mTLS (mutual TLS) connections. + * + * @param tlsKeyPath path to the TLS client key file + * @return this builder object + */ + public DurableTaskGrpcClientBuilder tlsKeyPath(String tlsKeyPath) { + this.tlsKeyPath = tlsKeyPath; + return this; + } + + /** + * Sets whether to use insecure (plaintext) mode for gRPC communication. + * When set to true, TLS will be disabled and communication will be unencrypted. + * This should only be used for development/testing. + * + * @param insecure whether to use insecure mode + * @return this builder object + */ + public DurableTaskGrpcClientBuilder insecure(boolean insecure) { + this.insecure = insecure; + return this; + } + + /** + * Initializes a new {@link DurableTaskClient} object with the settings specified in the current builder object. + * + * @return a new {@link DurableTaskClient} object + */ + public DurableTaskClient build() { + return new DurableTaskGrpcClient(this); + } +} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/NewOrchestrationInstanceOptions.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/NewOrchestrationInstanceOptions.java similarity index 98% rename from durabletask-client/src/main/java/io/dapr/durabletask/NewOrchestrationInstanceOptions.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/client/NewOrchestrationInstanceOptions.java index cac3c421ca..0b8a9a98e1 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/NewOrchestrationInstanceOptions.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/NewOrchestrationInstanceOptions.java @@ -11,7 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.client; + +import io.dapr.workflows.task.serialization.DataConverter; import java.time.Instant; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationMetadata.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/OrchestrationMetadata.java similarity index 89% rename from durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationMetadata.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/client/OrchestrationMetadata.java index 477b319959..e9efa27579 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationMetadata.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/OrchestrationMetadata.java @@ -11,14 +11,17 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.client; import io.dapr.durabletask.implementation.protobuf.Orchestration.WorkflowState; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; +import io.dapr.workflows.task.serialization.DataConverter; import java.time.Instant; -import static io.dapr.durabletask.Helpers.isNullOrEmpty; +import static io.dapr.workflows.task.internal.Helpers.isNullOrEmpty; /** * Represents a snapshot of an orchestration instance's current state, including metadata. @@ -33,13 +36,13 @@ public final class OrchestrationMetadata { private final String name; private final String instanceId; - private final OrchestrationRuntimeStatus runtimeStatus; + private final WorkflowRuntimeStatus runtimeStatus; private final Instant createdAt; private final Instant lastUpdatedAt; private final String serializedInput; private final String serializedOutput; private final String serializedCustomStatus; - private final FailureDetails failureDetails; + private final WorkflowFailureDetails failureDetails; OrchestrationMetadata( OrchestratorService.GetInstanceResponse fetchResponse, @@ -57,13 +60,13 @@ public final class OrchestrationMetadata { this.name = state.getName(); this.instanceId = state.getInstanceId(); - this.runtimeStatus = OrchestrationRuntimeStatus.fromProtobuf(state.getWorkflowStatus()); + this.runtimeStatus = WorkflowRuntimeStatus.fromProtobuf(state.getWorkflowStatus()); this.createdAt = DataConverter.getInstantFromTimestamp(state.getCreatedTimestamp()); this.lastUpdatedAt = DataConverter.getInstantFromTimestamp(state.getLastUpdatedTimestamp()); this.serializedInput = state.getInput().getValue(); this.serializedOutput = state.getOutput().getValue(); this.serializedCustomStatus = state.getCustomStatus().getValue(); - this.failureDetails = new FailureDetails(state.getFailureDetails()); + this.failureDetails = new WorkflowFailureDetails(state.getFailureDetails()); } /** @@ -89,7 +92,7 @@ public String getInstanceId() { * * @return the current runtime status of the orchestration instance at the time this object was fetched */ - public OrchestrationRuntimeStatus getRuntimeStatus() { + public WorkflowRuntimeStatus getRuntimeStatus() { return this.runtimeStatus; } @@ -132,12 +135,12 @@ public String getSerializedOutput() { /** * Gets the failure details, if any, for the failed orchestration instance. * - *

    This method returns data only if the orchestration is in the {@link OrchestrationRuntimeStatus#FAILED} state, + *

    This method returns data only if the orchestration is in the {@link WorkflowRuntimeStatus#FAILED} state, * and only if this instance metadata was fetched with the option to include output data.

    * * @return the failure details of the failed orchestration instance or {@code null} */ - public FailureDetails getFailureDetails() { + public WorkflowFailureDetails getFailureDetails() { return this.failureDetails; } @@ -147,23 +150,23 @@ public FailureDetails getFailureDetails() { * @return {@code true} if the orchestration existed and was in a running state; otherwise {@code false} */ public boolean isRunning() { - return isInstanceFound() && this.runtimeStatus == OrchestrationRuntimeStatus.RUNNING; + return isInstanceFound() && this.runtimeStatus == WorkflowRuntimeStatus.RUNNING; } /** * Gets a value indicating whether the orchestration instance was completed at the time this object was fetched. * *

    An orchestration instance is considered completed when its runtime status value is - * {@link OrchestrationRuntimeStatus#COMPLETED}, {@link OrchestrationRuntimeStatus#FAILED}, or - * {@link OrchestrationRuntimeStatus#TERMINATED}.

    + * {@link WorkflowRuntimeStatus#COMPLETED}, {@link WorkflowRuntimeStatus#FAILED}, or + * {@link WorkflowRuntimeStatus#TERMINATED}.

    * * @return {@code true} if the orchestration was in a terminal state; otherwise {@code false} */ public boolean isCompleted() { return - this.runtimeStatus == OrchestrationRuntimeStatus.COMPLETED - || this.runtimeStatus == OrchestrationRuntimeStatus.FAILED - || this.runtimeStatus == OrchestrationRuntimeStatus.TERMINATED; + this.runtimeStatus == WorkflowRuntimeStatus.COMPLETED + || this.runtimeStatus == WorkflowRuntimeStatus.FAILED + || this.runtimeStatus == WorkflowRuntimeStatus.TERMINATED; } /** diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/PurgeInstanceCriteria.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/PurgeInstanceCriteria.java similarity index 91% rename from durabletask-client/src/main/java/io/dapr/durabletask/PurgeInstanceCriteria.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/client/PurgeInstanceCriteria.java index 50260c1fc7..1fde8d7aa7 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/PurgeInstanceCriteria.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/PurgeInstanceCriteria.java @@ -11,9 +11,12 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.client; + +import io.dapr.workflows.client.WorkflowRuntimeStatus; import javax.annotation.Nullable; + import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -26,7 +29,7 @@ public final class PurgeInstanceCriteria { private Instant createdTimeFrom; private Instant createdTimeTo; - private List runtimeStatusList = new ArrayList<>(); + private List runtimeStatusList = new ArrayList<>(); private Duration timeout; /** @@ -66,7 +69,7 @@ public PurgeInstanceCriteria setCreatedTimeTo(Instant createdTimeTo) { * @param runtimeStatusList the list of runtime status values to use as a selection criteria * @return this criteria object */ - public PurgeInstanceCriteria setRuntimeStatusList(List runtimeStatusList) { + public PurgeInstanceCriteria setRuntimeStatusList(List runtimeStatusList) { this.runtimeStatusList = runtimeStatusList; return this; } @@ -108,7 +111,7 @@ public Instant getCreatedTimeTo() { * * @return the configured runtime status filter as a list of values */ - public List getRuntimeStatusList() { + public List getRuntimeStatusList() { return this.runtimeStatusList; } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/PurgeResult.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/PurgeResult.java similarity index 92% rename from durabletask-client/src/main/java/io/dapr/durabletask/PurgeResult.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/client/PurgeResult.java index 8d35218661..80bcba83c5 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/PurgeResult.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/client/PurgeResult.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.client; /** * Class representing the results of an orchestration state purge operation. @@ -22,7 +22,7 @@ public final class PurgeResult { private final int deletedInstanceCount; - PurgeResult(int deletedInstanceCount) { + public PurgeResult(int deletedInstanceCount) { this.deletedInstanceCount = deletedInstanceCount; } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/CompositeTaskFailedException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/CompositeTaskFailedException.java similarity index 72% rename from durabletask-client/src/main/java/io/dapr/durabletask/CompositeTaskFailedException.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/exception/CompositeTaskFailedException.java index d57ea37d2d..f26ef8f39e 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/CompositeTaskFailedException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/CompositeTaskFailedException.java @@ -11,7 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.exception; + +import io.dapr.workflows.task.Task; import java.util.ArrayList; import java.util.List; @@ -26,31 +28,31 @@ public class CompositeTaskFailedException extends RuntimeException { private final List exceptions; - CompositeTaskFailedException() { + public CompositeTaskFailedException() { this.exceptions = new ArrayList<>(); } - CompositeTaskFailedException(List exceptions) { + public CompositeTaskFailedException(List exceptions) { this.exceptions = exceptions; } - CompositeTaskFailedException(String message, List exceptions) { + public CompositeTaskFailedException(String message, List exceptions) { super(message); this.exceptions = exceptions; } - CompositeTaskFailedException(String message, Throwable cause, List exceptions) { + public CompositeTaskFailedException(String message, Throwable cause, List exceptions) { super(message, cause); this.exceptions = exceptions; } - CompositeTaskFailedException(Throwable cause, List exceptions) { + public CompositeTaskFailedException(Throwable cause, List exceptions) { super(cause); this.exceptions = exceptions; } - CompositeTaskFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, - List exceptions) { + public CompositeTaskFailedException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace, List exceptions) { super(message, cause, enableSuppression, writableStackTrace); this.exceptions = exceptions; } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/NonDeterministicOrchestratorException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/NonDeterministicOrchestratorException.java similarity index 84% rename from durabletask-client/src/main/java/io/dapr/durabletask/NonDeterministicOrchestratorException.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/exception/NonDeterministicOrchestratorException.java index 101e6bd04a..39c12f8ed0 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/NonDeterministicOrchestratorException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/NonDeterministicOrchestratorException.java @@ -11,9 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.exception; -final class NonDeterministicOrchestratorException extends RuntimeException { +public final class NonDeterministicOrchestratorException extends RuntimeException { public NonDeterministicOrchestratorException(String message) { super(message); } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskCanceledException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskCanceledException.java similarity index 78% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskCanceledException.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskCanceledException.java index 5b79882ed8..2e4e1ae778 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskCanceledException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskCanceledException.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.exception; //@TODO: This should inherit from Exception, not TaskFailedException @@ -20,7 +20,8 @@ */ public final class TaskCanceledException extends TaskFailedException { // Only intended to be created within this package - TaskCanceledException(String message, String taskName, int taskId) { - super(message, taskName, taskId, new FailureDetails(TaskCanceledException.class.getName(), message, "", true)); + public TaskCanceledException(String message, String taskName, int taskId) { + super(message, taskName, taskId, + new WorkflowFailureDetails(TaskCanceledException.class.getName(), message, "", true)); } } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskFailedException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskFailedException.java similarity index 74% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskFailedException.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskFailedException.java index 5362e830c7..a26f1385a9 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskFailedException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskFailedException.java @@ -11,7 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.exception; + +import io.dapr.workflows.task.Task; /** * Exception that gets thrown when awaiting a {@link Task} for an activity or sub-orchestration that fails with an @@ -21,15 +23,26 @@ * using the {@link #getErrorDetails()} method.

    */ public class TaskFailedException extends RuntimeException { - private final FailureDetails details; + private final WorkflowFailureDetails details; private final String taskName; private final int taskId; - TaskFailedException(String taskName, int taskId, FailureDetails details) { + public TaskFailedException(String taskName, int taskId, WorkflowFailureDetails details) { this(getExceptionMessage(taskName, taskId, details), taskName, taskId, details); } - TaskFailedException(String message, String taskName, int taskId, FailureDetails details) { + /** + * Creates a task failure with an explicit message. + * + *

    Public so the workflow executor in a sibling package can reach it; not intended for + * application code. + * + * @param message the exception message. + * @param taskName the name of the failed task. + * @param taskId the id of the failed task. + * @param details details of the failure. + */ + public TaskFailedException(String message, String taskName, int taskId, WorkflowFailureDetails details) { super(message); this.taskName = taskName; this.taskId = taskId; @@ -64,11 +77,11 @@ public String getTaskName() { * * @return the details of the task failure */ - public FailureDetails getErrorDetails() { + public WorkflowFailureDetails getErrorDetails() { return this.details; } - private static String getExceptionMessage(String taskName, int taskId, FailureDetails details) { + private static String getExceptionMessage(String taskName, int taskId, WorkflowFailureDetails details) { return String.format("Task '%s' (#%d) failed with an unhandled exception: %s", taskName, taskId, diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/exception/VersionNotRegisteredException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/VersionNotRegisteredException.java similarity index 92% rename from durabletask-client/src/main/java/io/dapr/durabletask/orchestration/exception/VersionNotRegisteredException.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/exception/VersionNotRegisteredException.java index f69ad9ea65..955e978183 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/exception/VersionNotRegisteredException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/VersionNotRegisteredException.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask.orchestration.exception; +package io.dapr.workflows.task.exception; public class VersionNotRegisteredException extends RuntimeException { } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/FailureDetails.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/WorkflowFailureDetails.java similarity index 54% rename from durabletask-client/src/main/java/io/dapr/durabletask/FailureDetails.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/exception/WorkflowFailureDetails.java index 897ac7f4e6..dc1be9634f 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/FailureDetails.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/WorkflowFailureDetails.java @@ -11,14 +11,24 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.exception; import com.google.protobuf.StringValue; import io.dapr.durabletask.implementation.protobuf.Orchestration.TaskFailureDetails; +import io.dapr.workflows.task.history.PropagatedHistoryException; +import io.dapr.workflows.task.interruption.ContinueAsNewInterruption; +import io.dapr.workflows.task.interruption.OrchestratorBlockedException; +import io.dapr.workflows.task.serialization.DataConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + /** * Class that represents the details of a task failure. * @@ -26,13 +36,64 @@ * instances of this class will expose the details of the exception. However, it's also possible that other types * of errors could result in task failures, in which case there may not be any exception-specific information.

    */ -public final class FailureDetails { +public final class WorkflowFailureDetails { + + private static final Logger LOGGER = LoggerFactory.getLogger(WorkflowFailureDetails.class); + + /** + * Error types written into workflow history by SDK versions that shipped these exceptions under + * {@code io.dapr.durabletask}, mapped to where they live now. + * + *

    Includes the nested {@code DataConverter$DataConverterException}; enumerating the legacy + * types by source file name misses it, because a nested class has no file of its own. + * + *

    The error type is the exception's fully qualified name and it is persisted, so a workflow + * started before the durable task client was folded into this module carries the old names in its + * history. Without this mapping {@link #isCausedBy(Class)} would silently answer {@code false} for + * those instances after an upgrade, sending compensation logic down the wrong branch. + */ + private static final Map LEGACY_ERROR_TYPES = legacyErrorTypes(); + + private static Map legacyErrorTypes() { + Map legacy = new HashMap<>(); + legacy.put("io.dapr.durabletask.TaskFailedException", TaskFailedException.class.getName()); + legacy.put("io.dapr.durabletask.TaskCanceledException", TaskCanceledException.class.getName()); + legacy.put("io.dapr.durabletask.CompositeTaskFailedException", + CompositeTaskFailedException.class.getName()); + legacy.put("io.dapr.durabletask.NonDeterministicOrchestratorException", + NonDeterministicOrchestratorException.class.getName()); + legacy.put("io.dapr.durabletask.PropagatedHistoryException", + PropagatedHistoryException.class.getName()); + legacy.put("io.dapr.durabletask.orchestration.exception.VersionNotRegisteredException", + VersionNotRegisteredException.class.getName()); + legacy.put("io.dapr.durabletask.interruption.OrchestratorBlockedException", + OrchestratorBlockedException.class.getName()); + legacy.put("io.dapr.durabletask.interruption.ContinueAsNewInterruption", + ContinueAsNewInterruption.class.getName()); + // Nested inside the DataConverter interface, so its binary name uses '$'. Easy to miss when + // enumerating exception types by file name - it has no file of its own. + legacy.put("io.dapr.durabletask.DataConverter$DataConverterException", + DataConverter.DataConverterException.class.getName()); + return Collections.unmodifiableMap(legacy); + } + private final String errorType; private final String errorMessage; private final String stackTrace; private final boolean isNonRetriable; - FailureDetails( + /** + * Creates failure details from their individual parts. + * + *

    Public so the workflow executor in a sibling package can reach it; not intended for + * application code. + * + * @param errorType the namespace-qualified exception type name. + * @param errorMessage the error message, if any. + * @param errorDetails the stack trace, if any. + * @param isNonRetriable whether the failure must not be retried. + */ + public WorkflowFailureDetails( String errorType, @Nullable String errorMessage, @Nullable String errorDetails, @@ -45,11 +106,19 @@ public final class FailureDetails { this.isNonRetriable = isNonRetriable; } - FailureDetails(Exception exception) { + public WorkflowFailureDetails(Exception exception) { this(exception.getClass().getName(), exception.getMessage(), getFullStackTrace(exception), false); } - FailureDetails(TaskFailureDetails proto) { + /** + * Creates failure details from their protobuf form. + * + *

    Public so the workflow executor in a sibling package can reach it; not intended for + * application code. + * + * @param proto the protobuf failure details. + */ + public WorkflowFailureDetails(TaskFailureDetails proto) { this(proto.getErrorType(), proto.getErrorMessage(), proto.getStackTrace().getValue(), @@ -113,13 +182,19 @@ public boolean isNonRetriable() { */ public boolean isCausedBy(Class exceptionClass) { String actualClassName = this.getErrorType(); + String resolvedClassName = LEGACY_ERROR_TYPES.getOrDefault(actualClassName, actualClassName); + try { // Try using reflection to load the failure's class type and see if it's a subtype of the specified // exception. For example, this should always succeed if exceptionClass is System.Exception. - Class actualExceptionClass = Class.forName(actualClassName); + Class actualExceptionClass = Class.forName(resolvedClassName); return exceptionClass.isAssignableFrom(actualExceptionClass); } catch (ClassNotFoundException ex) { - // Can't load the class and thus can't tell if it's related + // Can't load the class and thus can't tell if it's related. Say so rather than failing silently: + // an unloadable error type is usually history written by a different application or SDK version, + // and a quiet false here is indistinguishable from a genuine "not caused by". + LOGGER.warn("Cannot determine whether failure type '{}' is a {}: the class is not on the " + + "classpath, so isCausedBy is answering false.", actualClassName, exceptionClass.getName()); return false; } } @@ -141,7 +216,15 @@ public static String getFullStackTrace(Throwable e) { return sb.toString(); } - TaskFailureDetails toProto() { + /** + * Converts these failure details to their protobuf form. + * + *

    Public so the workflow executor in a sibling package can reach it; not intended for + * application code. + * + * @return the protobuf failure details. + */ + public TaskFailureDetails toProto() { return TaskFailureDetails.newBuilder() .setErrorType(this.getErrorType()) .setErrorMessage(this.getErrorMessage()) diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/ActivityResult.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/ActivityResult.java similarity index 98% rename from durabletask-client/src/main/java/io/dapr/durabletask/ActivityResult.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/history/ActivityResult.java index a81a53782e..10fe13ec08 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/ActivityResult.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/ActivityResult.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.history; import com.google.protobuf.StringValue; import io.dapr.durabletask.implementation.protobuf.Orchestration.TaskFailureDetails; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/ChildWorkflowResult.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/ChildWorkflowResult.java similarity index 98% rename from durabletask-client/src/main/java/io/dapr/durabletask/ChildWorkflowResult.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/history/ChildWorkflowResult.java index 1bc015858e..add4717ce2 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/ChildWorkflowResult.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/ChildWorkflowResult.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.history; import com.google.protobuf.StringValue; import io.dapr.durabletask.implementation.protobuf.Orchestration.TaskFailureDetails; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/HistoryPropagationScope.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/HistoryPropagationScope.java similarity index 85% rename from durabletask-client/src/main/java/io/dapr/durabletask/HistoryPropagationScope.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/history/HistoryPropagationScope.java index 1dc6009c47..b5f6ebf16c 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/HistoryPropagationScope.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/HistoryPropagationScope.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.history; import io.dapr.durabletask.implementation.protobuf.Orchestration; @@ -36,7 +36,15 @@ public enum HistoryPropagationScope { */ LINEAGE; - Orchestration.HistoryPropagationScope toProto() { + /** + * Converts this scope to its protobuf form. + * + *

    Public so the workflow executor in a sibling package can reach it; not intended for + * application code. + * + * @return the protobuf scope. + */ + public Orchestration.HistoryPropagationScope toProto() { switch (this) { case OWN_HISTORY: return Orchestration.HistoryPropagationScope.HISTORY_PROPAGATION_SCOPE_OWN_HISTORY; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/PropagatedHistory.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/PropagatedHistory.java similarity index 93% rename from durabletask-client/src/main/java/io/dapr/durabletask/PropagatedHistory.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/history/PropagatedHistory.java index 7aef8b8bec..2d8e6c36cf 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/PropagatedHistory.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/PropagatedHistory.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.history; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; @@ -38,7 +38,16 @@ public final class PropagatedHistory { this.workflows = Collections.unmodifiableList(new ArrayList<>(workflows)); } - static PropagatedHistory fromProto(HistoryEvents.PropagatedHistory proto) { + /** + * Builds a propagated history from its protobuf form. + * + *

    Public so the workflow executor in a sibling package can reach it; not intended for + * application code. + * + * @param proto the protobuf history. + * @return the propagated history. + */ + public static PropagatedHistory fromProto(HistoryEvents.PropagatedHistory proto) { List workflows = proto.getChunksList().stream() .map(WorkflowResult::fromProto) .collect(Collectors.toList()); diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/PropagatedHistoryException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/PropagatedHistoryException.java similarity index 96% rename from durabletask-client/src/main/java/io/dapr/durabletask/PropagatedHistoryException.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/history/PropagatedHistoryException.java index 89256bd9b6..f6d3ff2cc1 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/PropagatedHistoryException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/PropagatedHistoryException.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.history; /** * Thrown when propagated history received from a parent workflow cannot be diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/WorkflowResult.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/WorkflowResult.java similarity index 99% rename from durabletask-client/src/main/java/io/dapr/durabletask/WorkflowResult.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/history/WorkflowResult.java index edbe9e2020..6a0d655450 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/WorkflowResult.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/history/WorkflowResult.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.history; import com.google.protobuf.InvalidProtocolBufferException; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/Helpers.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/Helpers.java new file mode 100644 index 0000000000..1258cc0e7b --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/Helpers.java @@ -0,0 +1,124 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.task.internal; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import java.time.Duration; + +/** + * Internal argument and arithmetic helpers shared across the workflow task packages. + * + *

    Public only so the sibling packages can reach it. Not part of the supported API. + */ +public final class Helpers { + /** Sentinel duration meaning "no timeout". */ + public static final Duration maxDuration = Duration.ofSeconds(Long.MAX_VALUE, 999999999L); + + /** + * Throws when the argument is null. + * + * @param argValue the value to check. + * @param argName the argument name used in the error message. + * @param the argument type. + * @return the argument, when non-null. + */ + public static @Nonnull V throwIfArgumentNull(@Nullable V argValue, String argName) { + if (argValue == null) { + throw new IllegalArgumentException("The argument '" + argName + "' was null."); + } + + return argValue; + } + + /** + * Throws when the argument is null, empty, or only whitespace. + * + * @param argValue the value to check. + * @param argName the argument name used in the error message. + * @return the argument, when non-blank. + */ + public static @Nonnull String throwIfArgumentNullOrWhiteSpace(String argValue, String argName) { + throwIfArgumentNull(argValue, argName); + if (argValue.trim().length() == 0) { + throw new IllegalArgumentException("The argument '" + argName + "' was empty or contained only whitespace."); + } + + return argValue; + } + + /** + * Throws when the orchestrator has already completed. + * + * @param isComplete whether the orchestrator has completed. + */ + public static void throwIfOrchestratorComplete(boolean isComplete) { + if (isComplete) { + throw new IllegalStateException("The orchestrator has already completed"); + } + } + + /** + * Indicates whether a timeout means "wait forever". + * + * @param timeout the timeout to test; may be null. + * @return true when the timeout is null, negative, or the maximum duration. + */ + public static boolean isInfiniteTimeout(Duration timeout) { + return timeout == null || timeout.isNegative() || timeout.equals(maxDuration); + } + + /** + * Raises base to exponent, failing loudly on overflow rather than returning infinity or zero. + * + * @param base the base. + * @param exponent the exponent. + * @return the result. + * @throws ArithmeticException when the result overflows. + */ + public static double powExact(double base, double exponent) throws ArithmeticException { + if (base == 0.0) { + return 0.0; + } + + double result = Math.pow(base, exponent); + + if (result == Double.POSITIVE_INFINITY) { + throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY"); + } else if (result == Double.NEGATIVE_INFINITY) { + throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY"); + } else if (Double.compare(-0.0f, result) == 0) { + throw new ArithmeticException("Double overflow resulting in negative zero"); + } else if (Double.compare(+0.0f, result) == 0) { + throw new ArithmeticException("Double overflow resulting in positive zero"); + } + + return result; + } + + /** + * Indicates whether a string is null or empty. + * + * @param s the string to test. + * @return true when null or empty. + */ + public static boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } + + // Cannot be instantiated + private Helpers() { + } +} diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityExecutor.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskActivityExecutor.java similarity index 94% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityExecutor.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskActivityExecutor.java index 51222d98ad..bb82059371 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityExecutor.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskActivityExecutor.java @@ -11,11 +11,18 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; +import io.dapr.workflows.task.TaskActivity; +import io.dapr.workflows.task.TaskActivityContext; +import io.dapr.workflows.task.TaskActivityFactory; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.PropagatedHistoryException; +import io.dapr.workflows.task.serialization.DataConverter; import javax.annotation.Nullable; + import java.util.HashMap; import java.util.Optional; import java.util.logging.Logger; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskOrchestrationExecutor.java similarity index 95% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskOrchestrationExecutor.java index 1501856fbc..49e9897d01 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskOrchestrationExecutor.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; @@ -19,15 +19,30 @@ import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions.ScheduleTaskAction.Builder; -import io.dapr.durabletask.interruption.ContinueAsNewInterruption; -import io.dapr.durabletask.interruption.OrchestratorBlockedException; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; -import io.dapr.durabletask.orchestration.exception.VersionNotRegisteredException; -import io.dapr.durabletask.util.UuidGenerator; +import io.dapr.workflows.WorkflowContext; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryContext; +import io.dapr.workflows.WorkflowTaskRetryHandler; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import io.dapr.workflows.task.Task; +import io.dapr.workflows.task.TaskOrchestration; +import io.dapr.workflows.task.exception.CompositeTaskFailedException; +import io.dapr.workflows.task.exception.NonDeterministicOrchestratorException; +import io.dapr.workflows.task.exception.TaskCanceledException; +import io.dapr.workflows.task.exception.TaskFailedException; +import io.dapr.workflows.task.exception.VersionNotRegisteredException; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.PropagatedHistoryException; +import io.dapr.workflows.task.interruption.ContinueAsNewInterruption; +import io.dapr.workflows.task.interruption.OrchestratorBlockedException; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.DataConverter; import org.apache.commons.lang3.StringUtils; import javax.annotation.Nullable; + import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; @@ -166,12 +181,12 @@ public TaskOrchestratorResult execute(List pastEvent context.complete(null); } catch (PropagatedHistoryException propagatedHistoryException) { logger.warning("The orchestrator failed parsing propagated history: " + propagatedHistoryException); - context.fail(new FailureDetails(propagatedHistoryException)); + context.fail(new WorkflowFailureDetails(propagatedHistoryException)); } catch (Exception e) { // The orchestrator threw an unhandled exception - fail it // TODO: What's the right way to log this? logger.warning("The orchestrator failed with an unhandled exception: " + e); - context.fail(new FailureDetails(e)); + context.fail(new WorkflowFailureDetails(e)); } if ((context.continuedAsNew && !context.isComplete) || (completed @@ -188,7 +203,7 @@ public TaskOrchestratorResult execute(List pastEvent context.encounteredPatches); } - private class ContextImplTask implements TaskOrchestrationContext { + private class ContextImplTask implements WorkflowContext { private String orchestratorName; private final List encounteredPatches = new ArrayList<>(); @@ -285,7 +300,7 @@ private boolean hasSourceAppId() { return this.appId != null && !this.appId.isEmpty(); } - private boolean hasTargetAppId(TaskOptions options) { + private boolean hasTargetAppId(WorkflowTaskOptions options) { return options != null && options.hasAppID(); } @@ -315,7 +330,16 @@ public void clearCustomStatus() { } @Override - public boolean getIsReplaying() { + public org.slf4j.Logger getLogger() { + if (this.isReplaying()) { + return org.slf4j.helpers.NOPLogger.NOP_LOGGER; + } + + return org.slf4j.LoggerFactory.getLogger(this.getName()); + } + + @Override + public boolean isReplaying() { return this.isReplaying; } @@ -405,14 +429,14 @@ public Task> anyOf(List> tasks) { public Task callActivity( String name, @Nullable Object input, - @Nullable TaskOptions options, + @Nullable WorkflowTaskOptions options, Class returnType) { Helpers.throwIfOrchestratorComplete(this.isComplete); Helpers.throwIfArgumentNull(name, "name"); Helpers.throwIfArgumentNull(returnType, "returnType"); - if (input instanceof TaskOptions) { - throw new IllegalArgumentException("TaskOptions cannot be used as an input. " + if (input instanceof WorkflowTaskOptions) { + throw new IllegalArgumentException("WorkflowTaskOptions cannot be used as an input. " + "Did you call the wrong method overload?"); } @@ -439,7 +463,7 @@ public Task callActivity( .setId(id) .setScheduleTask(scheduleTaskBuilder); if (hasSourceAppId() && hasTargetAppId(options)) { - String targetAppId = options.getAppID(); + String targetAppId = options.getAppId(); actionBuilder.setRouter(Orchestration.TaskRouter.newBuilder() .setSourceAppID(this.appId) .setTargetAppID(targetAppId) @@ -563,18 +587,18 @@ public void sendEvent(String instanceId, String eventName, Object eventData) { } @Override - public Task callSubOrchestrator( + public Task callChildWorkflow( String name, @Nullable Object input, @Nullable String instanceId, - @Nullable TaskOptions options, + @Nullable WorkflowTaskOptions options, Class returnType) { Helpers.throwIfOrchestratorComplete(this.isComplete); Helpers.throwIfArgumentNull(name, "name"); Helpers.throwIfArgumentNull(returnType, "returnType"); - if (input instanceof TaskOptions) { - throw new IllegalArgumentException("TaskOptions cannot be used as an input. " + if (input instanceof WorkflowTaskOptions) { + throw new IllegalArgumentException("WorkflowTaskOptions cannot be used as an input. " + "Did you call the wrong method overload?"); } @@ -609,10 +633,10 @@ public Task callSubOrchestrator( Orchestration.TaskRouter.Builder actionRouterBuilder = Orchestration.TaskRouter.newBuilder() .setSourceAppID(this.appId); if (hasTargetAppId(options)) { - actionRouterBuilder.setTargetAppID(options.getAppID()); + actionRouterBuilder.setTargetAppID(options.getAppId()); this.logger.fine(() -> String.format( "cross app sub-orchestration routing detected: source=%s, target=%s", - this.appId, options.getAppID())); + this.appId, options.getAppId())); } actionBuilder.setRouter(actionRouterBuilder.build()); } @@ -646,7 +670,7 @@ public Task callSubOrchestrator( private Task createAppropriateTask( TaskFactory taskFactory, - TaskOptions options, + WorkflowTaskOptions options, Consumer retryTimerOriginSetter) { // Retry policies and retry handlers will cause us to return a RetriableTask if (options != null && (options.hasRetryPolicy() || options.hasRetryHandler())) { @@ -793,7 +817,7 @@ private void handleTaskFailed(HistoryEvents.HistoryEvent e) { return; } - FailureDetails details = new FailureDetails(failedEvent.getFailureDetails()); + WorkflowFailureDetails details = new WorkflowFailureDetails(failedEvent.getFailureDetails()); if (!this.isReplaying) { // TODO: Log task failure, including the number of bytes in the result @@ -1122,7 +1146,8 @@ private void handleSubOrchestrationFailed(HistoryEvents.HistoryEvent e) { return; } - FailureDetails details = new FailureDetails(subOrchestrationInstanceFailedEvent.getFailureDetails()); + WorkflowFailureDetails details = + new WorkflowFailureDetails(subOrchestrationInstanceFailedEvent.getFailureDetails()); if (!this.isReplaying) { // TODO: Log task failure, including the number of bytes in the result @@ -1152,7 +1177,7 @@ public void complete(Object output) { } } - public void fail(FailureDetails failureDetails) { + public void fail(WorkflowFailureDetails failureDetails) { // TODO: How does a parent orchestration use the output to construct an exception? this.completeInternal(null, failureDetails, Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED); @@ -1165,7 +1190,7 @@ private void completeInternal(Object output, Orchestration.OrchestrationStatus r private void completeInternal( @Nullable String rawOutput, - @Nullable FailureDetails failureDetails, + @Nullable WorkflowFailureDetails failureDetails, Orchestration.OrchestrationStatus runtimeStatus) { Helpers.throwIfOrchestratorComplete(this.isComplete); @@ -1543,24 +1568,24 @@ protected void handleException(Throwable e) { // Task implementation that implements a retry policy private class RetriableTask extends CompletableTask { - private final RetryPolicy policy; - private final RetryHandler handler; - private final TaskOrchestrationContext context; + private final WorkflowTaskRetryPolicy policy; + private final WorkflowTaskRetryHandler handler; + private final WorkflowContext context; private final Instant firstAttempt; private final TaskFactory taskFactory; private final Consumer retryTimerOriginSetter; - private FailureDetails lastFailure; + private WorkflowFailureDetails lastFailure; private Duration totalRetryTime; private Instant startTime; private int attemptNumber; private Task childTask; public RetriableTask( - TaskOrchestrationContext context, + WorkflowContext context, TaskFactory taskFactory, - RetryPolicy policy, - RetryHandler handler, + WorkflowTaskRetryPolicy policy, + WorkflowTaskRetryHandler handler, Consumer retryTimerOriginSetter) { this.context = context; this.taskFactory = taskFactory; @@ -1661,7 +1686,7 @@ private boolean shouldRetry() { return false; } - RetryContext retryContext = new RetryContext( + WorkflowTaskRetryContext retryContext = new WorkflowTaskRetryContext( this.context, this.attemptNumber, this.lastFailure, @@ -1672,7 +1697,7 @@ private boolean shouldRetry() { boolean shouldRetryBasedOnHandler = this.handler != null ? this.handler.handle(retryContext) : true; // Only log when not replaying, so only the current attempt is logged and not all previous attempts. - if (!this.context.getIsReplaying()) { + if (!this.context.isReplaying()) { if (this.policy != null) { logger.fine(() -> String.format("shouldRetryBasedOnPolicy: %s", shouldRetryBasedOnPolicy)); } @@ -1687,7 +1712,7 @@ private boolean shouldRetry() { private boolean shouldRetryBasedOnPolicy() { // Only log when not replaying, so only the current attempt is logged and not all previous attempts. - if (!this.context.getIsReplaying()) { + if (!this.context.isReplaying()) { logger.fine(() -> String.format("Retry Policy: %d retries out of total %d performed ", this.attemptNumber, this.policy.getMaxNumberOfAttempts())); } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestratorResult.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskOrchestratorResult.java similarity index 97% rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestratorResult.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskOrchestratorResult.java index 35ea8b2f32..9fe8de0eec 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestratorResult.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/TaskOrchestratorResult.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/util/UuidGenerator.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/UuidGenerator.java similarity index 98% rename from durabletask-client/src/main/java/io/dapr/durabletask/util/UuidGenerator.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/UuidGenerator.java index a55ed5fb12..47d9e9d061 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/util/UuidGenerator.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/UuidGenerator.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask.util; +package io.dapr.workflows.task.internal; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/WorkflowHistoryCache.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/WorkflowHistoryCache.java similarity index 99% rename from durabletask-client/src/main/java/io/dapr/durabletask/WorkflowHistoryCache.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/WorkflowHistoryCache.java index f331992d93..16b0729364 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/WorkflowHistoryCache.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/WorkflowHistoryCache.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/runner/ActivityRunner.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/ActivityRunner.java similarity index 95% rename from durabletask-client/src/main/java/io/dapr/durabletask/runner/ActivityRunner.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/ActivityRunner.java index 2bd5b5adc0..2913f935c4 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/runner/ActivityRunner.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/ActivityRunner.java @@ -11,14 +11,14 @@ limitations under the License. */ -package io.dapr.durabletask.runner; +package io.dapr.workflows.task.internal.runner; import com.google.protobuf.StringValue; -import io.dapr.durabletask.FailureDetails; -import io.dapr.durabletask.TaskActivityExecutor; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; +import io.dapr.workflows.task.internal.TaskActivityExecutor; import io.grpc.StatusRuntimeException; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; @@ -30,6 +30,7 @@ import io.opentelemetry.context.propagation.TextMapGetter; import javax.annotation.Nullable; + import java.util.HashMap; import java.util.Map; import java.util.logging.Level; @@ -118,7 +119,7 @@ private void executeActivity() throws Throwable { failureDetails = Orchestration.TaskFailureDetails.newBuilder() .setErrorType(e.getClass().getName()) .setErrorMessage(e.getMessage()) - .setStackTrace(StringValue.of(FailureDetails.getFullStackTrace(e))) + .setStackTrace(StringValue.of(WorkflowFailureDetails.getFullStackTrace(e))) .build(); failureException = e; } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/runner/DurableRunner.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/DurableRunner.java similarity index 98% rename from durabletask-client/src/main/java/io/dapr/durabletask/runner/DurableRunner.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/DurableRunner.java index b59aa58046..f73a7caa6f 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/runner/DurableRunner.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/DurableRunner.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask.runner; +package io.dapr.workflows.task.internal.runner; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; @@ -20,6 +20,7 @@ import io.opentelemetry.api.trace.Tracer; import javax.annotation.Nullable; + import java.util.logging.Level; import java.util.logging.Logger; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRunner.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/OrchestrationRunner.java similarity index 93% rename from durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRunner.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/OrchestrationRunner.java index fe31b1fd33..135a5fef86 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRunner.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/OrchestrationRunner.java @@ -11,13 +11,19 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal.runner; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.StringValue; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.OrchestratorFunction; +import io.dapr.workflows.task.TaskOrchestration; +import io.dapr.workflows.task.internal.TaskOrchestrationExecutor; +import io.dapr.workflows.task.internal.TaskOrchestratorResult; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.JacksonDataConverter; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; import java.time.Duration; import java.util.Base64; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/runner/OrchestratorRunner.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/OrchestratorRunner.java similarity index 97% rename from durabletask-client/src/main/java/io/dapr/durabletask/runner/OrchestratorRunner.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/OrchestratorRunner.java index 6276aa8e1d..d710e9fbf1 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/runner/OrchestratorRunner.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/internal/runner/OrchestratorRunner.java @@ -11,17 +11,17 @@ limitations under the License. */ -package io.dapr.durabletask.runner; +package io.dapr.workflows.task.internal.runner; import com.google.protobuf.StringValue; -import io.dapr.durabletask.TaskOrchestrationExecutor; -import io.dapr.durabletask.TaskOrchestratorResult; -import io.dapr.durabletask.WorkflowHistoryCache; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.task.internal.TaskOrchestrationExecutor; +import io.dapr.workflows.task.internal.TaskOrchestratorResult; +import io.dapr.workflows.task.internal.WorkflowHistoryCache; import io.grpc.StatusRuntimeException; import io.opentelemetry.api.trace.Tracer; import org.apache.commons.lang3.StringUtils; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/interruption/ContinueAsNewInterruption.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/interruption/ContinueAsNewInterruption.java similarity index 85% rename from durabletask-client/src/main/java/io/dapr/durabletask/interruption/ContinueAsNewInterruption.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/interruption/ContinueAsNewInterruption.java index e95c511573..2f89fa1466 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/interruption/ContinueAsNewInterruption.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/interruption/ContinueAsNewInterruption.java @@ -11,17 +11,17 @@ limitations under the License. */ -package io.dapr.durabletask.interruption; +package io.dapr.workflows.task.interruption; -import io.dapr.durabletask.TaskOrchestrationContext; +import io.dapr.workflows.WorkflowContext; /** - * Control flow {@code Throwable} class for orchestrator when invoke {@link TaskOrchestrationContext#continueAsNew}. + * Control flow {@code Throwable} class for orchestrator when invoke {@link WorkflowContext#continueAsNew}. * This {@code Throwable} must never be caught by user * code. * *

    {@code ContinueAsNewInterruption} is thrown when an orchestrator calls - * {@link TaskOrchestrationContext#continueAsNew}. + * {@link WorkflowContext#continueAsNew}. * Catching {@code ContinueAsNewInterruption} in user code could prevent the orchestration from saving * state and scheduling new tasks, resulting in the orchestration getting stuck.

    */ diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/interruption/OrchestratorBlockedException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/interruption/OrchestratorBlockedException.java similarity index 94% rename from durabletask-client/src/main/java/io/dapr/durabletask/interruption/OrchestratorBlockedException.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/interruption/OrchestratorBlockedException.java index 7eff5248f6..c7cc77673e 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/interruption/OrchestratorBlockedException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/interruption/OrchestratorBlockedException.java @@ -11,9 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask.interruption; +package io.dapr.workflows.task.interruption; -import io.dapr.durabletask.Task; +import io.dapr.workflows.task.Task; /** * Control flow {@code Throwable} class for orchestrator functions. This {@code Throwable} must never be caught by user diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactories.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/orchestration/TaskOrchestrationFactories.java similarity index 97% rename from durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactories.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/orchestration/TaskOrchestrationFactories.java index 8ed48aa295..ffa08b444a 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactories.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/orchestration/TaskOrchestrationFactories.java @@ -11,9 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask.orchestration; +package io.dapr.workflows.task.orchestration; -import io.dapr.durabletask.orchestration.exception.VersionNotRegisteredException; +import io.dapr.workflows.task.exception.VersionNotRegisteredException; import java.util.HashMap; import java.util.logging.Logger; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactory.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/orchestration/TaskOrchestrationFactory.java similarity index 91% rename from durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactory.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/orchestration/TaskOrchestrationFactory.java index a5e1b6a3cf..7b09d772c2 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactory.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/orchestration/TaskOrchestrationFactory.java @@ -11,9 +11,9 @@ limitations under the License. */ -package io.dapr.durabletask.orchestration; +package io.dapr.workflows.task.orchestration; -import io.dapr.durabletask.TaskOrchestration; +import io.dapr.workflows.task.TaskOrchestration; /** * Factory interface for producing {@link TaskOrchestration} implementations. diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DataConverter.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/serialization/DataConverter.java similarity index 98% rename from durabletask-client/src/main/java/io/dapr/durabletask/DataConverter.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/serialization/DataConverter.java index 3c2dd7b7ec..1939c459dc 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DataConverter.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/serialization/DataConverter.java @@ -11,11 +11,12 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.serialization; import com.google.protobuf.Timestamp; import javax.annotation.Nullable; + import java.time.Instant; import java.time.temporal.ChronoUnit; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/JacksonDataConverter.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/serialization/JacksonDataConverter.java similarity index 97% rename from durabletask-client/src/main/java/io/dapr/durabletask/JacksonDataConverter.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/serialization/JacksonDataConverter.java index 29912aa3f1..2ebd04488c 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/JacksonDataConverter.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/serialization/JacksonDataConverter.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.serialization; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorker.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/worker/DurableTaskGrpcWorker.java similarity index 96% rename from durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorker.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/worker/DurableTaskGrpcWorker.java index e46f2c2bdc..ec2ed50b83 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorker.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/worker/DurableTaskGrpcWorker.java @@ -11,14 +11,20 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.worker; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.runner.ActivityRunner; -import io.dapr.durabletask.runner.OrchestratorRunner; +import io.dapr.workflows.task.TaskActivityFactory; +import io.dapr.workflows.task.internal.TaskActivityExecutor; +import io.dapr.workflows.task.internal.TaskOrchestrationExecutor; +import io.dapr.workflows.task.internal.WorkflowHistoryCache; +import io.dapr.workflows.task.internal.runner.ActivityRunner; +import io.dapr.workflows.task.internal.runner.OrchestratorRunner; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.serialization.DataConverter; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import io.grpc.Channel; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorkerBuilder.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/worker/DurableTaskGrpcWorkerBuilder.java similarity index 96% rename from durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorkerBuilder.java rename to sdk-workflows/src/main/java/io/dapr/workflows/task/worker/DurableTaskGrpcWorkerBuilder.java index b7135643eb..a6c7ad2ac6 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcWorkerBuilder.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/worker/DurableTaskGrpcWorkerBuilder.java @@ -11,10 +11,12 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.worker; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.TaskActivityFactory; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.DataConverter; import io.grpc.Channel; import java.time.Duration; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java deleted file mode 100644 index 920f3c0804..0000000000 --- a/sdk-workflows/src/test/java/io/dapr/workflows/DefaultWorkflowContextTest.java +++ /dev/null @@ -1,610 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows; - -import io.dapr.durabletask.CompositeTaskFailedException; -import io.dapr.durabletask.HistoryPropagationScope; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.RetryContext; -import io.dapr.durabletask.RetryHandler; -import io.dapr.durabletask.Task; -import io.dapr.durabletask.TaskCanceledException; -import io.dapr.durabletask.TaskOptions; -import io.dapr.durabletask.TaskOrchestrationContext; -import io.dapr.workflows.runtime.DefaultWorkflowContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.slf4j.Logger; - -import javax.annotation.Nullable; -import java.time.Duration; -import java.time.Instant; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class DefaultWorkflowContextTest { - private DefaultWorkflowContext context; - private DefaultWorkflowContext contextWithClass; - private TaskOrchestrationContext mockInnerContext; - private WorkflowContext testWorkflowContext; - - @BeforeEach - public void setUp() { - mockInnerContext = mock(TaskOrchestrationContext.class); - testWorkflowContext = new WorkflowContext() { - @Override - public Logger getLogger() { - return null; - } - - @Override - public String getName() { - return null; - } - - @Override - public String getInstanceId() { - return null; - } - - @Override - public Instant getCurrentInstant() { - return null; - } - - @Override - public void complete(Object output) { - - } - - @Override - public Task waitForExternalEvent(String name, Duration timeout, Class dataType) - throws TaskCanceledException { - return null; - } - - @Override - public Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException { - return null; - } - - @Override - public Task waitForExternalEvent(String name) throws TaskCanceledException { - return null; - } - - @Override - public Task callActivity(String name, Object input, WorkflowTaskOptions options, Class returnType) { - return null; - } - - @Override - public boolean isReplaying() { - return false; - } - - @Override - public Task> allOf(List> tasks) throws CompositeTaskFailedException { - return null; - } - - @Override - public Task> anyOf(List> tasks) { - return null; - } - - @Override - public Task createTimer(Duration duration) { - return null; - } - - @Override - public Task createTimer(ZonedDateTime zonedDateTime) { - return null; - } - - @Override - public V getInput(Class targetType) { - return null; - } - - @Override - public Task callChildWorkflow(String name, @Nullable Object input, @Nullable String instanceID, - @Nullable WorkflowTaskOptions options, Class returnType) { - return null; - } - - @Override - public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { - } - - @Override - public void setCustomStatus(Object status) { - - } - - @Override - public boolean isPatched(String patchName) { - return false; - } - - @Override - public Optional getPropagatedHistory() { - return Optional.empty(); - } - }; - context = new DefaultWorkflowContext(mockInnerContext); - contextWithClass = new DefaultWorkflowContext(mockInnerContext, testWorkflowContext.getClass()); - } - - @Test - public void getNameTest() { - context.getName(); - verify(mockInnerContext, times(1)).getName(); - } - - @Test - public void getInstanceIdTest() { - context.getInstanceId(); - verify(mockInnerContext, times(1)).getInstanceId(); - } - - @Test - public void getCurrentInstantTest() { - context.getCurrentInstant(); - verify(mockInnerContext, times(1)).getCurrentInstant(); - } - - @Test - public void waitForExternalEventWithEventAndDurationTest() { - String expectedEvent = "TestEvent"; - Duration expectedDuration = Duration.ofSeconds(1); - - context.waitForExternalEvent(expectedEvent, expectedDuration); - verify(mockInnerContext, times(1)).waitForExternalEvent(expectedEvent, expectedDuration, Void.class); - } - - @Test - public void waitForExternalEventTest() { - String expectedEvent = "TestEvent"; - Duration expectedDuration = Duration.ofSeconds(1); - - context.waitForExternalEvent(expectedEvent, expectedDuration, String.class); - verify(mockInnerContext, times(1)).waitForExternalEvent(expectedEvent, expectedDuration, String.class); - } - - @Test - public void callActivityTest() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - - context.callActivity(expectedName, expectedInput, String.class); - verify(mockInnerContext, times(1)).callActivity(expectedName, expectedInput, null, String.class); - } - - @Test - public void DaprWorkflowContextWithEmptyInnerContext() { - assertThrows(IllegalArgumentException.class, () -> - context = new DefaultWorkflowContext(mockInnerContext, (Logger)null)); } - - @Test - public void DaprWorkflowContextWithEmptyLogger() { - assertThrows(IllegalArgumentException.class, () -> context = new DefaultWorkflowContext(null, (Logger)null)); - } - - @Test - public void completeTest() { - context.complete(null); - verify(mockInnerContext, times(1)).complete(null); - } - - @Test - public void getIsReplayingTest() { - context.isReplaying(); - verify(mockInnerContext, times(1)).getIsReplaying(); - } - - @Test - public void getLoggerReplayingTest() { - Logger mockLogger = mock(Logger.class); - when(context.isReplaying()).thenReturn(true); - DefaultWorkflowContext testContext = new DefaultWorkflowContext(mockInnerContext, mockLogger); - - String expectedArg = "test print"; - testContext.getLogger().info(expectedArg); - - verify(mockLogger, times(0)).info(any(String.class)); - } - - @Test - public void getLoggerFirstTimeTest() { - Logger mockLogger = mock(Logger.class); - when(context.isReplaying()).thenReturn(false); - DefaultWorkflowContext testContext = new DefaultWorkflowContext(mockInnerContext, mockLogger); - - String expectedArg = "test print"; - testContext.getLogger().info(expectedArg); - - verify(mockLogger, times(1)).info(expectedArg); - } - - @Test - public void continueAsNewTest() { - String expectedInput = "TestInput"; - context.continueAsNew(expectedInput); - verify(mockInnerContext, times(1)).continueAsNew(expectedInput); - } - - @Test - public void allOfTest() { - Task t1 = mockInnerContext.callActivity("task1"); - Task t2 = mockInnerContext.callActivity("task2"); - List> taskList = Arrays.asList(t1, t2); - context.allOf(taskList); - verify(mockInnerContext, times(1)).allOf(taskList); - } - - @Test - public void anyOfTest() { - Task t1 = mockInnerContext.callActivity("task1"); - Task t2 = mockInnerContext.callActivity("task2"); - Task t3 = mockInnerContext.callActivity("task3"); - List> taskList = Arrays.asList(t1, t2); - - context.anyOf(taskList); - verify(mockInnerContext, times(1)).anyOf(taskList); - - context.anyOf(t1, t2, t3); - verify(mockInnerContext, times(1)).anyOf(Arrays.asList(t1, t2, t3)); - } - - @Test - public void createTimerTest() { - context.createTimer(Duration.ofSeconds(10)); - verify(mockInnerContext, times(1)).createTimer(Duration.ofSeconds(10)); - } - - @Test - public void createTimerWithZonedDateTimeTest() { - ZonedDateTime now = ZonedDateTime.now(); - context.createTimer(now); - verify(mockInnerContext, times(1)).createTimer(now); - } - - @Test - public void callChildWorkflowWithName() { - String expectedName = "TestActivity"; - - context.callChildWorkflow(expectedName); - verify(mockInnerContext, times(1)).callSubOrchestrator(expectedName, null, null, null, Void.class); - } - - @Test - public void callChildWorkflowWithRetryPolicy() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - String expectedInstanceId = "TestInstanceId"; - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(1) - .setFirstRetryInterval(Duration.ofSeconds(10)) - .build(); - WorkflowTaskOptions executionOptions = new WorkflowTaskOptions(retryPolicy); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callChildWorkflow(expectedName, expectedInput, expectedInstanceId, executionOptions, String.class); - - verify(mockInnerContext, times(1)) - .callSubOrchestrator( - eq(expectedName), - eq(expectedInput), - eq(expectedInstanceId), - captor.capture(), - eq(String.class) - ); - - TaskOptions taskOptions = captor.getValue(); - - assertEquals(retryPolicy.getMaxNumberOfAttempts(), taskOptions.getRetryPolicy().getMaxNumberOfAttempts()); - assertEquals(retryPolicy.getFirstRetryInterval(), taskOptions.getRetryPolicy().getFirstRetryInterval()); - assertEquals(Duration.ZERO, taskOptions.getRetryPolicy().getRetryTimeout()); - assertNull(taskOptions.getRetryHandler()); - } - - @Test - public void callChildWorkflowWithRetryHandler() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - String expectedInstanceId = "TestInstanceId"; - - WorkflowTaskRetryHandler retryHandler = spy(new WorkflowTaskRetryHandler() { - @Override - public boolean handle(WorkflowTaskRetryContext retryContext) { - return true; - } - }); - - WorkflowTaskOptions executionOptions = new WorkflowTaskOptions(retryHandler); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callChildWorkflow(expectedName, expectedInput, expectedInstanceId, executionOptions, String.class); - - verify(mockInnerContext, times(1)) - .callSubOrchestrator( - eq(expectedName), - eq(expectedInput), - eq(expectedInstanceId), - captor.capture(), - eq(String.class) - ); - - TaskOptions taskOptions = captor.getValue(); - - RetryHandler durableRetryHandler = taskOptions.getRetryHandler(); - RetryContext retryContext = mock(RetryContext.class, invocationOnMock -> null); - - durableRetryHandler.handle(retryContext); - - verify(retryHandler, times(1)).handle(any()); - assertNull(taskOptions.getRetryPolicy()); - } - - @Test - public void callChildWorkflowWithRetryPolicyAndHandler() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - String expectedInstanceId = "TestInstanceId"; - - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(1) - .setFirstRetryInterval(Duration.ofSeconds(10)) - .build(); - - WorkflowTaskRetryHandler retryHandler = spy(new WorkflowTaskRetryHandler() { - @Override - public boolean handle(WorkflowTaskRetryContext retryContext) { - return true; - } - }); - - WorkflowTaskOptions executionOptions = new WorkflowTaskOptions(retryPolicy, retryHandler); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callChildWorkflow(expectedName, expectedInput, expectedInstanceId, executionOptions, String.class); - - verify(mockInnerContext, times(1)) - .callSubOrchestrator( - eq(expectedName), - eq(expectedInput), - eq(expectedInstanceId), - captor.capture(), - eq(String.class) - ); - - TaskOptions taskOptions = captor.getValue(); - - RetryHandler durableRetryHandler = taskOptions.getRetryHandler(); - RetryContext retryContext = mock(RetryContext.class, invocationOnMock -> null); - - durableRetryHandler.handle(retryContext); - - verify(retryHandler, times(1)).handle(any()); - assertEquals(retryPolicy.getMaxNumberOfAttempts(), taskOptions.getRetryPolicy().getMaxNumberOfAttempts()); - assertEquals(retryPolicy.getFirstRetryInterval(), taskOptions.getRetryPolicy().getFirstRetryInterval()); - assertEquals(Duration.ZERO, taskOptions.getRetryPolicy().getRetryTimeout()); - } - - @Test - public void callChildWorkflow() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - - context.callChildWorkflow(expectedName, expectedInput, String.class); - verify(mockInnerContext, times(1)).callSubOrchestrator(expectedName, expectedInput, null, null, String.class); - } - - @Test - public void callChildWorkflowWithAppId() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - String expectedInstanceId = "TestInstanceId"; - String expectedAppId = "remote-app"; - WorkflowTaskOptions executionOptions = new WorkflowTaskOptions(expectedAppId); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callChildWorkflow(expectedName, expectedInput, expectedInstanceId, executionOptions, String.class); - - verify(mockInnerContext, times(1)) - .callSubOrchestrator( - eq(expectedName), - eq(expectedInput), - eq(expectedInstanceId), - captor.capture(), - eq(String.class) - ); - - TaskOptions taskOptions = captor.getValue(); - - assertEquals(expectedAppId, taskOptions.getAppID()); - assertNull(taskOptions.getRetryPolicy()); - assertNull(taskOptions.getRetryHandler()); - } - - @Test - public void setCustomStatusWorkflow() { - String customStatus = "CustomStatus"; - - context.setCustomStatus(customStatus); - verify(mockInnerContext, times(1)).setCustomStatus(customStatus); - - } - - @Test - public void testIsPatched() { - context.isPatched("patch"); - verify(mockInnerContext, times(1)).isPatched("patch"); - - } - - @Test - public void newUuidTest() { - context.newUuid(); - verify(mockInnerContext, times(1)).newUuid(); - } - - @Test - public void newUuidTestNoImplementationExceptionTest() { - RuntimeException runtimeException = assertThrows(RuntimeException.class, testWorkflowContext::newUuid); - String expectedMessage = "No implementation found."; - assertEquals(expectedMessage, runtimeException.getMessage()); - } - - @Test - public void workflowRetryPolicyRetryTimeoutValueShouldHaveRightValueWhenBeingSet() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - String expectedInstanceId = "TestInstanceId"; - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(1) - .setFirstRetryInterval(Duration.ofSeconds(10)) - .setRetryTimeout(Duration.ofSeconds(10)) - .build(); - WorkflowTaskOptions executionOptions = new WorkflowTaskOptions(retryPolicy); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callChildWorkflow(expectedName, expectedInput, expectedInstanceId, executionOptions, String.class); - - verify(mockInnerContext, times(1)) - .callSubOrchestrator( - eq(expectedName), - eq(expectedInput), - eq(expectedInstanceId), - captor.capture(), - eq(String.class) - ); - - TaskOptions taskOptions = captor.getValue(); - - assertEquals(Duration.ofSeconds(10), taskOptions.getRetryPolicy().getRetryTimeout()); - } - - @Test - public void workflowRetryPolicyRetryThrowIllegalArgumentWhenNullRetryTimeoutIsSet() { - assertThrows(IllegalArgumentException.class, () -> - WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(1) - .setFirstRetryInterval(Duration.ofSeconds(10)) - .setRetryTimeout(null) - .build()); - } - - @Test - public void workflowRetryPolicyRetryThrowIllegalArgumentWhenRetryTimeoutIsLessThanMaxRetryInterval() { - assertThrows(IllegalArgumentException.class, () -> WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(1) - .setFirstRetryInterval(Duration.ofSeconds(10)) - .setRetryTimeout(Duration.ofSeconds(9)) - .build()); - } - - @Test - public void callActivityRetryPolicyMaxRetryIntervalShouldBePropagated() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - Duration expectedMaxRetryInterval = Duration.ofSeconds(60); - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(5) - .setFirstRetryInterval(Duration.ofSeconds(1)) - .setMaxRetryInterval(expectedMaxRetryInterval) - .build(); - WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callActivity(expectedName, expectedInput, options, String.class); - - verify(mockInnerContext, times(1)) - .callActivity(eq(expectedName), eq(expectedInput), captor.capture(), eq(String.class)); - - assertEquals(expectedMaxRetryInterval, captor.getValue().getRetryPolicy().getMaxRetryInterval()); - } - - @Test - public void callActivityRetryPolicyDefaultMaxRetryIntervalShouldBeZeroWhenNotSet() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(5) - .setFirstRetryInterval(Duration.ofSeconds(1)) - .build(); - WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callActivity(expectedName, expectedInput, options, String.class); - - verify(mockInnerContext, times(1)) - .callActivity(eq(expectedName), eq(expectedInput), captor.capture(), eq(String.class)); - - assertEquals(Duration.ZERO, captor.getValue().getRetryPolicy().getMaxRetryInterval()); - } - - @Test - public void callActivityWithHistoryPropagationScope() { - String expectedName = "TestActivity"; - String expectedInput = "TestInput"; - WorkflowTaskOptions options = WorkflowTaskOptions.propagateLineage(); - ArgumentCaptor captor = ArgumentCaptor.forClass(TaskOptions.class); - - context.callActivity(expectedName, expectedInput, options, String.class); - - verify(mockInnerContext, times(1)) - .callActivity(eq(expectedName), eq(expectedInput), captor.capture(), eq(String.class)); - - assertEquals(HistoryPropagationScope.LINEAGE, captor.getValue().getHistoryPropagationScope()); - } - - @Test - public void getPropagatedHistoryDelegatesToInnerContext() { - PropagatedHistory mockHistory = mock(PropagatedHistory.class); - when(mockInnerContext.getPropagatedHistory()).thenReturn(Optional.of(mockHistory)); - - Optional result = context.getPropagatedHistory(); - - assertTrue(result.isPresent()); - assertEquals(mockHistory, result.get()); - verify(mockInnerContext, times(1)).getPropagatedHistory(); - } - - @Test - public void getPropagatedHistoryReturnsEmptyWhenNone() { - when(mockInnerContext.getPropagatedHistory()).thenReturn(Optional.empty()); - - Optional result = context.getPropagatedHistory(); - - assertTrue(result.isEmpty()); - } -} diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/TaskOptionsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsMergedTest.java similarity index 64% rename from durabletask-client/src/test/java/io/dapr/durabletask/TaskOptionsTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsMergedTest.java index 43fad5f526..da38d83904 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/TaskOptionsTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsMergedTest.java @@ -11,7 +11,7 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows; import org.junit.jupiter.api.Test; @@ -20,30 +20,30 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Unit tests for TaskOptions with cross-app workflow support. + * Unit tests for WorkflowTaskOptions with cross-app workflow support. */ -public class TaskOptionsTest { +public class WorkflowTaskOptionsMergedTest { @Test void taskOptionsWithAppID() { - TaskOptions options = TaskOptions.withAppID("app1"); + WorkflowTaskOptions options = WorkflowTaskOptions.withAppID("app1"); assertTrue(options.hasAppID()); - assertEquals("app1", options.getAppID()); + assertEquals("app1", options.getAppId()); assertFalse(options.hasRetryPolicy()); assertFalse(options.hasRetryHandler()); } @Test void taskOptionsWithRetryPolicyAndAppID() { - RetryPolicy retryPolicy = new RetryPolicy(3, Duration.ofSeconds(1)); - TaskOptions options = TaskOptions.builder() + WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(3, Duration.ofSeconds(1)); + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .retryPolicy(retryPolicy) .appID("app2") .build(); assertTrue(options.hasAppID()); - assertEquals("app2", options.getAppID()); + assertEquals("app2", options.getAppId()); assertTrue(options.hasRetryPolicy()); assertEquals(retryPolicy, options.getRetryPolicy()); assertFalse(options.hasRetryHandler()); @@ -51,19 +51,19 @@ void taskOptionsWithRetryPolicyAndAppID() { @Test void taskOptionsWithRetryHandlerAndAppID() { - RetryHandler retryHandler = new RetryHandler() { + WorkflowTaskRetryHandler retryHandler = new WorkflowTaskRetryHandler() { @Override - public boolean handle(RetryContext context) { + public boolean handle(WorkflowTaskRetryContext context) { return context.getLastAttemptNumber() < 2; } }; - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .retryHandler(retryHandler) .appID("app3") .build(); assertTrue(options.hasAppID()); - assertEquals("app3", options.getAppID()); + assertEquals("app3", options.getAppId()); assertFalse(options.hasRetryPolicy()); assertTrue(options.hasRetryHandler()); assertEquals(retryHandler, options.getRetryHandler()); @@ -71,32 +71,32 @@ public boolean handle(RetryContext context) { @Test void taskOptionsWithoutAppID() { - TaskOptions options = TaskOptions.create(); + WorkflowTaskOptions options = WorkflowTaskOptions.create(); assertFalse(options.hasAppID()); - assertNull(options.getAppID()); + assertNull(options.getAppId()); } @Test void taskOptionsWithEmptyAppID() { - TaskOptions options = TaskOptions.withAppID(""); + WorkflowTaskOptions options = WorkflowTaskOptions.withAppID(""); assertFalse(options.hasAppID()); - assertEquals("", options.getAppID()); + assertEquals("", options.getAppId()); } @Test void taskOptionsWithNullAppID() { - TaskOptions options = TaskOptions.builder().appID(null).build(); + WorkflowTaskOptions options = WorkflowTaskOptions.builder().appID(null).build(); assertFalse(options.hasAppID()); - assertNull(options.getAppID()); + assertNull(options.getAppId()); } @Test void taskOptionsWithRetryPolicy() { - RetryPolicy retryPolicy = new RetryPolicy(5, Duration.ofMinutes(1)); - TaskOptions options = TaskOptions.withRetryPolicy(retryPolicy); + WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(5, Duration.ofMinutes(1)); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryPolicy(retryPolicy); assertTrue(options.hasRetryPolicy()); assertEquals(retryPolicy, options.getRetryPolicy()); @@ -106,13 +106,13 @@ void taskOptionsWithRetryPolicy() { @Test void taskOptionsWithRetryHandler() { - RetryHandler retryHandler = new RetryHandler() { + WorkflowTaskRetryHandler retryHandler = new WorkflowTaskRetryHandler() { @Override - public boolean handle(RetryContext context) { + public boolean handle(WorkflowTaskRetryContext context) { return context.getLastAttemptNumber() < 3; } }; - TaskOptions options = TaskOptions.withRetryHandler(retryHandler); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryHandler(retryHandler); assertTrue(options.hasRetryHandler()); assertEquals(retryHandler, options.getRetryHandler()); @@ -122,10 +122,10 @@ public boolean handle(RetryContext context) { @Test void taskOptionsWithBuilderChaining() { - RetryPolicy retryPolicy = new RetryPolicy(3, Duration.ofSeconds(1)); - RetryHandler retryHandler = context -> true; + WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(3, Duration.ofSeconds(1)); + WorkflowTaskRetryHandler retryHandler = context -> true; - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .retryPolicy(retryPolicy) .retryHandler(retryHandler) .appID("test-app") @@ -137,6 +137,6 @@ void taskOptionsWithBuilderChaining() { assertTrue(options.hasRetryHandler()); assertEquals(retryHandler, options.getRetryHandler()); assertTrue(options.hasAppID()); - assertEquals("test-app", options.getAppID()); + assertEquals("test-app", options.getAppId()); } } \ No newline at end of file diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java index c69c9f1730..c8fd3d3832 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java @@ -13,15 +13,21 @@ package io.dapr.workflows; -import io.dapr.durabletask.HistoryPropagationScope; +import io.dapr.workflows.task.history.HistoryPropagationScope; import org.junit.jupiter.api.Test; + +import java.time.Duration; + import static org.junit.jupiter.api.Assertions.*; class WorkflowTaskOptionsTest { @Test void testConstructorWithRetryPolicyAndHandler() { - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build(); + WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(1)) + .build(); WorkflowTaskRetryHandler retryHandler = (context) -> true; WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy, retryHandler); @@ -33,7 +39,10 @@ void testConstructorWithRetryPolicyAndHandler() { @Test void testConstructorWithRetryPolicyOnly() { - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build(); + WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(1)) + .build(); WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy); @@ -66,7 +75,10 @@ void testConstructorWithAppIdOnly() { @Test void testConstructorWithAllParameters() { - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build(); + WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(1)) + .build(); WorkflowTaskRetryHandler retryHandler = (context) -> true; String appId = "test-app"; @@ -79,7 +91,10 @@ void testConstructorWithAllParameters() { @Test void testConstructorWithRetryPolicyAndAppId() { - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build(); + WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(1)) + .build(); String appId = "test-app"; WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy, appId); @@ -134,7 +149,10 @@ void testPropagateOwnHistory_factory() { @Test void testConstructorWithAllParametersIncludingScope() { - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build(); + WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(1)) + .build(); WorkflowTaskRetryHandler retryHandler = (context) -> true; String appId = "test-app"; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskRetryPolicyTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskRetryPolicyTest.java new file mode 100644 index 0000000000..1480a9407e --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskRetryPolicyTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Builder validation carried over from DefaultWorkflowContextTest, which was deleted along with + * the DefaultWorkflowContext adapter. These assertions were never about the adapter; they test + * WorkflowTaskRetryPolicy itself. + */ +public class WorkflowTaskRetryPolicyTest { + + @Test + public void retryTimeoutIsRetained() { + WorkflowTaskRetryPolicy policy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(10)) + .setRetryTimeout(Duration.ofSeconds(10)) + .build(); + + assertEquals(Duration.ofSeconds(10), policy.getRetryTimeout()); + } + + @Test + public void nullRetryTimeoutIsRejected() { + assertThrows(IllegalArgumentException.class, () -> WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(10)) + .setRetryTimeout(null) + .build()); + } + + @Test + public void retryTimeoutBelowFirstRetryIntervalIsRejectedByTheBuilder() { + assertThrows(IllegalArgumentException.class, () -> WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(1) + .setFirstRetryInterval(Duration.ofSeconds(10)) + .setRetryTimeout(Duration.ofSeconds(9)) + .build()); + } + + /** + * The durable task client's RetryPolicy validated in its setters, and the deleted adapter routed + * every policy through them. These pin that validation to the constructors, so an invalid policy + * still fails fast at construction instead of NPE-ing part-way through a replay or silently + * producing wrong retry timing. + */ + @Test + public void nonPositiveMaxNumberOfAttemptsIsRejected() { + assertThrows(IllegalArgumentException.class, () -> new WorkflowTaskRetryPolicy(0, Duration.ofSeconds(1))); + assertThrows(IllegalArgumentException.class, () -> new WorkflowTaskRetryPolicy(-1, Duration.ofSeconds(1))); + } + + @Test + public void nullMaxNumberOfAttemptsIsRejected() { + assertThrows(IllegalArgumentException.class, + () -> new WorkflowTaskRetryPolicy(null, Duration.ofSeconds(1), 1.0, null, null)); + } + + @Test + public void nullOrNonPositiveFirstRetryIntervalIsRejected() { + assertThrows(IllegalArgumentException.class, () -> new WorkflowTaskRetryPolicy(1, null)); + assertThrows(IllegalArgumentException.class, () -> new WorkflowTaskRetryPolicy(1, Duration.ZERO)); + assertThrows(IllegalArgumentException.class, () -> new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(-1))); + } + + @Test + public void backoffCoefficientBelowOneIsRejected() { + assertThrows(IllegalArgumentException.class, + () -> new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(1), 0.5, null, null)); + assertThrows(IllegalArgumentException.class, + () -> new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(1), null, null, null)); + } + + @Test + public void maxRetryIntervalBelowFirstRetryIntervalIsRejected() { + assertThrows(IllegalArgumentException.class, + () -> new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(10), 1.0, Duration.ofSeconds(9), null)); + } + + @Test + public void retryTimeoutBelowFirstRetryIntervalIsRejectedByTheConstructor() { + assertThrows(IllegalArgumentException.class, + () -> new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(10), 1.0, null, Duration.ofSeconds(9))); + } + + /** + * Only null means "unset". An explicitly supplied ZERO is a real value and v1 rejected it, since + * ZERO is always below a valid firstRetryInterval. Accepting it here would silently uncap retries + * and would disagree with the Builder, which rejects it. + */ + @Test + public void anExplicitZeroOptionalDurationIsRejected() { + assertThrows(IllegalArgumentException.class, + () -> new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(10), 1.0, Duration.ZERO, null)); + assertThrows(IllegalArgumentException.class, + () -> new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(10), 1.0, null, Duration.ZERO)); + } + + /** + * Null is the "unset" signal and is coerced to ZERO, which is what the executor reads as + * "no maximum interval" / "no overall timeout". + */ + @Test + public void nullOptionalDurationsMeanUnsetAndBecomeZero() { + WorkflowTaskRetryPolicy policy = + new WorkflowTaskRetryPolicy(1, Duration.ofSeconds(10), 1.0, null, null); + + assertEquals(Duration.ZERO, policy.getMaxRetryInterval()); + assertEquals(Duration.ZERO, policy.getRetryTimeout()); + } + + /** + * The two-argument constructor replaces the durable task client's RetryPolicy(int, Duration), + * whose maxRetryInterval and retryTimeout defaulted to ZERO. A null here reaches the executor + * and suppresses the retry timer, so the defaults must match exactly. + */ + @Test + public void twoArgConstructorMatchesTheDurableTaskDefaults() { + WorkflowTaskRetryPolicy policy = new WorkflowTaskRetryPolicy(3, Duration.ofSeconds(5)); + + assertEquals(3, policy.getMaxNumberOfAttempts()); + assertEquals(Duration.ofSeconds(5), policy.getFirstRetryInterval()); + assertEquals(1.0, policy.getBackoffCoefficient()); + assertEquals(Duration.ZERO, policy.getMaxRetryInterval()); + assertEquals(Duration.ZERO, policy.getRetryTimeout()); + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java index 3c8a7048cc..7768409b34 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java @@ -1,5 +1,7 @@ package io.dapr.workflows; +import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; @@ -8,8 +10,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import org.junit.jupiter.api.Test; - public class WorkflowTest { @Test diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java index 9c76c4ca43..2e6e109f61 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java @@ -14,14 +14,13 @@ package io.dapr.workflows.client; import io.dapr.config.Properties; -import io.dapr.durabletask.DurableTaskClient; -import io.dapr.durabletask.DurableTaskGrpcClientBuilder; -import io.dapr.durabletask.NewOrchestrationInstanceOptions; -import io.dapr.durabletask.OrchestrationMetadata; -import io.dapr.durabletask.OrchestrationRuntimeStatus; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowContext; import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.client.DurableTaskGrpcClientBuilder; +import io.dapr.workflows.task.client.NewOrchestrationInstanceOptions; +import io.dapr.workflows.task.client.OrchestrationMetadata; import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.StatusRuntimeException; @@ -38,10 +37,10 @@ import java.util.Arrays; import java.util.concurrent.TimeoutException; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -307,7 +306,7 @@ public void getInstanceMetadata() { OrchestrationMetadata expectedMetadata = mock(OrchestrationMetadata.class); when(expectedMetadata.getInstanceId()).thenReturn(instanceId); when(expectedMetadata.getName()).thenReturn("WorkflowName"); - when(expectedMetadata.getRuntimeStatus()).thenReturn(OrchestrationRuntimeStatus.RUNNING); + when(expectedMetadata.getRuntimeStatus()).thenReturn(WorkflowRuntimeStatus.RUNNING); when(mockInnerClient.getInstanceMetadata(instanceId, true)).thenReturn(expectedMetadata); // Act diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstanceStatusTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstanceStatusTest.java deleted file mode 100644 index 776e070812..0000000000 --- a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstanceStatusTest.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.client; - -import io.dapr.durabletask.FailureDetails; -import io.dapr.durabletask.OrchestrationMetadata; -import io.dapr.durabletask.OrchestrationRuntimeStatus; -import io.dapr.workflows.runtime.DefaultWorkflowInstanceStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.Instant; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class WorkflowInstanceStatusTest { - - private OrchestrationMetadata mockOrchestrationMetadata; - private WorkflowInstanceStatus workflowMetadata; - - @BeforeEach - public void setUp() { - mockOrchestrationMetadata = mock(OrchestrationMetadata.class); - workflowMetadata = new DefaultWorkflowInstanceStatus(mockOrchestrationMetadata); - } - - @Test - public void getInstanceId() { - String expected = "instanceId"; - - when(mockOrchestrationMetadata.getInstanceId()).thenReturn(expected); - - String result = workflowMetadata.getInstanceId(); - - verify(mockOrchestrationMetadata, times(1)).getInstanceId(); - assertEquals(expected, result); - } - - @Test - public void getName() { - String expected = "WorkflowName"; - - when(mockOrchestrationMetadata.getName()).thenReturn(expected); - - String result = workflowMetadata.getName(); - - verify(mockOrchestrationMetadata, times(1)).getName(); - assertEquals(expected, result); - } - - @Test - public void getCreatedAt() { - Instant expected = Instant.now(); - when(mockOrchestrationMetadata.getCreatedAt()).thenReturn(expected); - - Instant result = workflowMetadata.getCreatedAt(); - - verify(mockOrchestrationMetadata, times(1)).getCreatedAt(); - assertEquals(expected, result); - } - - @Test - public void getLastUpdatedAt() { - Instant expected = Instant.now(); - - when(mockOrchestrationMetadata.getLastUpdatedAt()).thenReturn(expected); - - Instant result = workflowMetadata.getLastUpdatedAt(); - - verify(mockOrchestrationMetadata, times(1)).getLastUpdatedAt(); - assertEquals(expected, result); - } - - @Test - public void getFailureDetails() { - FailureDetails mockFailureDetails = mock(FailureDetails.class); - - when(mockFailureDetails.getErrorType()).thenReturn("errorType"); - when(mockFailureDetails.getErrorMessage()).thenReturn("errorMessage"); - when(mockFailureDetails.getStackTrace()).thenReturn("stackTrace"); - - OrchestrationMetadata orchestrationMetadata = mock(OrchestrationMetadata.class); - when(orchestrationMetadata.getFailureDetails()).thenReturn(mockFailureDetails); - - WorkflowInstanceStatus metadata = new DefaultWorkflowInstanceStatus(orchestrationMetadata); - WorkflowFailureDetails result = metadata.getFailureDetails(); - - verify(orchestrationMetadata, times(1)).getFailureDetails(); - assertEquals(mockFailureDetails.getErrorType(), result.getErrorType()); - assertEquals(mockFailureDetails.getErrorMessage(), result.getErrorMessage()); - assertEquals(mockFailureDetails.getStackTrace(), result.getStackTrace()); - } - - @Test - public void getRuntimeStatus() { - WorkflowRuntimeStatus expected = WorkflowRuntimeStatus.RUNNING; - - when(mockOrchestrationMetadata.getRuntimeStatus()).thenReturn(OrchestrationRuntimeStatus.RUNNING); - - WorkflowRuntimeStatus result = workflowMetadata.getRuntimeStatus(); - - verify(mockOrchestrationMetadata, times(1)).getRuntimeStatus(); - assertEquals(expected, result); - } - - @Test - public void isRunning() { - boolean expected = true; - - when(mockOrchestrationMetadata.isRunning()).thenReturn(expected); - - boolean result = workflowMetadata.isRunning(); - - verify(mockOrchestrationMetadata, times(1)).isRunning(); - assertEquals(expected, result); - } - - @Test - public void isCompleted() { - boolean expected = true; - - when(mockOrchestrationMetadata.isCompleted()).thenReturn(expected); - - boolean result = workflowMetadata.isCompleted(); - - verify(mockOrchestrationMetadata, times(1)).isCompleted(); - assertEquals(expected, result); - } - - @Test - public void getSerializedInput() { - String expected = "{input: \"test\"}"; - - when(mockOrchestrationMetadata.getSerializedInput()).thenReturn(expected); - - String result = workflowMetadata.getSerializedInput(); - - verify(mockOrchestrationMetadata, times(1)).getSerializedInput(); - assertEquals(expected, result); - } - - @Test - public void getSerializedOutput() { - String expected = "{output: \"test\"}"; - - when(mockOrchestrationMetadata.getSerializedOutput()).thenReturn(expected); - - String result = workflowMetadata.getSerializedOutput(); - - verify(mockOrchestrationMetadata, times(1)).getSerializedOutput(); - assertEquals(expected, result); - } - - @Test - public void readInputAs() { - String expected = "[{property: \"test input\"}}]"; - - when(mockOrchestrationMetadata.readInputAs(String.class)).thenReturn(expected); - - String result = workflowMetadata.readInputAs(String.class); - - verify(mockOrchestrationMetadata, times(1)).readInputAs(String.class); - assertEquals(expected, result); - } - - @Test - public void readOutputAs() { - String expected = "[{property: \"test output\"}}]"; - - when(mockOrchestrationMetadata.readOutputAs(String.class)).thenReturn(expected); - - String result = workflowMetadata.readOutputAs(String.class); - - verify(mockOrchestrationMetadata, times(1)).readOutputAs(String.class); - assertEquals(expected, result); - } - - @Test - public void testToString() { - String expected = "string value"; - - when(mockOrchestrationMetadata.toString()).thenReturn(expected); - - String result = workflowMetadata.toString(); - - assertEquals(expected, result); - } - -} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowRuntimeStatusTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowRuntimeStatusTest.java new file mode 100644 index 0000000000..657a283723 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowRuntimeStatusTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.client; + +import io.dapr.durabletask.implementation.protobuf.Orchestration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class WorkflowRuntimeStatusTest { + + /** + * Before the durable task client was folded in, this enum was a copy of the task-side enum + * that lacked STALLED, and a hand-written converter threw IllegalArgumentException on it. + * A stalled workflow therefore made getWorkflowState() throw instead of reporting status. + */ + @Test + public void stalledIsMappedRatherThanRejected() { + assertEquals(WorkflowRuntimeStatus.STALLED, + WorkflowRuntimeStatus.fromProtobuf(Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_STALLED)); + assertEquals(Orchestration.OrchestrationStatus.ORCHESTRATION_STATUS_STALLED, + WorkflowRuntimeStatus.toProtobuf(WorkflowRuntimeStatus.STALLED)); + } + + /** + * The deleted WorkflowRuntimeStatusConverter had a test for its rejection branch. The mapping it + * was replaced by has the same branch, and it is reachable: a sidecar newer than this SDK can + * send a status this enum does not know, which protobuf surfaces as UNRECOGNIZED. + */ + @Test + public void anUnknownProtobufStatusIsRejectedRatherThanMisreported() { + assertThrows(IllegalArgumentException.class, + () -> WorkflowRuntimeStatus.fromProtobuf( + Orchestration.OrchestrationStatus.UNRECOGNIZED)); + } + + @ParameterizedTest + @EnumSource(WorkflowRuntimeStatus.class) + public void everyStatusRoundTripsThroughProtobuf(WorkflowRuntimeStatus status) { + assertEquals(status, WorkflowRuntimeStatus.fromProtobuf(WorkflowRuntimeStatus.toProtobuf(status))); + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowStateTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowStateTest.java index dc3db11b0d..ff233bb778 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowStateTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowStateTest.java @@ -13,10 +13,9 @@ package io.dapr.workflows.client; -import io.dapr.durabletask.FailureDetails; -import io.dapr.durabletask.OrchestrationMetadata; -import io.dapr.durabletask.OrchestrationRuntimeStatus; import io.dapr.workflows.runtime.DefaultWorkflowState; +import io.dapr.workflows.task.client.OrchestrationMetadata; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -89,7 +88,7 @@ public void getLastUpdatedAt() { @Test public void getFailureDetails() { - FailureDetails mockFailureDetails = mock(FailureDetails.class); + WorkflowFailureDetails mockFailureDetails = mock(WorkflowFailureDetails.class); when(mockFailureDetails.getErrorType()).thenReturn("errorType"); when(mockFailureDetails.getErrorMessage()).thenReturn("errorMessage"); @@ -111,7 +110,7 @@ public void getFailureDetails() { public void getRuntimeStatus() { WorkflowRuntimeStatus expected = WorkflowRuntimeStatus.RUNNING; - when(mockOrchestrationMetadata.getRuntimeStatus()).thenReturn(OrchestrationRuntimeStatus.RUNNING); + when(mockOrchestrationMetadata.getRuntimeStatus()).thenReturn(WorkflowRuntimeStatus.RUNNING); WorkflowRuntimeStatus result = workflowMetadata.getRuntimeStatus(); diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java new file mode 100644 index 0000000000..f3831ad1f2 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.internal; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The module is compiled for Java 17, where {@code Thread.isVirtual()} does not exist, so + * these tests cannot assert that the returned executor produces virtual threads. They assert + * that a usable executor comes back on every supported runtime, and that the thread kind + * matches the runtime the tests happen to be executing on. + */ +public class DefaultExecutorServiceTest { + + @Test + public void createReturnsAUsableExecutor() throws Exception { + ExecutorService executorService = DefaultExecutorService.create(); + + assertNotNull(executorService); + AtomicBoolean ran = new AtomicBoolean(false); + executorService.submit(() -> ran.set(true)).get(5, TimeUnit.SECONDS); + assertTrue(ran.get(), "the default executor must actually run submitted work"); + + executorService.shutdown(); + } + + @Test + public void threadKindMatchesTheRunningJavaVersion() throws Exception { + ExecutorService executorService = DefaultExecutorService.create(); + try { + boolean virtual = executorService.submit(DefaultExecutorServiceTest::currentThreadIsVirtual) + .get(5, TimeUnit.SECONDS); + + assertEquals(Runtime.version().feature() >= 21, virtual, + "Java 21+ should produce virtual threads; earlier runtimes should produce platform threads"); + } finally { + executorService.shutdown(); + } + } + + /** + * Calls {@code Thread.isVirtual()} reflectively, since it is absent at this module's + * Java 17 compile target. + * + * @return true when the calling thread is a virtual thread. + */ + private static boolean currentThreadIsVirtual() { + try { + return (boolean) Thread.class.getMethod("isVirtual").invoke(Thread.currentThread()); + } catch (ReflectiveOperationException ex) { + return false; + } + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContextTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContextTest.java index ceb6648e6c..067c73ca24 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContextTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContextTest.java @@ -1,6 +1,6 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskActivityContext; +import io.dapr.workflows.task.TaskActivityContext; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.slf4j.Logger; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java index 4ec03bf1e0..32525b01ba 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java @@ -1,8 +1,8 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskActivityContext; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.WorkflowActivityContext; +import io.dapr.workflows.task.TaskActivityContext; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java index 08b5adbdd2..640ba86c5c 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityInstanceWrapperTest.java @@ -1,8 +1,8 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskActivityContext; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.WorkflowActivityContext; +import io.dapr.workflows.task.TaskActivityContext; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowClassWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowClassWrapperTest.java index 8458739a79..3b7a714d30 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowClassWrapperTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowClassWrapperTest.java @@ -13,7 +13,6 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskOrchestrationContext; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowContext; import io.dapr.workflows.WorkflowStub; @@ -63,7 +62,7 @@ public void getName() { @Test public void createWithClass() { - TaskOrchestrationContext mockContext = mock(TaskOrchestrationContext.class); + WorkflowContext mockContext = mock(WorkflowContext.class); WorkflowClassWrapper wrapper = new WorkflowClassWrapper<>(TestWorkflow.class); when(mockContext.getInstanceId()).thenReturn("uuid"); @@ -73,7 +72,7 @@ public void createWithClass() { @Test public void createWithClassAndVersion() { - TaskOrchestrationContext mockContext = mock(TaskOrchestrationContext.class); + WorkflowContext mockContext = mock(WorkflowContext.class); WorkflowClassWrapper wrapper = new WorkflowClassWrapper<>("TestWorkflow", TestWorkflow.class, "v1",false); when(mockContext.getInstanceId()).thenReturn("uuid"); wrapper.create().run(mockContext); @@ -86,7 +85,7 @@ public void createErrorClassAndVersion() { assertThrowsExactly(RuntimeException.class, () -> new WorkflowClassWrapper<>("TestErrorWorkflow", TestErrorWorkflow.class, "v1",false)); WorkflowClassWrapper wrapper = new WorkflowClassWrapper<>("TestPrivateWorkflow", TestPrivateWorkflow.class, "v2",false); - TaskOrchestrationContext mockContext = mock(TaskOrchestrationContext.class); + WorkflowContext mockContext = mock(WorkflowContext.class); assertThrowsExactly(RuntimeException.class, () -> wrapper.create().run(mockContext)); } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java index 85849af212..93eaa67095 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowInstanceWrapperTest.java @@ -13,7 +13,6 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskOrchestrationContext; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowContext; import io.dapr.workflows.WorkflowStub; @@ -45,7 +44,7 @@ public void getName() { @Test public void createWithInstance() { - TaskOrchestrationContext mockContext = mock(TaskOrchestrationContext.class); + WorkflowContext mockContext = mock(WorkflowContext.class); WorkflowInstanceWrapper wrapper = new WorkflowInstanceWrapper<>(new TestWorkflow()); when(mockContext.getInstanceId()).thenReturn("uuid"); diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java index ee50b447a9..687feb5d30 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java @@ -12,15 +12,15 @@ */ package io.dapr.workflows.runtime; -import io.dapr.durabletask.DurableTaskGrpcWorkerBuilder; -import io.dapr.durabletask.TaskActivity; -import io.dapr.durabletask.TaskActivityFactory; -import io.dapr.durabletask.TaskOrchestration; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.WorkflowActivityContext; import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.task.TaskActivity; +import io.dapr.workflows.task.TaskActivityFactory; +import io.dapr.workflows.task.TaskOrchestration; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -135,7 +135,7 @@ public String getName() { @Override public TaskOrchestration create() { W w = new W(); - return ctx -> w.run(new DefaultWorkflowContext(ctx, w.getClass())); + return ctx -> w.run(ctx); } @Override diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverterTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverterTest.java deleted file mode 100644 index 9e2d4f9835..0000000000 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeStatusConverterTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2023 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.workflows.runtime; - -import io.dapr.durabletask.OrchestrationRuntimeStatus; -import io.dapr.workflows.client.WorkflowRuntimeStatus; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -public class WorkflowRuntimeStatusConverterTest { - - @Test - public void fromOrchestrationRuntimeStatus() { - - assertEquals(WorkflowRuntimeStatus.RUNNING, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.RUNNING) - ); - - assertEquals(WorkflowRuntimeStatus.COMPLETED, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.COMPLETED) - ); - - assertEquals(WorkflowRuntimeStatus.CONTINUED_AS_NEW, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.CONTINUED_AS_NEW) - ); - - assertEquals(WorkflowRuntimeStatus.FAILED, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.FAILED) - ); - - assertEquals(WorkflowRuntimeStatus.CANCELED, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.CANCELED) - ); - - assertEquals(WorkflowRuntimeStatus.TERMINATED, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.TERMINATED) - ); - - assertEquals(WorkflowRuntimeStatus.PENDING, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.PENDING) - ); - - assertEquals(WorkflowRuntimeStatus.SUSPENDED, - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(OrchestrationRuntimeStatus.SUSPENDED) - ); - } - - @Test - public void fromOrchestrationRuntimeStatusThrowsIllegalArgumentException() { - try { - WorkflowRuntimeStatusConverter.fromOrchestrationRuntimeStatus(null); - - fail("Expected exception not thrown"); - } catch (IllegalArgumentException e) { - assertEquals("status cannot be null", e.getMessage()); - } - } -} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeTest.java index af534b836e..fefde0fae2 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeTest.java @@ -14,11 +14,11 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.DurableTaskGrpcWorker; -import io.dapr.durabletask.DurableTaskGrpcWorkerBuilder; import io.dapr.config.Properties; import io.dapr.utils.NetworkUtils; import io.dapr.workflows.internal.GrpcChannelKeepalive; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import io.grpc.ManagedChannel; import org.junit.jupiter.api.Test; @@ -72,4 +72,30 @@ public void closeWithoutStarting() { assertDoesNotThrow(runtime::close); } } + + @Test + public void closeShutsDownAnExecutorItOwns() { + DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder().build(); + ExecutorService executorService = Executors.newCachedThreadPool(); + WorkflowRuntime runtime = new WorkflowRuntime(worker, NetworkUtils.buildGrpcManagedChannel(new Properties()), + executorService, null, true); + + runtime.close(); + + assertTrue(executorService.isShutdown(), "an executor created by the runtime must be shut down on close()"); + } + + @Test + public void closeLeavesACallerSuppliedExecutorRunning() { + DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder().build(); + ExecutorService executorService = Executors.newCachedThreadPool(); + WorkflowRuntime runtime = new WorkflowRuntime(worker, NetworkUtils.buildGrpcManagedChannel(new Properties()), + executorService, null, false); + + runtime.close(); + + assertFalse(executorService.isShutdown(), + "an executor supplied by the caller must outlive the runtime, so Spring's shared task executor is not killed"); + executorService.shutdown(); + } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowVersionWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowVersionWrapperTest.java index 31cebd5efc..23525b67dd 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowVersionWrapperTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowVersionWrapperTest.java @@ -13,7 +13,7 @@ package io.dapr.workflows.runtime; -import io.dapr.durabletask.TaskOrchestration; +import io.dapr.workflows.task.TaskOrchestration; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/ActivityHistoryPropagationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/ActivityHistoryPropagationTest.java similarity index 94% rename from durabletask-client/src/test/java/io/dapr/durabletask/ActivityHistoryPropagationTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/ActivityHistoryPropagationTest.java index 083e2db63e..7f1a56d4f3 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/ActivityHistoryPropagationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/ActivityHistoryPropagationTest.java @@ -11,11 +11,16 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import com.google.protobuf.Timestamp; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.Orchestration; +import io.dapr.workflows.task.history.HistoryPropagationScope; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.PropagatedHistoryException; +import io.dapr.workflows.task.internal.TaskActivityExecutor; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import org.junit.jupiter.api.Test; import java.util.HashMap; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskClientIT.java similarity index 91% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskClientIT.java index 6c870b8452..fba4c6b213 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskClientIT.java @@ -10,8 +10,22 @@ * See the License for the specific language governing permissions and limitations under the License. */ -package io.dapr.durabletask; - +package io.dapr.workflows.task; + +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.client.DurableTaskGrpcClientBuilder; +import io.dapr.workflows.task.client.NewOrchestrationInstanceOptions; +import io.dapr.workflows.task.client.OrchestrationMetadata; +import io.dapr.workflows.task.client.PurgeInstanceCriteria; +import io.dapr.workflows.task.client.PurgeResult; +import io.dapr.workflows.task.exception.CompositeTaskFailedException; +import io.dapr.workflows.task.exception.TaskCanceledException; +import io.dapr.workflows.task.exception.TaskFailedException; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -80,7 +94,7 @@ void emptyOrchestration() throws TimeoutException { true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(input, instance.readInputAs(String.class)); assertEquals(input, instance.readOutputAs(String.class)); } @@ -106,7 +120,7 @@ void singleTimer() throws IOException, TimeoutException { Duration timeout = delay.plus(defaultTimeout); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, timeout, false); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // Verify that the delay actually happened long expectedCompletionSecond = instance.getCreatedAt().plus(delay).getEpochSecond(); @@ -137,7 +151,7 @@ void loopWithTimer() throws IOException, TimeoutException { DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(orchestratorName, ctx -> { for (int i = 0; i < 3; i++) { - if (!ctx.getIsReplaying()) { + if (!ctx.isReplaying()) { timestamps.set(counter.get(), LocalDateTime.now()); counter.incrementAndGet(); } @@ -152,7 +166,7 @@ void loopWithTimer() throws IOException, TimeoutException { Duration timeout = delay.plus(defaultTimeout); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, timeout, false); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // Verify that the delay actually happened long expectedCompletionSecond = instance.getCreatedAt().plus(delay).getEpochSecond(); @@ -189,7 +203,7 @@ void loopWithWaitForEvent() throws IOException, TimeoutException { try { ctx.waitForExternalEvent("HELLO", delay).await(); } catch (TaskCanceledException tce) { - if (!ctx.getIsReplaying()) { + if (!ctx.isReplaying()) { timestamps.set(counter.get(), LocalDateTime.now()); counter.incrementAndGet(); } @@ -204,7 +218,7 @@ void loopWithWaitForEvent() throws IOException, TimeoutException { Duration timeout = delay.plus(defaultTimeout); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, timeout, false); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // Verify that the delay actually happened long expectedCompletionSecond = instance.getCreatedAt().plus(delay).getEpochSecond(); @@ -255,7 +269,7 @@ void longTimer() throws TimeoutException { Duration timeout = delay.plus(defaultTimeout); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, timeout, false); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus(), + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus(), String.format("Orchestration failed with error: %s", instance.getFailureDetails().getErrorMessage())); // Verify that the delay actually happened @@ -303,7 +317,7 @@ void longTimerNonblocking() throws TimeoutException { client.raiseEvent(instanceId, externalEventActivityName, "Hello world"); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String output = instance.readOutputAs(String.class); assertNotNull(output); @@ -340,7 +354,7 @@ void longTimerNonblockingNoExternal() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String output = instance.readOutputAs(String.class); assertNotNull(output); @@ -386,7 +400,7 @@ void longTimeStampTimer() throws TimeoutException { Duration timeout = delay.plus(defaultTimeout); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, timeout, false); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // Verify that the delay actually happened long expectedCompletionSecond = scheduledBefore.toInstant().getEpochSecond(); @@ -419,7 +433,7 @@ void singleTimeStampTimer() throws IOException, TimeoutException { Duration timeout = delay.plus(defaultTimeout); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, timeout, false); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // Verify that the delay actually happened long expectedCompletionSecond = zonedDateTime.toInstant().getEpochSecond(); @@ -444,7 +458,7 @@ void singleTimeStampCreateTimer() throws IOException, TimeoutException { Duration timeout = delay.plus(defaultTimeout); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, timeout, false); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // Verify that the delay actually happened long expectedCompletionSecond = zonedDateTime.toInstant().getEpochSecond(); @@ -459,11 +473,11 @@ void isReplaying() throws IOException, InterruptedException, TimeoutException { DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(orchestratorName, ctx -> { ArrayList list = new ArrayList(); - list.add(ctx.getIsReplaying()); + list.add(ctx.isReplaying()); ctx.createTimer(Duration.ofSeconds(0)).await(); - list.add(ctx.getIsReplaying()); + list.add(ctx.isReplaying()); ctx.createTimer(Duration.ofSeconds(0)).await(); - list.add(ctx.getIsReplaying()); + list.add(ctx.isReplaying()); ctx.complete(list); }) .buildAndStart(); @@ -477,7 +491,7 @@ void isReplaying() throws IOException, InterruptedException, TimeoutException { true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // Verify that the orchestrator reported the correct isReplaying values. // Note that only the values of the *final* replay are returned. @@ -514,7 +528,7 @@ void singleActivity() throws IOException, InterruptedException, TimeoutException true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String output = instance.readOutputAs(String.class); String expected = String.format("Hello, %s!", input); assertEquals(expected, output); @@ -555,7 +569,7 @@ void currentDateTimeUtc() throws IOException, TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertTrue(instance.readOutputAs(boolean.class)); } } @@ -582,7 +596,7 @@ void activityChain() throws IOException, TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(10, instance.readOutputAs(int.class)); } } @@ -594,7 +608,7 @@ void subOrchestration() throws TimeoutException { int result = 5; int input = ctx.getInput(int.class); if (input < 3) { - result += ctx.callSubOrchestrator(orchestratorName, input + 1, int.class).await(); + result += ctx.callChildWorkflow(orchestratorName, input + 1, int.class).await(); } ctx.complete(result); }).buildAndStart(); @@ -603,7 +617,7 @@ void subOrchestration() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 1); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(15, instance.readOutputAs(int.class)); } } @@ -617,7 +631,7 @@ void subOrchestrationWithActivity() throws TimeoutException { DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(parentOrchestratorName, ctx -> { int input = ctx.getInput(int.class); - int childResult = ctx.callSubOrchestrator(childOrchestratorName, input, int.class).await(); + int childResult = ctx.callChildWorkflow(childOrchestratorName, input, int.class).await(); ctx.complete(childResult); }) .addOrchestrator(childOrchestratorName, ctx -> { @@ -633,7 +647,7 @@ void subOrchestrationWithActivity() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(parentOrchestratorName, 10); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(11, instance.readOutputAs(int.class)); } } @@ -648,7 +662,7 @@ void subOrchestrationChain() throws TimeoutException { .addOrchestrator(orchestratorName, ctx -> { int input = ctx.getInput(int.class); // Chain: parent calls child which calls leaf - int result = ctx.callSubOrchestrator(leafOrchestratorName, input, int.class).await(); + int result = ctx.callChildWorkflow(leafOrchestratorName, input, int.class).await(); // Call activity after sub-orchestration completes result = ctx.callActivity(activityName, result, int.class).await(); ctx.complete(result); @@ -667,7 +681,7 @@ void subOrchestrationChain() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 3); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(12, instance.readOutputAs(int.class)); } } @@ -683,7 +697,7 @@ void subOrchestrationFanOut() throws TimeoutException { .addOrchestrator(parentOrchestratorName, ctx -> { // Fan out: launch multiple sub-orchestrations in parallel List> tasks = IntStream.range(1, childCount + 1) - .mapToObj(i -> ctx.callSubOrchestrator(childOrchestratorName, i, int.class)) + .mapToObj(i -> ctx.callChildWorkflow(childOrchestratorName, i, int.class)) .collect(Collectors.toList()); List results = ctx.allOf(tasks).await(); @@ -706,7 +720,7 @@ void subOrchestrationFanOut() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(parentOrchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); // 1^2 + 2^2 + 3^2 + 4^2 + 5^2 = 1 + 4 + 9 + 16 + 25 = 55 assertEquals(55, instance.readOutputAs(int.class)); } @@ -720,7 +734,7 @@ void subOrchestrationWithInstanceId() throws TimeoutException { DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(parentOrchestratorName, ctx -> { String childInstanceId = ctx.getInstanceId() + ":child"; - String result = ctx.callSubOrchestrator( + String result = ctx.callChildWorkflow( childOrchestratorName, "hello", childInstanceId, String.class).await(); ctx.complete(result); }) @@ -735,7 +749,7 @@ void subOrchestrationWithInstanceId() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(parentOrchestratorName, "test"); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals("hello world", instance.readOutputAs(String.class)); } } @@ -758,7 +772,7 @@ void continueAsNew() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 1); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(10, instance.readOutputAs(int.class)); } } @@ -789,7 +803,7 @@ void continueAsNewWithExternalEvents() throws TimeoutException, InterruptedExcep OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(expectedEventCount, instance.readOutputAs(int.class)); } } @@ -811,7 +825,7 @@ void termination() throws TimeoutException { OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); assertEquals(instanceId, instance.getInstanceId()); - assertEquals(OrchestrationRuntimeStatus.TERMINATED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.TERMINATED, instance.getRuntimeStatus()); assertEquals(expectOutput, instance.readOutputAs(String.class)); } } @@ -839,7 +853,7 @@ void restartOrchestrationWithNewInstanceId(boolean restartWithNewInstanceId) thr } else { assertEquals(instanceId, newInstanceId); } - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals("\"RestartTest\"", instance.getSerializedInput()); } } @@ -887,7 +901,7 @@ void suspendResumeOrchestration() throws TimeoutException, InterruptedException client.suspendInstance(instanceId); OrchestrationMetadata instance = client.waitForInstanceStart(instanceId, defaultTimeout); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.SUSPENDED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.SUSPENDED, instance.getRuntimeStatus()); client.raiseEvent(instanceId, eventName, eventPayload); @@ -903,7 +917,7 @@ void suspendResumeOrchestration() throws TimeoutException, InterruptedException assertNotNull(instance); assertEquals(instanceId, instance.getInstanceId()); assertEquals(eventPayload, instance.readOutputAs(String.class)); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); } } @@ -930,7 +944,7 @@ void terminateSuspendOrchestration() throws TimeoutException, InterruptedExcepti OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, false); assertNotNull(instance); assertEquals(instanceId, instance.getInstanceId()); - assertEquals(OrchestrationRuntimeStatus.TERMINATED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.TERMINATED, instance.getRuntimeStatus()); } } @@ -961,7 +975,7 @@ void activityFanOut() throws IOException, TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); List output = instance.readOutputAs(List.class); assertNotNull(output); @@ -1008,7 +1022,7 @@ void externalEvents() throws IOException, TimeoutException { OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); int output = instance.readOutputAs(int.class); assertEquals(eventCount, output); @@ -1043,7 +1057,7 @@ void externalEventsWithTimeouts(boolean raiseEvent) throws IOException, TimeoutE OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String output = instance.readOutputAs(String.class); if (raiseEvent) { @@ -1081,7 +1095,7 @@ void setCustomStatus() throws TimeoutException { metadata = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertTrue(metadata.isCustomStatusFetched()); assertEquals(payload, metadata.readCustomStatusAs(HashMap.class)); } @@ -1111,7 +1125,7 @@ void clearCustomStatus() throws TimeoutException { metadata = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertFalse(metadata.isCustomStatusFetched()); } } @@ -1135,7 +1149,7 @@ void purgeInstanceId() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata metadata = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertEquals(1, metadata.readOutputAs(int.class)); PurgeResult result = client.purgeInstance(instanceId); @@ -1184,7 +1198,7 @@ void purgeInstanceFilter() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata metadata = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertEquals(1, metadata.readOutputAs(int.class)); // Test CreatedTimeFrom @@ -1208,25 +1222,25 @@ void purgeInstanceFilter() throws TimeoutException { String instanceId1 = client.scheduleNewOrchestrationInstance(plusOne, 0); metadata = client.waitForInstanceCompletion(instanceId1, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertEquals(1, metadata.readOutputAs(int.class)); String instanceId2 = client.scheduleNewOrchestrationInstance(plusTwo, 10); metadata = client.waitForInstanceCompletion(instanceId2, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertEquals(12, metadata.readOutputAs(int.class)); String instanceId3 = client.scheduleNewOrchestrationInstance(terminate); client.terminate(instanceId3, terminate); metadata = client.waitForInstanceCompletion(instanceId3, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.TERMINATED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.TERMINATED, metadata.getRuntimeStatus()); assertEquals(terminate, metadata.readOutputAs(String.class)); - HashSet runtimeStatusFilters = Stream.of( - OrchestrationRuntimeStatus.TERMINATED, - OrchestrationRuntimeStatus.COMPLETED + HashSet runtimeStatusFilters = Stream.of( + WorkflowRuntimeStatus.TERMINATED, + WorkflowRuntimeStatus.COMPLETED ).collect(Collectors.toCollection(HashSet::new)); criteria.setCreatedTimeTo(Instant.now()); @@ -1276,19 +1290,19 @@ void purgeInstanceFilterTimeout() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata metadata = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertEquals(1, metadata.readOutputAs(int.class)); String instanceId1 = client.scheduleNewOrchestrationInstance(plusOne, 0); metadata = client.waitForInstanceCompletion(instanceId1, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertEquals(1, metadata.readOutputAs(int.class)); String instanceId2 = client.scheduleNewOrchestrationInstance(plusTwo, 10); metadata = client.waitForInstanceCompletion(instanceId2, defaultTimeout, true); assertNotNull(metadata); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); assertEquals(12, metadata.readOutputAs(int.class)); PurgeInstanceCriteria criteria = new PurgeInstanceCriteria(); @@ -1393,15 +1407,15 @@ void activityFanOutWithException() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.FAILED, instance.getRuntimeStatus()); List output = instance.readOutputAs(List.class); assertNull(output); - FailureDetails details = instance.getFailureDetails(); + WorkflowFailureDetails details = instance.getFailureDetails(); assertNotNull(details); assertEquals(exceptionMessage, details.getErrorMessage()); - assertEquals("io.dapr.durabletask.CompositeTaskFailedException", details.getErrorType()); + assertEquals(CompositeTaskFailedException.class.getName(), details.getErrorType()); assertNotNull(details.getStackTrace()); } } @@ -1440,7 +1454,7 @@ void thenApply() throws IOException, InterruptedException, TimeoutException { true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String output = instance.readOutputAs(String.class); String expected = String.format("Hello, %s!%s", input, suffix); assertEquals(expected, output); @@ -1476,7 +1490,7 @@ void externalEventThenAccept() throws InterruptedException, TimeoutException { OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(expectedEventCount, instance.readOutputAs(int.class)); } } @@ -1489,8 +1503,8 @@ void activityAllOf() throws IOException, TimeoutException { final int activityMiddle = 5; final int activityCount = 10; final AtomicBoolean throwException = new AtomicBoolean(true); - final RetryPolicy retryPolicy = new RetryPolicy(2, Duration.ofSeconds(5)); - final TaskOptions taskOptions = TaskOptions.withRetryPolicy(retryPolicy); + final WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(2, Duration.ofSeconds(5)); + final WorkflowTaskOptions taskOptions = WorkflowTaskOptions.withRetryPolicy(retryPolicy); DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(orchestratorName, ctx -> { @@ -1525,7 +1539,7 @@ void activityAllOf() throws IOException, TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); List output = instance.readOutputAs(List.class); assertNotNull(output); @@ -1547,8 +1561,8 @@ void activityAllOfException() throws IOException, TimeoutException { final String retryActivityName = "RetryToStringException"; final String result = "test fail"; final int activityMiddle = 5; - final RetryPolicy retryPolicy = new RetryPolicy(2, Duration.ofSeconds(5)); - final TaskOptions taskOptions = TaskOptions.withRetryPolicy(retryPolicy); + final WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(2, Duration.ofSeconds(5)); + final WorkflowTaskOptions taskOptions = WorkflowTaskOptions.withRetryPolicy(retryPolicy); DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(orchestratorName, ctx -> { @@ -1593,7 +1607,7 @@ void activityAllOfException() throws IOException, TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String output = instance.readOutputAs(String.class); assertNotNull(output); @@ -1610,8 +1624,8 @@ void activityAnyOf() throws IOException, TimeoutException { final int activityMiddle = 5; final int activityCount = 10; final AtomicBoolean throwException = new AtomicBoolean(true); - final RetryPolicy retryPolicy = new RetryPolicy(2, Duration.ofSeconds(5)); - final TaskOptions taskOptions = TaskOptions.withRetryPolicy(retryPolicy); + final WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(2, Duration.ofSeconds(5)); + final WorkflowTaskOptions taskOptions = WorkflowTaskOptions.withRetryPolicy(retryPolicy); DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(orchestratorName, ctx -> { @@ -1643,7 +1657,7 @@ void activityAnyOf() throws IOException, TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String output = instance.readOutputAs(String.class); assertNotNull(output); @@ -1692,7 +1706,7 @@ public void newUUIDTest() { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertTrue(instance.readOutputAs(boolean.class)); } catch (TimeoutException e) { throw new RuntimeException(e); @@ -1704,8 +1718,8 @@ public void newUUIDTest() { public void taskExecutionIdTest() { var orchestratorName = "test-task-execution-id"; var retryActivityName = "RetryN"; - final RetryPolicy retryPolicy = new RetryPolicy(4, Duration.ofSeconds(3)); - final TaskOptions taskOptions = TaskOptions.withRetryPolicy(retryPolicy); + final WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(4, Duration.ofSeconds(3)); + final WorkflowTaskOptions taskOptions = WorkflowTaskOptions.withRetryPolicy(retryPolicy); var execMap = new HashMap(); @@ -1737,7 +1751,7 @@ public void taskExecutionIdTest() { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); assertEquals(2, execMap.size()); assertTrue(instance.readOutputAs(boolean.class)); } catch (TimeoutException e) { diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientScheduleTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientScheduleTest.java similarity index 95% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientScheduleTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientScheduleTest.java index 4c9cc74775..8a32893e7c 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientScheduleTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientScheduleTest.java @@ -11,10 +11,13 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.client.DurableTaskGrpcClientBuilder; +import io.dapr.workflows.task.client.NewOrchestrationInstanceOptions; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.Status; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientTlsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientTlsTest.java similarity index 99% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientTlsTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientTlsTest.java index b60b26be74..bdb30d5c3e 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientTlsTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientTlsTest.java @@ -10,7 +10,7 @@ // * See the License for the specific language governing permissions and //limitations under the License. //*/ -//package io.dapr.durabletask; +//package io.dapr.workflows.task; // //import org.junit.jupiter.api.AfterEach; //import org.junit.jupiter.api.Test; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientTracingTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientTracingTest.java similarity index 97% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientTracingTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientTracingTest.java index ce7ee5fc48..5b01c23b83 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcClientTracingTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcClientTracingTest.java @@ -11,10 +11,12 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.client.DurableTaskGrpcClientBuilder; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.inprocess.InProcessChannelBuilder; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerChannelBackoffTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerChannelBackoffTest.java similarity index 97% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerChannelBackoffTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerChannelBackoffTest.java index ef975d736b..b5fd92e63f 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerChannelBackoffTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerChannelBackoffTest.java @@ -11,10 +11,12 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Server; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerReconnectTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerReconnectTest.java similarity index 97% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerReconnectTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerReconnectTest.java index 4042f9ad44..306fd8c4ed 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerReconnectTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerReconnectTest.java @@ -11,10 +11,12 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.Status; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerShutdownTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerShutdownTest.java similarity index 96% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerShutdownTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerShutdownTest.java index 71368af4be..5e457e539b 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerShutdownTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerShutdownTest.java @@ -11,8 +11,10 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import org.junit.jupiter.api.Test; import java.time.Duration; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerStatefulHistoryTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerStatefulHistoryTest.java similarity index 98% rename from durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerStatefulHistoryTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerStatefulHistoryTest.java index ca42d5916a..687bc11759 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskGrpcWorkerStatefulHistoryTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/DurableTaskGrpcWorkerStatefulHistoryTest.java @@ -11,10 +11,12 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.Status; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/ErrorHandlingIT.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/ErrorHandlingIT.java similarity index 80% rename from durabletask-client/src/test/java/io/dapr/durabletask/ErrorHandlingIT.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/ErrorHandlingIT.java index f1c868f0a4..14cf448bd6 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/ErrorHandlingIT.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/ErrorHandlingIT.java @@ -11,8 +11,18 @@ limitations under the License. */ -package io.dapr.durabletask; - +package io.dapr.workflows.task; + +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryHandler; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.client.DurableTaskGrpcClientBuilder; +import io.dapr.workflows.task.client.OrchestrationMetadata; +import io.dapr.workflows.task.exception.TaskFailedException; +import io.dapr.workflows.task.exception.WorkflowFailureDetails; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -50,9 +60,9 @@ void orchestratorException() throws TimeoutException { String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.FAILED, instance.getRuntimeStatus()); - FailureDetails details = instance.getFailureDetails(); + WorkflowFailureDetails details = instance.getFailureDetails(); assertNotNull(details); assertEquals("java.lang.RuntimeException", details.getErrorType()); assertTrue(details.getErrorMessage().contains(errorMessage)); @@ -95,9 +105,9 @@ void activityException(boolean handleException) throws TimeoutException { assertNotNull(result); assertEquals("handled", result); } else { - assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.FAILED, instance.getRuntimeStatus()); - FailureDetails details = instance.getFailureDetails(); + WorkflowFailureDetails details = instance.getFailureDetails(); assertNotNull(details); String expectedMessage = String.format( @@ -105,7 +115,7 @@ void activityException(boolean handleException) throws TimeoutException { activityName, errorMessage); assertEquals(expectedMessage, details.getErrorMessage()); - assertEquals("io.dapr.durabletask.TaskFailedException", details.getErrorType()); + assertEquals(TaskFailedException.class.getName(), details.getErrorType()); assertNotNull(details.getStackTrace()); // CONSIDER: Additional validation of getErrorDetails? } @@ -118,11 +128,11 @@ public void retryActivityFailures(int maxNumberOfAttempts) throws TimeoutExcepti // There is one task for each activity call and one task between each retry int expectedTaskCount = (maxNumberOfAttempts * 2) - 1; this.retryOnFailuresCoreTest(maxNumberOfAttempts, expectedTaskCount, ctx -> { - RetryPolicy retryPolicy = getCommonRetryPolicy(maxNumberOfAttempts); + WorkflowTaskRetryPolicy retryPolicy = getCommonRetryPolicy(maxNumberOfAttempts); ctx.callActivity( "BustedActivity", null, - TaskOptions.withRetryPolicy(retryPolicy)).await(); + WorkflowTaskOptions.withRetryPolicy(retryPolicy)).await(); }); } @@ -134,8 +144,8 @@ public void retryActivityFailuresWithCustomLogic(int maxNumberOfAttempts) throws // Run the test and get back the details of the last failure this.retryOnFailuresCoreTest(maxNumberOfAttempts, maxNumberOfAttempts, ctx -> { - RetryHandler retryHandler = getCommonRetryHandler(retryHandlerCalls, maxNumberOfAttempts); - TaskOptions options = TaskOptions.withRetryHandler(retryHandler); + WorkflowTaskRetryHandler retryHandler = getCommonRetryHandler(retryHandlerCalls, maxNumberOfAttempts); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryHandler(retryHandler); ctx.callActivity("BustedActivity", null, options).await(); }); @@ -153,7 +163,7 @@ void subOrchestrationException(boolean handleException) throws TimeoutException DurableTaskGrpcWorker worker = this.createWorkerBuilder() .addOrchestrator(orchestratorName, ctx -> { try { - String result = ctx.callSubOrchestrator(subOrchestratorName, "", String.class).await(); + String result = ctx.callChildWorkflow(subOrchestratorName, "", String.class).await(); ctx.complete(result); } catch (TaskFailedException ex) { if (handleException) { @@ -173,20 +183,20 @@ void subOrchestrationException(boolean handleException) throws TimeoutException OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); if (handleException) { - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); String result = instance.readOutputAs(String.class); assertNotNull(result); assertEquals("handled", result); } else { - assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus()); - FailureDetails details = instance.getFailureDetails(); + assertEquals(WorkflowRuntimeStatus.FAILED, instance.getRuntimeStatus()); + WorkflowFailureDetails details = instance.getFailureDetails(); assertNotNull(details); String expectedMessage = String.format( "Task '%s' (#0) failed with an unhandled exception: %s", subOrchestratorName, errorMessage); assertEquals(expectedMessage, details.getErrorMessage()); - assertEquals("io.dapr.durabletask.TaskFailedException", details.getErrorType()); + assertEquals(TaskFailedException.class.getName(), details.getErrorType()); assertNotNull(details.getStackTrace()); // CONSIDER: Additional validation of getStackTrace? } @@ -199,12 +209,12 @@ public void retrySubOrchestratorFailures(int maxNumberOfAttempts) throws Timeout // There is one task for each sub-orchestrator call and one task between each retry int expectedTaskCount = (maxNumberOfAttempts * 2) - 1; this.retryOnFailuresCoreTest(maxNumberOfAttempts, expectedTaskCount, ctx -> { - RetryPolicy retryPolicy = getCommonRetryPolicy(maxNumberOfAttempts); - ctx.callSubOrchestrator( + WorkflowTaskRetryPolicy retryPolicy = getCommonRetryPolicy(maxNumberOfAttempts); + ctx.callChildWorkflow( "BustedSubOrchestrator", null, null, - TaskOptions.withRetryPolicy(retryPolicy)).await(); + WorkflowTaskOptions.withRetryPolicy(retryPolicy)).await(); }); } @@ -216,24 +226,24 @@ public void retrySubOrchestrationFailuresWithCustomLogic(int maxNumberOfAttempts // Run the test and get back the details of the last failure this.retryOnFailuresCoreTest(maxNumberOfAttempts, maxNumberOfAttempts, ctx -> { - RetryHandler retryHandler = getCommonRetryHandler(retryHandlerCalls, maxNumberOfAttempts); - TaskOptions options = TaskOptions.withRetryHandler(retryHandler); - ctx.callSubOrchestrator("BustedSubOrchestrator", null, null, options).await(); + WorkflowTaskRetryHandler retryHandler = getCommonRetryHandler(retryHandlerCalls, maxNumberOfAttempts); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryHandler(retryHandler); + ctx.callChildWorkflow("BustedSubOrchestrator", null, null, options).await(); }); // Assert that the retry handle got invoked the expected number of times assertEquals(maxNumberOfAttempts, retryHandlerCalls.get()); } - private static RetryPolicy getCommonRetryPolicy(int maxNumberOfAttempts) { + private static WorkflowTaskRetryPolicy getCommonRetryPolicy(int maxNumberOfAttempts) { // Include a small delay between each retry to exercise the implicit timer path - return new RetryPolicy(maxNumberOfAttempts, Duration.ofMillis(1)); + return new WorkflowTaskRetryPolicy(maxNumberOfAttempts, Duration.ofMillis(1)); } - private static RetryHandler getCommonRetryHandler(AtomicInteger handlerInvocationCounter, int maxNumberOfAttempts) { + private static WorkflowTaskRetryHandler getCommonRetryHandler(AtomicInteger handlerInvocationCounter, int maxNumberOfAttempts) { return ctx -> { // Retry handlers get executed on the orchestrator thread and go through replay - if (!ctx.getOrchestrationContext().getIsReplaying()) { + if (!ctx.getWorkflowContext().isReplaying()) { handlerInvocationCounter.getAndIncrement(); } @@ -261,7 +271,7 @@ private static RetryHandler getCommonRetryHandler(AtomicInteger handlerInvocatio * "BustedActivity" activity or the "BustedSubOrchestrator" sub-orchestration. * @return Returns the details of the last activity or sub-orchestration failure. */ - private FailureDetails retryOnFailuresCoreTest( + private WorkflowFailureDetails retryOnFailuresCoreTest( int maxNumberOfAttempts, int expectedTaskCount, TaskOrchestration mainOrchestration) throws TimeoutException { @@ -291,10 +301,10 @@ private FailureDetails retryOnFailuresCoreTest( String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, ""); OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.FAILED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.FAILED, instance.getRuntimeStatus()); // Make sure the exception details are still what we expect - FailureDetails details = instance.getFailureDetails(); + WorkflowFailureDetails details = instance.getFailureDetails(); assertNotNull(details); // Confirm the number of attempts diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/HistoryPropagationIntegrationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/HistoryPropagationIntegrationTest.java similarity index 94% rename from durabletask-client/src/test/java/io/dapr/durabletask/HistoryPropagationIntegrationTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/HistoryPropagationIntegrationTest.java index 7f2d183a04..01f6814bfc 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/HistoryPropagationIntegrationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/HistoryPropagationIntegrationTest.java @@ -11,15 +11,24 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.task.history.ActivityResult; +import io.dapr.workflows.task.history.HistoryPropagationScope; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.WorkflowResult; +import io.dapr.workflows.task.internal.TaskActivityExecutor; +import io.dapr.workflows.task.internal.TaskOrchestrationExecutor; +import io.dapr.workflows.task.internal.TaskOrchestratorResult; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -109,10 +118,10 @@ void lineageScope_parentToChild_childReceivesParentEvents() { final String childName = "FraudDetection"; TaskOrchestration parentOrchestration = ctx -> { - TaskOptions opts = TaskOptions.builder() + WorkflowTaskOptions opts = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.LINEAGE) .build(); - ctx.callSubOrchestrator(childName, "payment-data", "child-inst-1", opts, String.class); + ctx.callChildWorkflow(childName, "payment-data", "child-inst-1", opts, String.class); }; TaskOrchestrationExecutor parentExecutor = createExecutor(parentName, parentOrchestration, "payment-app"); @@ -220,10 +229,10 @@ void ownHistoryScope_parentToChild_childReceivesOnlyCallerEvents() { final String childName = "RecordTransaction"; TaskOrchestration parentOrchestration = ctx -> { - TaskOptions opts = TaskOptions.builder() + WorkflowTaskOptions opts = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.OWN_HISTORY) .build(); - ctx.callSubOrchestrator(childName, "settlement-data", "child-inst-2", opts, String.class); + ctx.callChildWorkflow(childName, "settlement-data", "child-inst-2", opts, String.class); }; TaskOrchestrationExecutor parentExecutor = createExecutor(parentName, parentOrchestration, "settlement-app"); @@ -301,7 +310,7 @@ void lineageScope_parentToActivity_activityReceivesHistory() throws Throwable { final String activityName = "SettlePayment"; TaskOrchestration parentOrchestration = ctx -> { - TaskOptions opts = TaskOptions.builder() + WorkflowTaskOptions opts = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.LINEAGE) .build(); ctx.callActivity(activityName, "settle-data", opts, String.class); @@ -489,11 +498,11 @@ void historyPropagation_combinedWithCrossAppRouting_bothWork() { final String targetAppId = "target-app"; TaskOrchestration parentOrchestration = ctx -> { - TaskOptions opts = TaskOptions.builder() + WorkflowTaskOptions opts = WorkflowTaskOptions.builder() .appID(targetAppId) .historyPropagationScope(HistoryPropagationScope.LINEAGE) .build(); - ctx.callSubOrchestrator(childName, "input", "child-inst-combined", opts, String.class); + ctx.callChildWorkflow(childName, "input", "child-inst-combined", opts, String.class); }; TaskOrchestrationExecutor parentExecutor = createExecutor(parentName, parentOrchestration, sourceAppId); diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/IntegrationTestBase.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/IntegrationTestBase.java similarity index 93% rename from durabletask-client/src/test/java/io/dapr/durabletask/IntegrationTestBase.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/IntegrationTestBase.java index 6877d1ea43..5f76f816bc 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/IntegrationTestBase.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/IntegrationTestBase.java @@ -11,9 +11,11 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorkerBuilder; import io.grpc.Channel; import org.junit.jupiter.api.AfterEach; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/StatefulHistoryIT.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/StatefulHistoryIT.java similarity index 94% rename from durabletask-client/src/test/java/io/dapr/durabletask/StatefulHistoryIT.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/StatefulHistoryIT.java index f2099f85be..a7f6776713 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/StatefulHistoryIT.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/StatefulHistoryIT.java @@ -11,8 +11,13 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.task.client.DurableTaskClient; +import io.dapr.workflows.task.client.DurableTaskGrpcClientBuilder; +import io.dapr.workflows.task.client.OrchestrationMetadata; +import io.dapr.workflows.task.worker.DurableTaskGrpcWorker; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.jupiter.api.Tag; @@ -112,7 +117,7 @@ private RunResult runAccumulate(boolean disableStatefulHistory) throws TimeoutEx OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, COMPLETION_TIMEOUT, true); assertNotNull(instance); - assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + assertEquals(WorkflowRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); return new RunResult( observer.deltas(instanceId), diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/SubOrchestrationCrossAppTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/SubOrchestrationCrossAppTest.java similarity index 93% rename from durabletask-client/src/test/java/io/dapr/durabletask/SubOrchestrationCrossAppTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/SubOrchestrationCrossAppTest.java index 854ac2012d..0271a6ebba 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/SubOrchestrationCrossAppTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/SubOrchestrationCrossAppTest.java @@ -11,15 +11,20 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import io.dapr.workflows.task.internal.TaskOrchestrationExecutor; +import io.dapr.workflows.task.internal.TaskOrchestratorResult; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -114,7 +119,7 @@ public Boolean isLatestVersion() { } // ================================================================================== - // Tests for callSubOrchestrator with cross-app routing + // Tests for callChildWorkflow with cross-app routing // ================================================================================== @Test @@ -126,8 +131,8 @@ void callSubOrchestrator_withTargetAppId_setsRouterOnAction() { // The orchestrator calls a sub-orchestration with a target app ID TaskOrchestration orchestration = ctx -> { - TaskOptions options = TaskOptions.withAppID(targetAppId); - ctx.callSubOrchestrator(subOrchestratorName, "input", "child-instance-1", options, String.class); + WorkflowTaskOptions options = WorkflowTaskOptions.withAppID(targetAppId); + ctx.callChildWorkflow(subOrchestratorName, "input", "child-instance-1", options, String.class); }; TaskOrchestrationExecutor executor = createExecutor(orchestratorName, orchestration, sourceAppId); @@ -170,7 +175,7 @@ void callSubOrchestrator_withoutTargetAppId_setsRouterWithSourceOnly() { // The orchestrator calls a sub-orchestration WITHOUT a target app ID TaskOrchestration orchestration = ctx -> { - ctx.callSubOrchestrator(subOrchestratorName, "input", "child-instance-1", null, String.class); + ctx.callChildWorkflow(subOrchestratorName, "input", "child-instance-1", null, String.class); }; TaskOrchestrationExecutor executor = createExecutor(orchestratorName, orchestration, sourceAppId); @@ -206,7 +211,7 @@ void callSubOrchestrator_withNullAppId_noRouterSet() { // The orchestrator calls a sub-orchestration with no app routing context TaskOrchestration orchestration = ctx -> { - ctx.callSubOrchestrator(subOrchestratorName, "input", "child-instance-1", null, String.class); + ctx.callChildWorkflow(subOrchestratorName, "input", "child-instance-1", null, String.class); }; // Create executor with null appId (no router context) @@ -446,8 +451,8 @@ void crossAppSubOrchestration_fullFlow_routersCorrectlySet() { // Parent orchestrator calls a cross-app sub-orchestration and then completes TaskOrchestration orchestration = ctx -> { - TaskOptions options = TaskOptions.withAppID(targetAppId); - ctx.callSubOrchestrator(subOrchestratorName, "data", "child-id-1", options, String.class); + WorkflowTaskOptions options = WorkflowTaskOptions.withAppID(targetAppId); + ctx.callChildWorkflow(subOrchestratorName, "data", "child-id-1", options, String.class); // Note: orchestrator will yield here waiting for the sub-orchestration to complete }; @@ -488,7 +493,7 @@ void callSubOrchestrator_withEmptyAppId_noRouterSet() { final String subOrchestratorName = "ChildOrchestrator"; TaskOrchestration orchestration = ctx -> { - ctx.callSubOrchestrator(subOrchestratorName, "input", "child-1", null, String.class); + ctx.callChildWorkflow(subOrchestratorName, "input", "child-1", null, String.class); }; // Executor created with empty appId @@ -520,12 +525,12 @@ void callSubOrchestrator_withRetryPolicyAndAppId_setsRouterAndRetries() { final String targetAppId = "app2"; TaskOrchestration orchestration = ctx -> { - RetryPolicy retryPolicy = new RetryPolicy(3, Duration.ofSeconds(1)); - TaskOptions options = TaskOptions.builder() + WorkflowTaskRetryPolicy retryPolicy = new WorkflowTaskRetryPolicy(3, Duration.ofSeconds(1)); + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .retryPolicy(retryPolicy) .appID(targetAppId) .build(); - ctx.callSubOrchestrator(subOrchestratorName, "input", "child-1", options, String.class); + ctx.callChildWorkflow(subOrchestratorName, "input", "child-1", options, String.class); }; TaskOrchestrationExecutor executor = createExecutor(orchestratorName, orchestration, sourceAppId); diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/WorkItemObserver.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/WorkItemObserver.java similarity index 99% rename from durabletask-client/src/test/java/io/dapr/durabletask/WorkItemObserver.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/WorkItemObserver.java index 60eb1a5146..270224deb1 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/WorkItemObserver.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/WorkItemObserver.java @@ -11,7 +11,7 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.grpc.CallOptions; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/task/exception/WorkflowFailureDetailsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/exception/WorkflowFailureDetailsTest.java new file mode 100644 index 0000000000..d2bc6bbee2 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/exception/WorkflowFailureDetailsTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.task.exception; + +import io.dapr.workflows.task.history.PropagatedHistoryException; +import io.dapr.workflows.task.interruption.ContinueAsNewInterruption; +import io.dapr.workflows.task.interruption.OrchestratorBlockedException; +import io.dapr.workflows.task.serialization.DataConverter; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WorkflowFailureDetailsTest { + + private static WorkflowFailureDetails failureOfType(String errorType) { + return new WorkflowFailureDetails(errorType, "boom", "", false); + } + + @Test + public void isCausedByResolvesACurrentErrorType() { + assertTrue(failureOfType(TaskFailedException.class.getName()).isCausedBy(TaskFailedException.class)); + } + + @Test + public void isCausedByHonoursTheExceptionHierarchy() { + // TaskCanceledException extends TaskFailedException + assertTrue(failureOfType(TaskCanceledException.class.getName()).isCausedBy(TaskFailedException.class)); + assertTrue(failureOfType(TaskFailedException.class.getName()).isCausedBy(RuntimeException.class)); + } + + /** + * The error type is the exception's fully qualified name and it is persisted into workflow + * history. These names shipped under io.dapr.durabletask before the durable task client was + * folded into this module, so a workflow started on an older SDK carries them across an upgrade. + * Without the legacy mapping isCausedBy answered false and compensation logic took the wrong + * branch, with nothing logged and nothing thrown. + */ + @Test + public void legacyNamesMapOntoTheirCurrentTypes() { + assertTrue(failureOfType("io.dapr.durabletask.TaskFailedException") + .isCausedBy(TaskFailedException.class)); + assertTrue(failureOfType("io.dapr.durabletask.TaskCanceledException") + .isCausedBy(TaskCanceledException.class)); + assertTrue(failureOfType("io.dapr.durabletask.CompositeTaskFailedException") + .isCausedBy(CompositeTaskFailedException.class)); + assertTrue(failureOfType("io.dapr.durabletask.NonDeterministicOrchestratorException") + .isCausedBy(NonDeterministicOrchestratorException.class)); + assertTrue(failureOfType("io.dapr.durabletask.PropagatedHistoryException") + .isCausedBy(PropagatedHistoryException.class)); + assertTrue(failureOfType("io.dapr.durabletask.orchestration.exception.VersionNotRegisteredException") + .isCausedBy(VersionNotRegisteredException.class)); + assertTrue(failureOfType("io.dapr.durabletask.interruption.OrchestratorBlockedException") + .isCausedBy(OrchestratorBlockedException.class)); + assertTrue(failureOfType("io.dapr.durabletask.interruption.ContinueAsNewInterruption") + .isCausedBy(ContinueAsNewInterruption.class)); + } + + /** + * DataConverterException is nested inside the DataConverter interface, so its binary name uses + * '$' and it has no source file of its own. Enumerating the legacy exception types by file name + * missed it, leaving serialization failures persisted by the old SDK unresolvable. + */ + @Test + public void theNestedConverterExceptionIsMappedToo() { + assertTrue(failureOfType("io.dapr.durabletask.DataConverter$DataConverterException") + .isCausedBy(DataConverter.DataConverterException.class)); + } + + /** + * An unresolvable error type answers false to EVERY query, not just the exact-type check, so a + * missing legacy entry also breaks the broad catch-all questions callers most often ask. + */ + @Test + public void theNestedConverterExceptionAlsoAnswersBroaderQueries() { + WorkflowFailureDetails details = + failureOfType("io.dapr.durabletask.DataConverter$DataConverterException"); + + assertTrue(details.isCausedBy(RuntimeException.class)); + assertTrue(details.isCausedBy(Exception.class)); + } + + @Test + public void aLegacyNameDoesNotMatchAnUnrelatedType() { + assertFalse(failureOfType("io.dapr.durabletask.TaskFailedException") + .isCausedBy(IllegalStateException.class)); + } + + /** + * A user exception that happens to share a simple name with an SDK one must not match. This is + * why the fix is an explicit alias table rather than a simple-name comparison. + */ + @Test + public void anUnrelatedTypeWithTheSameSimpleNameDoesNotMatch() { + assertFalse(failureOfType("com.example.TaskFailedException").isCausedBy(TaskFailedException.class)); + } + + @Test + public void anUnloadableErrorTypeAnswersFalse() { + assertFalse(failureOfType("com.example.NotOnTheClasspath").isCausedBy(RuntimeException.class)); + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/task/history/HistoryPropagationScopeTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/history/HistoryPropagationScopeTest.java new file mode 100644 index 0000000000..c02964c4b5 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/history/HistoryPropagationScopeTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.task.history; + +import io.dapr.durabletask.implementation.protobuf.Orchestration; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Lives in this package because fromProto is package-private: it is called only by + * PropagatedHistory, so it stays hidden rather than being widened for a test. + */ +public class HistoryPropagationScopeTest { + + @Test + void fromProtoConvertsCorrectly() { + assertEquals(HistoryPropagationScope.NONE, + HistoryPropagationScope.fromProto( + Orchestration.HistoryPropagationScope.HISTORY_PROPAGATION_SCOPE_NONE)); + assertEquals(HistoryPropagationScope.OWN_HISTORY, + HistoryPropagationScope.fromProto( + Orchestration.HistoryPropagationScope.HISTORY_PROPAGATION_SCOPE_OWN_HISTORY)); + assertEquals(HistoryPropagationScope.LINEAGE, + HistoryPropagationScope.fromProto( + Orchestration.HistoryPropagationScope.HISTORY_PROPAGATION_SCOPE_LINEAGE)); + } +} diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/AgentLoopEventDeliveryTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/AgentLoopEventDeliveryTest.java similarity index 97% rename from durabletask-client/src/test/java/io/dapr/durabletask/AgentLoopEventDeliveryTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/internal/AgentLoopEventDeliveryTest.java index 7dde1c6d73..3d38effcf6 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/AgentLoopEventDeliveryTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/AgentLoopEventDeliveryTest.java @@ -11,15 +11,17 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.TaskOrchestration; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import org.junit.jupiter.api.Test; import java.time.Duration; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/HistoryPropagationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/HistoryPropagationTest.java similarity index 95% rename from durabletask-client/src/test/java/io/dapr/durabletask/HistoryPropagationTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/internal/HistoryPropagationTest.java index 5525f63738..1b14e0a578 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/HistoryPropagationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/HistoryPropagationTest.java @@ -11,15 +11,23 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.task.TaskOrchestration; +import io.dapr.workflows.task.history.ActivityResult; +import io.dapr.workflows.task.history.ChildWorkflowResult; +import io.dapr.workflows.task.history.HistoryPropagationScope; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.WorkflowResult; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -108,7 +116,7 @@ void callActivity_withLineageScope_setsHistoryPropagationScopeOnAction() { final String activityName = "MyActivity"; TaskOrchestration orchestration = ctx -> { - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.LINEAGE) .build(); ctx.callActivity(activityName, "input", options, String.class); @@ -143,7 +151,7 @@ void callActivity_withOwnHistoryScope_setsHistoryPropagationScopeOnAction() { final String activityName = "MyActivity"; TaskOrchestration orchestration = ctx -> { - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.OWN_HISTORY) .build(); ctx.callActivity(activityName, "input", options, String.class); @@ -200,10 +208,10 @@ void callSubOrchestrator_withLineageScope_setsHistoryPropagationScopeOnAction() final String childName = "ChildOrchestrator"; TaskOrchestration orchestration = ctx -> { - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.LINEAGE) .build(); - ctx.callSubOrchestrator(childName, "input", "child-1", options, String.class); + ctx.callChildWorkflow(childName, "input", "child-1", options, String.class); }; TaskOrchestrationExecutor executor = createExecutor(orchestratorName, orchestration); @@ -235,10 +243,10 @@ void callSubOrchestrator_withOwnHistoryScope_setsHistoryPropagationScopeOnAction final String childName = "ChildOrchestrator"; TaskOrchestration orchestration = ctx -> { - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.OWN_HISTORY) .build(); - ctx.callSubOrchestrator(childName, "input", "child-1", options, String.class); + ctx.callChildWorkflow(childName, "input", "child-1", options, String.class); }; TaskOrchestrationExecutor executor = createExecutor(orchestratorName, orchestration); @@ -264,7 +272,7 @@ void callSubOrchestrator_withoutScope_doesNotSetHistoryPropagationScope() { final String childName = "ChildOrchestrator"; TaskOrchestration orchestration = ctx -> { - ctx.callSubOrchestrator(childName, "input", "child-1", null, String.class); + ctx.callChildWorkflow(childName, "input", "child-1", null, String.class); }; TaskOrchestrationExecutor executor = createExecutor(orchestratorName, orchestration); @@ -618,38 +626,25 @@ void historyPropagationScope_toProto_convertsCorrectly() { HistoryPropagationScope.LINEAGE.toProto()); } - @Test - void historyPropagationScope_fromProto_convertsCorrectly() { - assertEquals(HistoryPropagationScope.NONE, - HistoryPropagationScope.fromProto( - Orchestration.HistoryPropagationScope.HISTORY_PROPAGATION_SCOPE_NONE)); - assertEquals(HistoryPropagationScope.OWN_HISTORY, - HistoryPropagationScope.fromProto( - Orchestration.HistoryPropagationScope.HISTORY_PROPAGATION_SCOPE_OWN_HISTORY)); - assertEquals(HistoryPropagationScope.LINEAGE, - HistoryPropagationScope.fromProto( - Orchestration.HistoryPropagationScope.HISTORY_PROPAGATION_SCOPE_LINEAGE)); - } - // ================================================================================== - // Tests for TaskOptions builder with historyPropagationScope + // Tests for WorkflowTaskOptions builder with historyPropagationScope // ================================================================================== @Test void taskOptions_builder_setsHistoryPropagationScope() { - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.LINEAGE) .appID("myApp") .build(); assertEquals(HistoryPropagationScope.LINEAGE, options.getHistoryPropagationScope()); assertTrue(options.hasHistoryPropagationScope()); - assertEquals("myApp", options.getAppID()); + assertEquals("myApp", options.getAppId()); } @Test void taskOptions_builder_withoutScope_hasNoHistoryPropagationScope() { - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .appID("myApp") .build(); @@ -659,7 +654,7 @@ void taskOptions_builder_withoutScope_hasNoHistoryPropagationScope() { @Test void taskOptions_noneScope_isNotConsideredSet() { - TaskOptions options = TaskOptions.builder() + WorkflowTaskOptions options = WorkflowTaskOptions.builder() .historyPropagationScope(HistoryPropagationScope.NONE) .build(); diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/TimerOriginTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/TimerOriginTest.java similarity index 95% rename from durabletask-client/src/test/java/io/dapr/durabletask/TimerOriginTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/internal/TimerOriginTest.java index 4225d31df6..5d78aa8ed3 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/TimerOriginTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/TimerOriginTest.java @@ -11,15 +11,21 @@ limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactories; -import io.dapr.durabletask.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import io.dapr.workflows.task.TaskOrchestration; +import io.dapr.workflows.task.exception.TaskCanceledException; +import io.dapr.workflows.task.exception.TaskFailedException; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.JacksonDataConverter; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -287,8 +293,8 @@ void test2_finiteTimeoutWaitForExternalEventSetsExternalEventOrigin() { @Test void test3_activityRetryTimerSetsActivityRetryOrigin() { - RetryPolicy policy = new RetryPolicy(2, Duration.ofSeconds(1)); - TaskOptions options = TaskOptions.withRetryPolicy(policy); + WorkflowTaskRetryPolicy policy = new WorkflowTaskRetryPolicy(2, Duration.ofSeconds(1)); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryPolicy(policy); TaskOrchestration orchestration = ctx -> { try { @@ -331,8 +337,8 @@ void test3_activityRetryTimerSetsActivityRetryOrigin() { @Test void test4_activityRetryTaskExecutionIdStable() { - RetryPolicy policy = new RetryPolicy(3, Duration.ofSeconds(1)); - TaskOptions options = TaskOptions.withRetryPolicy(policy); + WorkflowTaskRetryPolicy policy = new WorkflowTaskRetryPolicy(3, Duration.ofSeconds(1)); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryPolicy(policy); TaskOrchestration orchestration = ctx -> { try { @@ -433,13 +439,13 @@ private static HistoryEvents.HistoryEvent childWorkflowInstanceFailed(int taskSc @Test void test5_childWorkflowRetryTimerSetsChildWorkflowRetryOrigin() { - RetryPolicy policy = new RetryPolicy(2, Duration.ofSeconds(1)); - TaskOptions options = TaskOptions.withRetryPolicy(policy); + WorkflowTaskRetryPolicy policy = new WorkflowTaskRetryPolicy(2, Duration.ofSeconds(1)); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryPolicy(policy); String childInstanceId = "child-1"; TaskOrchestration orchestration = ctx -> { try { - ctx.callSubOrchestrator("Child", null, childInstanceId, options, String.class).await(); + ctx.callChildWorkflow("Child", null, childInstanceId, options, String.class).await(); } catch (TaskFailedException e) { // swallow } @@ -467,13 +473,13 @@ void test5_childWorkflowRetryTimerSetsChildWorkflowRetryOrigin() { @Test void test6_childWorkflowRetryInstanceIdStaysOnFirstChild() { - RetryPolicy policy = new RetryPolicy(3, Duration.ofSeconds(1)); - TaskOptions options = TaskOptions.withRetryPolicy(policy); + WorkflowTaskRetryPolicy policy = new WorkflowTaskRetryPolicy(3, Duration.ofSeconds(1)); + WorkflowTaskOptions options = WorkflowTaskOptions.withRetryPolicy(policy); String firstChildInstanceId = "child-1"; TaskOrchestration orchestration = ctx -> { try { - ctx.callSubOrchestrator("Child", null, firstChildInstanceId, options, String.class).await(); + ctx.callChildWorkflow("Child", null, firstChildInstanceId, options, String.class).await(); } catch (TaskFailedException e) { // swallow } @@ -625,7 +631,7 @@ void test10_prePatchReplayIndefiniteWaitThenCallActivity() { void test11_prePatchReplayIndefiniteWaitThenCallChildWorkflow() { TaskOrchestration orchestration = ctx -> { ctx.waitForExternalEvent("myEvent", Duration.ofSeconds(-1), String.class).await(); - ctx.callSubOrchestrator("Child", null, "child-1", null, String.class).await(); + ctx.callChildWorkflow("Child", null, "child-1", null, String.class).await(); }; TaskOrchestrationExecutor executor = createExecutor("Orch11", orchestration); diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/WorkflowContextLoggerTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/WorkflowContextLoggerTest.java new file mode 100644 index 0000000000..327feefb0f --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/WorkflowContextLoggerTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.workflows.task.internal; + +import com.google.protobuf.StringValue; +import com.google.protobuf.Timestamp; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents; +import io.dapr.durabletask.implementation.protobuf.Orchestration; +import io.dapr.workflows.task.TaskOrchestration; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactories; +import io.dapr.workflows.task.orchestration.TaskOrchestrationFactory; +import io.dapr.workflows.task.serialization.JacksonDataConverter; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.helpers.NOPLogger; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; + +/** + * Pins the replay-silencing contract of {@code WorkflowContext.getLogger()}. + * + *

    This behaviour used to live in the DefaultWorkflowContext adapter and was covered by + * DefaultWorkflowContextTest. Folding the durable task client in moved it verbatim onto the + * executor's context implementation, and deleting the adapter took its test with it. The + * implementation is a private inner class, so it is exercised here by driving the executor: + * events supplied as PAST history replay, events supplied as NEW do not. + * + *

    The assertion is on the branch decision rather than on the returned logger's behaviour. This + * module has no SLF4J provider on its test classpath, so {@code LoggerFactory.getLogger} returns + * the same NOP singleton the replay branch returns — comparing instances would pass even if the + * replay branch were deleted. Statically mocking the factory pins the real contract: it must not + * be consulted at all while replaying. + */ +public class WorkflowContextLoggerTest { + + private static final Duration MAX_TIMER_INTERVAL = Duration.ofDays(3); + private static final String ORCHESTRATOR = "LoggerProbeOrchestrator"; + + // The executor's own logger is java.util.logging; the context's getLogger() returns slf4j. + private final java.util.logging.Logger executorLogger = + java.util.logging.Logger.getLogger(WorkflowContextLoggerTest.class.getName()); + + @Test + public void loggerIsSilencedWhileReplayingAndLiveOtherwise() { + Logger realLogger = mock(Logger.class); + + try (MockedStatic factory = mockStatic(LoggerFactory.class)) { + factory.when(() -> LoggerFactory.getLogger(anyString())).thenReturn(realLogger); + + AtomicReference captured = new AtomicReference<>(); + AtomicReference replaying = new AtomicReference<>(); + TaskOrchestration orchestration = ctx -> { + captured.set(ctx.getLogger()); + replaying.set(ctx.isReplaying()); + }; + + // Replaying: the body runs while the executor walks PAST history. + execute(orchestration, new ArrayList<>(List.of(orchestratorStarted(), executionStarted())), + List.of(orchestratorCompleted())); + + assertTrue(Boolean.TRUE.equals(replaying.get()), "sanity: this run must be a replay"); + assertSame(NOPLogger.NOP_LOGGER, captured.get(), + "a replaying context must return the no-op logger so workflow logs are emitted once"); + factory.verify(() -> LoggerFactory.getLogger(anyString()), never()); + + // Not replaying: with no past history the executor is done replaying before the first event. + execute(orchestration, new ArrayList<>(), + List.of(orchestratorStarted(), executionStarted(), orchestratorCompleted())); + + assertTrue(Boolean.FALSE.equals(replaying.get()), "sanity: this run must not be a replay"); + assertSame(realLogger, captured.get(), "a live context must return a real logger"); + factory.verify(() -> LoggerFactory.getLogger(ORCHESTRATOR)); + } + } + + private void execute(TaskOrchestration orchestration, + List pastEvents, + List newEvents) { + TaskOrchestrationFactories factories = new TaskOrchestrationFactories(); + factories.addOrchestration(new TaskOrchestrationFactory() { + @Override + public String getName() { + return ORCHESTRATOR; + } + + @Override + public TaskOrchestration create() { + return orchestration; + } + + @Override + public String getVersionName() { + return null; + } + + @Override + public Boolean isLatestVersion() { + return false; + } + }); + + new TaskOrchestrationExecutor(factories, new JacksonDataConverter(), MAX_TIMER_INTERVAL, executorLogger, null) + .execute(pastEvents, newEvents); + } + + private static HistoryEvents.HistoryEvent orchestratorStarted() { + return HistoryEvents.HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.newBuilder().setSeconds(1000).build()) + .setWorkflowStarted(HistoryEvents.WorkflowStartedEvent.newBuilder().build()) + .build(); + } + + private static HistoryEvents.HistoryEvent executionStarted() { + return HistoryEvents.HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.newBuilder().setSeconds(1000).build()) + .setExecutionStarted(HistoryEvents.ExecutionStartedEvent.newBuilder() + .setName(ORCHESTRATOR) + .setWorkflowInstance( + Orchestration.WorkflowInstance.newBuilder().setInstanceId("instance-1").build()) + .setInput(StringValue.of("\"hello\"")) + .build()) + .build(); + } + + private static HistoryEvents.HistoryEvent orchestratorCompleted() { + return HistoryEvents.HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.newBuilder().setSeconds(1000).build()) + .setWorkflowCompleted(HistoryEvents.WorkflowCompletedEvent.newBuilder().build()) + .build(); + } +} diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/WorkflowHistoryCacheTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/WorkflowHistoryCacheTest.java similarity index 99% rename from durabletask-client/src/test/java/io/dapr/durabletask/WorkflowHistoryCacheTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/internal/WorkflowHistoryCacheTest.java index da9904cec5..f486d89766 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/WorkflowHistoryCacheTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/WorkflowHistoryCacheTest.java @@ -11,7 +11,7 @@ * limitations under the License. */ -package io.dapr.durabletask; +package io.dapr.workflows.task.internal; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import org.junit.jupiter.api.Test; diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/runner/OrchestratorRunnerHistoryTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/runner/OrchestratorRunnerHistoryTest.java similarity index 97% rename from durabletask-client/src/test/java/io/dapr/durabletask/runner/OrchestratorRunnerHistoryTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/task/internal/runner/OrchestratorRunnerHistoryTest.java index 915a8ef8cd..c67fa935f8 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/runner/OrchestratorRunnerHistoryTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/task/internal/runner/OrchestratorRunnerHistoryTest.java @@ -11,13 +11,13 @@ * limitations under the License. */ -package io.dapr.durabletask.runner; +package io.dapr.workflows.task.internal.runner; -import io.dapr.durabletask.TaskOrchestratorResult; -import io.dapr.durabletask.WorkflowHistoryCache; import io.dapr.durabletask.implementation.protobuf.HistoryEvents; import io.dapr.durabletask.implementation.protobuf.OrchestratorActions; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; +import io.dapr.workflows.task.internal.TaskOrchestratorResult; +import io.dapr.workflows.task.internal.WorkflowHistoryCache; import org.junit.jupiter.api.Test; import java.util.ArrayList; diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java index 0e124d4a41..b716bcb803 100644 --- a/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java +++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java @@ -13,8 +13,8 @@ package io.dapr.springboot.examples.orchestrator; -import io.dapr.durabletask.TaskCanceledException; -import io.dapr.durabletask.TaskFailedException; +import io.dapr.workflows.task.exception.TaskCanceledException; +import io.dapr.workflows.task.exception.TaskFailedException; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java index 3921521706..6ec75eef8b 100644 --- a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.springboot.examples.wfp.fanoutin; -import io.dapr.durabletask.Task; +import io.dapr.workflows.task.Task; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import org.springframework.stereotype.Component; diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudCheckActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudCheckActivity.java index 736191465b..4dfdbc8c67 100644 --- a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudCheckActivity.java +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudCheckActivity.java @@ -13,7 +13,7 @@ package io.dapr.springboot.examples.wfp.historypropagation; -import io.dapr.durabletask.PropagatedHistory; +import io.dapr.workflows.task.history.PropagatedHistory; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.WorkflowActivityContext; import org.springframework.stereotype.Component; diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudDetectionWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudDetectionWorkflow.java index 2d5d94d474..4066c3f6a3 100644 --- a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudDetectionWorkflow.java +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/FraudDetectionWorkflow.java @@ -13,9 +13,9 @@ package io.dapr.springboot.examples.wfp.historypropagation; -import io.dapr.durabletask.ActivityResult; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.WorkflowResult; +import io.dapr.workflows.task.history.ActivityResult; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.WorkflowResult; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/ProcessPaymentWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/ProcessPaymentWorkflow.java index 8a2e640f20..8718ddbbab 100644 --- a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/ProcessPaymentWorkflow.java +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/ProcessPaymentWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.springboot.examples.wfp.historypropagation; -import io.dapr.durabletask.HistoryPropagationScope; +import io.dapr.workflows.task.history.HistoryPropagationScope; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/SettlePaymentActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/SettlePaymentActivity.java index e0a0741523..fbbe92341e 100644 --- a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/SettlePaymentActivity.java +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/historypropagation/SettlePaymentActivity.java @@ -13,8 +13,8 @@ package io.dapr.springboot.examples.wfp.historypropagation; -import io.dapr.durabletask.PropagatedHistory; -import io.dapr.durabletask.WorkflowResult; +import io.dapr.workflows.task.history.PropagatedHistory; +import io.dapr.workflows.task.history.WorkflowResult; import io.dapr.workflows.WorkflowActivity; import io.dapr.workflows.WorkflowActivityContext; import org.springframework.stereotype.Component; diff --git a/spring-boot-sdk-tests/src/test/java/io/dapr/it/springboot/testcontainers/workflows/TestExecutionKeysWorkflow.java b/spring-boot-sdk-tests/src/test/java/io/dapr/it/springboot/testcontainers/workflows/TestExecutionKeysWorkflow.java index 6c5537988a..0f487c6b96 100644 --- a/spring-boot-sdk-tests/src/test/java/io/dapr/it/springboot/testcontainers/workflows/TestExecutionKeysWorkflow.java +++ b/spring-boot-sdk-tests/src/test/java/io/dapr/it/springboot/testcontainers/workflows/TestExecutionKeysWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.it.springboot.testcontainers.workflows; -import io.dapr.durabletask.Task; +import io.dapr.workflows.task.Task; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; import io.dapr.workflows.WorkflowTaskOptions; From 265bf9a3b724fc30b5bdfc5c3a497c061a48562a Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Tue, 1 Sep 2026 11:26:49 +0200 Subject: [PATCH 2/6] docs: add a migration guide for the unified workflows module Folding durabletask-client into dapr-sdk-workflows is a breaking change, so document it where an upgrader will look: MIGRATION.md carries the full old-to-new type mapping, the renames, the removed API, the behaviour changes that are invisible to the compiler, and a tested rewrite script for the mechanical import changes. The section on workflows that are already running is the important one. The error type is persisted into workflow history, so a workflow started before the upgrade carries the old exception names; the guide explains what the SDK maps for you and what it does not. Link the guide from README.md and SUPPORT.md, drop durabletask-client from the module lists in both, and record that Java 21 is recommended for workflows now that the default executor uses virtual threads there. Signed-off-by: Javier Aliaga --- MIGRATION.md | 278 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 +- SUPPORT.md | 31 +++++- 3 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 MIGRATION.md diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000000..03047dd40b --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,278 @@ +# Migrating workflows to the unified `dapr-sdk-workflows` + +The durable task client has been folded into the workflows SDK. `io.dapr:durabletask-client` no +longer exists as a separate artifact, and everything it contained now ships inside +`io.dapr:dapr-sdk-workflows` under `io.dapr.workflows.*`. + +This is a **breaking change**. Most of it is mechanical: change a dependency, change some imports. +A smaller part needs real attention, and one item affects workflows that are already running when +you upgrade. Read [Behaviour changes](#4-behaviour-changes) and [Workflows already +running](#5-workflows-already-running) even if the rest applies cleanly. + +--- + +## 1. Dependencies + +Remove `durabletask-client`. It is gone, and its classes are in `dapr-sdk-workflows`. + +```xml + + + io.dapr + dapr-sdk-workflows + + + io.dapr + durabletask-client + + + + + io.dapr + dapr-sdk-workflows + +``` + +If you import `dapr-sdk-bom`, nothing changes beyond removing any explicit `durabletask-client` +dependency: the BOM no longer manages that artifact. + +> **Do not keep the old `durabletask-client` on the classpath alongside the new +> `dapr-sdk-workflows`.** Both jars contain the generated protobuf classes under +> `io.dapr.durabletask.implementation.protobuf`, so the two will collide. Which copy wins is +> whichever the classloader reaches first. + +--- + +## 2. What most codebases actually have to change + +These are the types application code normally names. If you only use these, the migration is an +import rewrite. + +| Was | Now | +|---|---| +| `io.dapr.durabletask.Task` | `io.dapr.workflows.task.Task` | +| `io.dapr.durabletask.TaskFailedException` | `io.dapr.workflows.task.exception.TaskFailedException` | +| `io.dapr.durabletask.TaskCanceledException` | `io.dapr.workflows.task.exception.TaskCanceledException` | +| `io.dapr.durabletask.CompositeTaskFailedException` | `io.dapr.workflows.task.exception.CompositeTaskFailedException` | +| `io.dapr.durabletask.interruption.OrchestratorBlockedException` | `io.dapr.workflows.task.interruption.OrchestratorBlockedException` | +| `io.dapr.durabletask.interruption.ContinueAsNewInterruption` | `io.dapr.workflows.task.interruption.ContinueAsNewInterruption` | +| `io.dapr.durabletask.PropagatedHistory` | `io.dapr.workflows.task.history.PropagatedHistory` | +| `io.dapr.durabletask.HistoryPropagationScope` | `io.dapr.workflows.task.history.HistoryPropagationScope` | +| `io.dapr.durabletask.WorkflowResult` | `io.dapr.workflows.task.history.WorkflowResult` | +| `io.dapr.durabletask.ActivityResult` | `io.dapr.workflows.task.history.ActivityResult` | +| `io.dapr.durabletask.ChildWorkflowResult` | `io.dapr.workflows.task.history.ChildWorkflowResult` | + +**Unchanged.** `Workflow`, `WorkflowActivity`, `WorkflowActivityContext`, `WorkflowState`, +`WorkflowRuntime`, `WorkflowRuntimeBuilder` and `DaprWorkflowClient` keep their names and packages. +If your workflow code only touches those plus `WorkflowContext`, it compiles untouched apart from +the imports above. + +--- + +## 3. Renamed types + +Seven types were renamed. Each had a near-duplicate twin on the workflows side, and unifying the two +artifacts removed the duplication — that was the point of the merge, and it is not something an +alias can paper over. + +| Was | Now | Note | +|---|---|---| +| `io.dapr.durabletask.TaskOrchestrationContext` | `io.dapr.workflows.WorkflowContext` | `callSubOrchestrator` → `callChildWorkflow`; `getIsReplaying` → `isReplaying` | +| `io.dapr.durabletask.TaskOptions` | `io.dapr.workflows.WorkflowTaskOptions` | keeps both the builder and the constructors | +| `io.dapr.durabletask.RetryPolicy` | `io.dapr.workflows.WorkflowTaskRetryPolicy` | see [Behaviour changes](#4-behaviour-changes) | +| `io.dapr.durabletask.RetryHandler` | `io.dapr.workflows.WorkflowTaskRetryHandler` | | +| `io.dapr.durabletask.RetryContext` | `io.dapr.workflows.WorkflowTaskRetryContext` | `getOrchestrationContext()` → `getWorkflowContext()` | +| `io.dapr.durabletask.FailureDetails` | `io.dapr.workflows.task.exception.WorkflowFailureDetails` | was an interface on the workflows side, is now a final class | +| `io.dapr.durabletask.OrchestrationRuntimeStatus` | `io.dapr.workflows.client.WorkflowRuntimeStatus` | gains `STALLED` | + +### Removed + +| Removed | Use instead | +|---|---| +| `DaprWorkflowClient.getInstanceState(…)` | `getWorkflowState(…)` | +| `DaprWorkflowClient.waitForInstanceStart(…)` | `waitForWorkflowStart(…)` | +| `DaprWorkflowClient.waitForInstanceCompletion(…)` | `waitForWorkflowCompletion(…)` | +| `DaprWorkflowClient.purgeInstance(…)` | `purgeWorkflow(…)` | +| `io.dapr.workflows.client.WorkflowInstanceStatus` | `io.dapr.workflows.client.WorkflowState` | +| `WorkflowRuntimeStatusConverter` | folded into `WorkflowRuntimeStatus` | +| `DefaultWorkflowContext`, `DefaultWorkflowFailureDetails`, `DefaultWorkflowInstanceStatus` | internal adapters, no longer needed | + +All four client methods were already `@Deprecated(forRemoval = true)`. + +`WorkflowState` is the same shape as `WorkflowInstanceStatus` with one difference: the id accessor +is **`getWorkflowId()`**, not `getInstanceId()`. + +### Types that became internal + +These were public but are implementation detail. They now live under `io.dapr.workflows.task.internal` +and are not supported API: `Helpers`, `TaskOrchestrationExecutor`, `TaskActivityExecutor`, +`TaskOrchestratorResult`, `UuidGenerator`, and the runners (`DurableRunner`, `ActivityRunner`, +`OrchestratorRunner`, `OrchestrationRunner`). If you depend on any of these, open an issue describing +what you needed them for. + +--- + +## 4. Behaviour changes + +Three changes are not visible to the compiler. + +**Retry policy defaults are now explicit.** `WorkflowTaskRetryPolicy.getMaxRetryInterval()` and +`getRetryTimeout()` return `Duration.ZERO` when unset, where the workflows-side builder previously +returned `null`. Code doing `if (policy.getMaxRetryInterval() != null)` will now always take the +non-null branch. The value the runtime sees is unchanged — an adapter used to perform this +`null` → `ZERO` conversion, and it now happens in the constructor. + +**`WorkflowContext` gained methods.** `getAppId()`, `sendEvent(...)`, `clearCustomStatus()` and two +`createTimer(String, …)` overloads are now on the interface. Calling code is unaffected. **Anything +that *implements* `WorkflowContext` must implement them** — mocks and test doubles included. + +**Error type strings changed.** `WorkflowFailureDetails.getErrorType()` returns the exception's fully +qualified name, so the strings moved with the classes. If you compare error types as strings, update +the literals — or better, compare against `SomeException.class.getName()`. + +--- + +## 5. Workflows already running + +This is the one that bites silently. + +The error type is **persisted into workflow history**. A workflow that started before the upgrade and +resumes afterwards carries `io.dapr.durabletask.TaskFailedException` in its history, while the class +is now `io.dapr.workflows.task.exception.TaskFailedException`. + +`WorkflowFailureDetails.isCausedBy(...)` resolves the persisted name reflectively. To keep those +instances working, the SDK maps the exception names that shipped under `io.dapr.durabletask` onto +their current types, so `isCausedBy` answers correctly across the upgrade: + +``` +TaskFailedException TaskCanceledException +CompositeTaskFailedException NonDeterministicOrchestratorException +PropagatedHistoryException orchestration.exception.VersionNotRegisteredException +interruption.OrchestratorBlockedException +interruption.ContinueAsNewInterruption +DataConverter$DataConverterException +``` + +**What this covers:** `isCausedBy` on in-flight workflows, including compensation logic that branches +on the failure type. If your workflows only ever see SDK exception types, no action is needed. + +Note that an error type the SDK cannot resolve answers `false` to *every* query — including broad +ones like `isCausedBy(RuntimeException.class)` — not just to an exact-type check. If you have +persisted history whose error type is neither an SDK exception nor a class on your own classpath, +expect `false` and a `WARN` line rather than a match. + +**What it does not cover:** your own comparisons. If you do +`"io.dapr.durabletask.TaskFailedException".equals(details.getErrorType())`, that is now false for new +failures and true for old ones. Replace such comparisons with `isCausedBy(...)`. + +An error type that cannot be resolved at all now logs at `WARN` and returns `false`, rather than +returning `false` silently. + +Replay determinism is unaffected: the rethrown exception type is chosen by code, not by the persisted +string, so history written before the upgrade replays normally. + +--- + +## 6. Java version + +`dapr-sdk-workflows` is still compiled for **Java 17** and runs on Java 17 or later. **Java 21 or +later is now recommended.** + +On Java 21+ the runtime's default executor is a virtual-thread-per-task executor; on 17 through 20 it +is a cached thread pool, exactly as before. You can always supply your own with +`WorkflowRuntimeBuilder.withExecutorService(...)` — an executor you supply is never shut down by the +runtime. + +Virtual threads help **activities**, which run your code and typically block on I/O. Workflow code +itself is replay-based and does not block, so it sees little benefit. See `SUPPORT.md` for the caveats +around pinning and unbounded concurrency. + +--- + +## 7. Mechanical rewrite + +Most of section 2 can be applied with a script. Review the diff afterwards; this only rewrites +imports and fully qualified references, and it does not handle the renames in section 3. + +```bash +#!/usr/bin/env bash +# Rewrites v1 durable task imports to their unified homes. +# Usage: ./rewrite.sh [source-dir] (defaults to src). Works on bash 3.2+. +set -eu + +ROOT="${1:-src}" + +# " ", one per line. +MAP=' +Task task +TaskActivity task +TaskActivityContext task +TaskActivityFactory task +TaskOrchestration task +OrchestratorFunction task +TaskFailedException task.exception +TaskCanceledException task.exception +CompositeTaskFailedException task.exception +NonDeterministicOrchestratorException task.exception +PropagatedHistory task.history +PropagatedHistoryException task.history +HistoryPropagationScope task.history +WorkflowResult task.history +ActivityResult task.history +ChildWorkflowResult task.history +DurableTaskClient task.client +DurableTaskGrpcClient task.client +DurableTaskGrpcClientBuilder task.client +OrchestrationMetadata task.client +NewOrchestrationInstanceOptions task.client +PurgeInstanceCriteria task.client +PurgeResult task.client +DurableTaskGrpcWorker task.worker +DurableTaskGrpcWorkerBuilder task.worker +DataConverter task.serialization +JacksonDataConverter task.serialization +' + +count=0 +while IFS= read -r file; do + [ -n "$file" ] || continue + count=$((count + 1)) + + # Subpackages that keep their leaf name. Do these first: the type loop below + # would otherwise rewrite the leaf and leave a stale parent package behind. + perl -0pi -e ' + s/\bio\.dapr\.durabletask\.orchestration\.exception\.VersionNotRegisteredException\b/io.dapr.workflows.task.exception.VersionNotRegisteredException/g; + s/\bio\.dapr\.durabletask\.interruption\./io.dapr.workflows.task.interruption./g; + s/\bio\.dapr\.durabletask\.orchestration\./io.dapr.workflows.task.orchestration./g; + ' "$file" + + # Longest names first, so PropagatedHistory does not clip PropagatedHistoryException. + echo "$MAP" | grep -v '^$' | awk '{ print length($1), $1, $2 }' | sort -rn | + while read -r _len type pkg; do + perl -0pi -e "s/\\bio\\.dapr\\.durabletask\\.${type}\\b/io.dapr.workflows.${pkg}.${type}/g" "$file" + done +done < Upgrading workflow code from a release that still had `durabletask-client`? See +> [MIGRATION.md](MIGRATION.md). + +- **`io.dapr:dapr-sdk-bom`** — core SDK modules (`dapr-sdk`, `dapr-sdk-actors`, `dapr-sdk-workflows`, `dapr-sdk-autogen`, `testcontainers-dapr`) plus security-patched transitive dependencies (Netty, Jackson, commons-compress, commons-codec). - **`io.dapr.spring:dapr-spring-bom`** — Spring-specific modules (`dapr-sdk-springboot`, `dapr-spring-*`). Imports `dapr-sdk-bom` transitively, so Spring users only need this single BOM. Pick the one that matches your project. Importing a BOM ensures you inherit security fixes for transitive dependencies like the Netty CVEs. diff --git a/SUPPORT.md b/SUPPORT.md index eeb585d6c6..dd759f485e 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -12,7 +12,36 @@ at a given version pins the whole stack, core and Spring, to that single version | **1.19.x+** | v1.18 | 4.0.x | 17+ | Active — new features and fixes | | **1.18.x** | v1.18 | 3.5.x | 17+ | Maintenance — best-effort security/critical fixes | -The **core** SDK modules (`dapr-sdk`, `dapr-sdk-actors`, `dapr-sdk-workflows`, `durabletask-client`) +### Migrating to the unified workflows module + +`durabletask-client` has been folded into `dapr-sdk-workflows`, which is a breaking change for +workflow code. See [MIGRATION.md](MIGRATION.md) for the type mapping, the removed API, and what it +means for workflows that are already running when you upgrade. + +### Java 21 is recommended for workflows + +`dapr-sdk-workflows` is compiled for Java 17 and runs on Java 17 or later. **Java 21 or later is +recommended.** + +The workflow runtime executes workflows and activities on an `ExecutorService`. On Java 21+ the +default is a virtual-thread-per-task executor; on Java 17 through 20 it is a cached thread pool. +You can always supply your own with `WorkflowRuntimeBuilder.withExecutorService(...)`, and an +executor you supply is never shut down by the runtime. + +Note for Spring Boot users: `spring.threads.virtual.enabled=true` does **not** currently reach the +workflow runtime. It switches Spring's own executors, but the Dapr auto-configuration does not hand +the application's task executor to `WorkflowRuntimeBuilder`, so workflow and activity execution is +unaffected by it. Define your own `WorkflowRuntimeBuilder` bean and call `withExecutorService(...)` +if you want workflows on virtual threads. + +Virtual threads help **activities**, which run your code and typically block on I/O. Workflow code +itself is replay-based and does not block, so it sees little benefit. The SDK holds no monitor on +the work-item dispatch path and will not pin a carrier thread, but your own activity code can: a +`synchronized` block around a blocking call will pin on Java 21 through 23. Note also that a +virtual-thread-per-task executor is unbounded — if your activities hit a bounded resource such as +a JDBC pool or a rate-limited API, size that resource or supply a bounded executor instead. + +The **core** SDK modules (`dapr-sdk`, `dapr-sdk-actors`, `dapr-sdk-workflows`) are framework-agnostic and do not depend on Spring Boot — the Spring Boot column applies only to the Spring integration modules (`dapr-sdk-springboot`, `dapr-spring-*`). Core and Spring modules always share the same SDK version (the Spring BOM imports `dapr-sdk-bom` at its own version), so they never From 372c77007047bad6262662c0769d17e8833d61f0 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Tue, 1 Sep 2026 12:35:48 +0200 Subject: [PATCH 3/6] refactor: narrow the workflow exception constructors back to package-private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splitting the folded durable task types into subpackages moved the executor away from the exceptions it constructs, and the visibility was widened to compensate. It was widened further than the split required: every constructor on both classes became public where only the two the executor actually calls needed to be. Restore the rest to package-private, matching what they were before the merge. The two that stay public are the ones the executor constructs from io.dapr.workflows.task.internal, and their javadoc now says so accurately — the previous note claimed a sibling-package caller for members that have none. No test used any of these constructors, and no behaviour changes. Signed-off-by: Javier Aliaga --- .../CompositeTaskFailedException.java | 21 +++++++++++++------ .../task/exception/TaskFailedException.java | 15 +++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/CompositeTaskFailedException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/CompositeTaskFailedException.java index f26ef8f39e..08c70d61d4 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/CompositeTaskFailedException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/CompositeTaskFailedException.java @@ -28,31 +28,40 @@ public class CompositeTaskFailedException extends RuntimeException { private final List exceptions; - public CompositeTaskFailedException() { + CompositeTaskFailedException() { this.exceptions = new ArrayList<>(); } - public CompositeTaskFailedException(List exceptions) { + CompositeTaskFailedException(List exceptions) { this.exceptions = exceptions; } + /** + * Creates a composite failure from the tasks that failed. + * + *

    Public because the workflow executor constructs it from another package. Not intended for + * application code. + * + * @param message the exception message. + * @param exceptions the individual task failures. + */ public CompositeTaskFailedException(String message, List exceptions) { super(message); this.exceptions = exceptions; } - public CompositeTaskFailedException(String message, Throwable cause, List exceptions) { + CompositeTaskFailedException(String message, Throwable cause, List exceptions) { super(message, cause); this.exceptions = exceptions; } - public CompositeTaskFailedException(Throwable cause, List exceptions) { + CompositeTaskFailedException(Throwable cause, List exceptions) { super(cause); this.exceptions = exceptions; } - public CompositeTaskFailedException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace, List exceptions) { + CompositeTaskFailedException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace, List exceptions) { super(message, cause, enableSuppression, writableStackTrace); this.exceptions = exceptions; } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskFailedException.java b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskFailedException.java index a26f1385a9..25a662ee6c 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskFailedException.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/task/exception/TaskFailedException.java @@ -27,22 +27,21 @@ public class TaskFailedException extends RuntimeException { private final String taskName; private final int taskId; - public TaskFailedException(String taskName, int taskId, WorkflowFailureDetails details) { - this(getExceptionMessage(taskName, taskId, details), taskName, taskId, details); - } - /** - * Creates a task failure with an explicit message. + * Creates a task failure with a message derived from the task and failure details. * - *

    Public so the workflow executor in a sibling package can reach it; not intended for + *

    Public because the workflow executor constructs it from another package. Not intended for * application code. * - * @param message the exception message. * @param taskName the name of the failed task. * @param taskId the id of the failed task. * @param details details of the failure. */ - public TaskFailedException(String message, String taskName, int taskId, WorkflowFailureDetails details) { + public TaskFailedException(String taskName, int taskId, WorkflowFailureDetails details) { + this(getExceptionMessage(taskName, taskId, details), taskName, taskId, details); + } + + TaskFailedException(String message, String taskName, int taskId, WorkflowFailureDetails details) { super(message); this.taskName = taskName; this.taskId = taskId; From bc3c79362faae0719a69c074e744c4d4604fa92f Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Tue, 1 Sep 2026 12:35:58 +0200 Subject: [PATCH 4/6] feat: allow opting out of virtual threads for the workflow runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime defaults to a virtual-thread-per-task executor on Java 21+, which existing deployments get without changing any code. That is the right default — activities run user code and typically block on I/O — but it is not right for everyone: activity code that holds a monitor across a blocking call pins a carrier thread on Java 21 through 23. Add dapr.workflows.virtual.threads.enabled (env DAPR_WORKFLOWS_VIRTUAL_THREADS_ENABLED), default true, following the existing workflow property convention. Setting it false keeps the cached thread pool used on earlier runtimes. It only affects the executor the runtime creates for itself, so it has no bearing on Java 17 through 20 or on a runtime handed an executor via withExecutorService. Document the opt-out in SUPPORT.md and MIGRATION.md, and correct a claim in SUPPORT.md that spring.threads.virtual.enabled reaches the workflow runtime: it switches Spring's own executors, but the Dapr auto-configuration does not pass the application's task executor to WorkflowRuntimeBuilder, so workflow execution is unaffected by it. Signed-off-by: Javier Aliaga --- MIGRATION.md | 5 + SUPPORT.md | 5 + .../internal/DefaultExecutorService.java | 17 +- .../runtime/WorkflowRuntimeBuilder.java | 2 +- .../internal/DefaultExecutorServiceTest.java | 45 +- .../main/java/io/dapr/config/Properties.java | 693 +++++++++--------- 6 files changed, 423 insertions(+), 344 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 03047dd40b..a8745b3f69 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -183,6 +183,11 @@ is a cached thread pool, exactly as before. You can always supply your own with `WorkflowRuntimeBuilder.withExecutorService(...)` — an executor you supply is never shut down by the runtime. +Virtual threads are on by default there. To opt out and keep the cached thread pool, set +`dapr.workflows.virtual.threads.enabled=false` (or `DAPR_WORKFLOWS_VIRTUAL_THREADS_ENABLED=false`). +The setting only affects the executor the runtime creates for itself; it has no effect on Java 17 +through 20, or on a runtime given an executor via `withExecutorService(...)`. + Virtual threads help **activities**, which run your code and typically block on I/O. Workflow code itself is replay-based and does not block, so it sees little benefit. See `SUPPORT.md` for the caveats around pinning and unbounded concurrency. diff --git a/SUPPORT.md b/SUPPORT.md index dd759f485e..70afe3ac1f 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -28,6 +28,11 @@ default is a virtual-thread-per-task executor; on Java 17 through 20 it is a cac You can always supply your own with `WorkflowRuntimeBuilder.withExecutorService(...)`, and an executor you supply is never shut down by the runtime. +Virtual threads are on by default there. To opt out and keep the cached thread pool, set +`dapr.workflows.virtual.threads.enabled=false` (or `DAPR_WORKFLOWS_VIRTUAL_THREADS_ENABLED=false`). +The setting only affects the executor the runtime creates for itself; it has no effect on Java 17 +through 20, or on a runtime given an executor via `withExecutorService(...)`. + Note for Spring Boot users: `spring.threads.virtual.enabled=true` does **not** currently reach the workflow runtime. It switches Spring's own executors, but the Dapr auto-configuration does not hand the application's task executor to `WorkflowRuntimeBuilder`, so workflow and activity execution is diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java b/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java index 3b9a7f74e4..c0e8f89bc6 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java @@ -13,6 +13,7 @@ package io.dapr.workflows.internal; +import io.dapr.config.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,10 @@ * that factory is resolved reflectively; on Java 17 through 20 a cached thread pool is used, * which is what the workflow runtime has always used. * + *

    Virtual threads are the default on Java 21+ and can be turned off with + * {@link Properties#WORKFLOWS_VIRTUAL_THREADS_ENABLED} — useful when activity code holds monitors + * across blocking calls, which pins carrier threads on Java 21 through 23. + * *

    Callers that want explicit control should pass their own executor to * {@code WorkflowRuntimeBuilder.withExecutorService(...)} instead of relying on this default. * An executor supplied that way is never shut down by the runtime. @@ -45,9 +50,17 @@ private DefaultExecutorService() { /** * Creates the default executor for the current runtime. * - * @return a virtual-thread-per-task executor on Java 21+, otherwise a cached thread pool. + * @param properties configuration used to resolve the virtual-threads opt-out. + * @return a virtual-thread-per-task executor on Java 21+ unless virtual threads are disabled, + * otherwise a cached thread pool. */ - public static ExecutorService create() { + public static ExecutorService create(Properties properties) { + if (!properties.getValue(Properties.WORKFLOWS_VIRTUAL_THREADS_ENABLED)) { + LOGGER.info("Virtual threads are disabled by configuration, " + + "using a cached thread pool for workflow and activity execution"); + return Executors.newCachedThreadPool(); + } + if (Runtime.version().feature() >= VIRTUAL_THREADS_SINCE) { try { Method factory = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java index 14c28d1b37..ac29cfbcb9 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java @@ -86,7 +86,7 @@ public WorkflowRuntime build() { synchronized (WorkflowRuntime.class) { boolean ownsExecutorService = this.executorService == null; if (ownsExecutorService) { - this.executorService = DefaultExecutorService.create(); + this.executorService = DefaultExecutorService.create(this.properties); } if (instance == null) { GrpcChannelKeepalive keepalive = null; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java index f3831ad1f2..18be6404ec 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java @@ -13,13 +13,16 @@ package io.dapr.workflows.internal; +import io.dapr.config.Properties; import org.junit.jupiter.api.Test; +import java.util.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -33,7 +36,7 @@ public class DefaultExecutorServiceTest { @Test public void createReturnsAUsableExecutor() throws Exception { - ExecutorService executorService = DefaultExecutorService.create(); + ExecutorService executorService = DefaultExecutorService.create(new Properties()); assertNotNull(executorService); AtomicBoolean ran = new AtomicBoolean(false); @@ -45,7 +48,7 @@ public void createReturnsAUsableExecutor() throws Exception { @Test public void threadKindMatchesTheRunningJavaVersion() throws Exception { - ExecutorService executorService = DefaultExecutorService.create(); + ExecutorService executorService = DefaultExecutorService.create(new Properties()); try { boolean virtual = executorService.submit(DefaultExecutorServiceTest::currentThreadIsVirtual) .get(5, TimeUnit.SECONDS); @@ -57,6 +60,44 @@ public void threadKindMatchesTheRunningJavaVersion() throws Exception { } } + /** + * Virtual threads are the default on Java 21+, but an operator can turn them off — for instance + * when activity code holds a monitor across a blocking call, which pins a carrier thread on + * Java 21 through 23. + */ + @Test + public void virtualThreadsCanBeDisabledByConfiguration() throws Exception { + Properties optedOut = new Properties( + Collections.singletonMap("dapr.workflows.virtual.threads.enabled", "false")); + + ExecutorService executorService = DefaultExecutorService.create(optedOut); + try { + boolean virtual = executorService.submit(DefaultExecutorServiceTest::currentThreadIsVirtual) + .get(5, TimeUnit.SECONDS); + + assertFalse(virtual, "the opt-out must yield platform threads even on Java 21+"); + } finally { + executorService.shutdown(); + } + } + + @Test + public void virtualThreadsAreTheDefaultWhenNotDisabled() throws Exception { + Properties explicitlyEnabled = new Properties( + Collections.singletonMap("dapr.workflows.virtual.threads.enabled", "true")); + + ExecutorService executorService = DefaultExecutorService.create(explicitlyEnabled); + try { + boolean virtual = executorService.submit(DefaultExecutorServiceTest::currentThreadIsVirtual) + .get(5, TimeUnit.SECONDS); + + assertEquals(Runtime.version().feature() >= 21, virtual, + "with virtual threads enabled the thread kind must follow the runtime version"); + } finally { + executorService.shutdown(); + } + } + /** * Calls {@code Thread.isVirtual()} reflectively, since it is absent at this module's * Java 17 compile target. diff --git a/sdk/src/main/java/io/dapr/config/Properties.java b/sdk/src/main/java/io/dapr/config/Properties.java index eb99fd2671..8047f537c1 100644 --- a/sdk/src/main/java/io/dapr/config/Properties.java +++ b/sdk/src/main/java/io/dapr/config/Properties.java @@ -1,339 +1,354 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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.dapr.config; - -import io.dapr.utils.NetworkUtils; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Global properties for Dapr's SDK, using Supplier so they are dynamically resolved. - */ -public class Properties { - - /** - * Dapr's default IP for HTTP and gRPC communication. - */ - private static final String DEFAULT_SIDECAR_IP = NetworkUtils.getHostLoopbackAddress(); - - /** - * Dapr's default HTTP port. - */ - private static final Integer DEFAULT_HTTP_PORT = 3500; - - /** - * Dapr's default gRPC port. - */ - private static final Integer DEFAULT_GRPC_PORT = 50001; - - /** - * Dapr's default max retries. - */ - private static final Integer DEFAULT_API_MAX_RETRIES = 0; - - /** - * Dapr's default timeout in seconds. - */ - private static final Duration DEFAULT_API_TIMEOUT = Duration.ofMillis(0L); - - /** - * Dapr's default String encoding: UTF-8. - */ - private static final Charset DEFAULT_STRING_CHARSET = StandardCharsets.UTF_8; - - /** - * Dapr's default timeout in seconds for HTTP client reads. - */ - private static final Integer DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECONDS = 60; - - /** - * Dapr's default maximum number of requests for HTTP client to execute concurrently. - * - *

    Above this requests queue in memory, waiting for the running calls to complete. - * Default is 64 in okhttp which is OK for most case, but for some special case - * which is slow response and high concurrency, the value should set to a little big. - */ - private static final Integer DEFAULT_HTTP_CLIENT_MAX_REQUESTS = 1024; - - /** - * Dapr's default maximum number of idle connections of HTTP connection pool. - * - *

    Attention! This is max IDLE connection, NOT max connection! - * It is also very important for high concurrency cases. - */ - private static final Integer DEFAULT_HTTP_CLIENT_MAX_IDLE_CONNECTIONS = 128; - - /** - * IP for Dapr's sidecar. - */ - public static final Property SIDECAR_IP = new StringProperty( - "dapr.sidecar.ip", - "DAPR_SIDECAR_IP", - DEFAULT_SIDECAR_IP); - - /** - * HTTP port for Dapr after checking system property and environment variable. - */ - public static final Property HTTP_PORT = new IntegerProperty( - "dapr.http.port", - "DAPR_HTTP_PORT", - DEFAULT_HTTP_PORT); - - /** - * GRPC port for Dapr after checking system property and environment variable. - */ - public static final Property GRPC_PORT = new IntegerProperty( - "dapr.grpc.port", - "DAPR_GRPC_PORT", - DEFAULT_GRPC_PORT); - - /** - * GRPC TLS cert path for Dapr after checking system property and environment variable. - */ - public static final Property GRPC_TLS_CERT_PATH = new StringProperty( - "dapr.grpc.tls.cert.path", - "DAPR_GRPC_TLS_CERT_PATH", - null); - - /** - * GRPC TLS key path for Dapr after checking system property and environment variable. - */ - public static final Property GRPC_TLS_KEY_PATH = new StringProperty( - "dapr.grpc.tls.key.path", - "DAPR_GRPC_TLS_KEY_PATH", - null); - - /** - * GRPC TLS CA cert path for Dapr after checking system property and environment variable. - * This is used for TLS connections to servers with self-signed certificates. - */ - public static final Property GRPC_TLS_CA_PATH = new StringProperty( - "dapr.grpc.tls.ca.path", - "DAPR_GRPC_TLS_CA_PATH", - null); - - /** - * Use insecure TLS mode which still uses TLS but doesn't verify certificates. - * This uses InsecureTrustManagerFactory to trust all certificates. - * This should only be used for testing or in secure environments. - */ - public static final Property GRPC_TLS_INSECURE = new BooleanProperty( - "dapr.grpc.tls.insecure", - "DAPR_GRPC_TLS_INSECURE", - false); - - /** - * GRPC endpoint for remote sidecar connectivity. - */ - public static final Property GRPC_ENDPOINT = new StringProperty( - "dapr.grpc.endpoint", - "DAPR_GRPC_ENDPOINT", - null); - - /** - * GRPC enable keep alive. - * Environment variable: DAPR_GRPC_ENABLE_KEEP_ALIVE - * System property: dapr.grpc.enable.keep.alive - * Default: false - */ - public static final Property GRPC_ENABLE_KEEP_ALIVE = new BooleanProperty( - "dapr.grpc.enable.keep.alive", - "DAPR_GRPC_ENABLE_KEEP_ALIVE", - false); - - /** - * GRPC keep alive time in seconds. - * Environment variable: DAPR_GRPC_KEEP_ALIVE_TIME_SECONDS - * System property: dapr.grpc.keep.alive.time.seconds - * Default: 10 seconds - */ - public static final Property GRPC_KEEP_ALIVE_TIME_SECONDS = new SecondsDurationProperty( - "dapr.grpc.keep.alive.time.seconds", - "DAPR_GRPC_KEEP_ALIVE_TIME_SECONDS", - Duration.ofSeconds(10)); - - /** - * GRPC keep alive timeout in seconds. - * Environment variable: DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDS - * System property: dapr.grpc.keep.alive.timeout.seconds - * Default: 5 seconds - */ - public static final Property GRPC_KEEP_ALIVE_TIMEOUT_SECONDS = new SecondsDurationProperty( - "dapr.grpc.keep.alive.timeout.seconds", - "DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDS", - Duration.ofSeconds(5)); - - /** - * GRPC keep alive without calls. - * Environment variable: DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS - * System property: dapr.grpc.keep.alive.without.calls - * Default: true - */ - public static final Property GRPC_KEEP_ALIVE_WITHOUT_CALLS = new BooleanProperty( - "dapr.grpc.keep.alive.without.calls", - "DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS", - true); - - /** - * Enables the application-level keepalive on the workflow runtime's gRPC channel. - * While the runtime is started, the SDK periodically invokes the sidecar's hello RPC - * so that intermediaries that do not treat HTTP/2 PING frames as connection activity - * (e.g. AWS ALBs) never see the connection as idle and close it. - * Environment variable: DAPR_WORKFLOWS_RUNTIME_APP_KEEP_ALIVE_ENABLED - * System property: dapr.workflows.runtime.app.keep.alive.enabled - * Default: true - */ - public static final Property WORKFLOWS_RUNTIME_APP_KEEP_ALIVE_ENABLED = new BooleanProperty( - "dapr.workflows.runtime.app.keep.alive.enabled", - "DAPR_WORKFLOWS_RUNTIME_APP_KEEP_ALIVE_ENABLED", - true); - - /** - * Interval between application-level keepalive pings on the workflow runtime's gRPC channel. - * Environment variable: DAPR_WORKFLOWS_APP_KEEP_ALIVE_INTERVAL_SECONDS - * System property: dapr.workflows.app.keep.alive.interval.seconds - * Default: 30 seconds - */ - public static final Property WORKFLOWS_APP_KEEP_ALIVE_INTERVAL_SECONDS = new SecondsDurationProperty( - "dapr.workflows.app.keep.alive.interval.seconds", - "DAPR_WORKFLOWS_APP_KEEP_ALIVE_INTERVAL_SECONDS", - Duration.ofSeconds(30)); - - /** - * GRPC endpoint for remote sidecar connectivity. - */ - public static final Property HTTP_ENDPOINT = new StringProperty( - "dapr.http.endpoint", - "DAPR_HTTP_ENDPOINT", - null); - - /** - * Maximum number of retries for retriable exceptions. - */ - public static final Property MAX_RETRIES = new IntegerProperty( - "dapr.api.maxRetries", - "DAPR_API_MAX_RETRIES", - DEFAULT_API_MAX_RETRIES); - - /** - * Timeout for API calls. - */ - public static final Property TIMEOUT = new MillisecondsDurationProperty( - "dapr.api.timeoutMilliseconds", - "DAPR_API_TIMEOUT_MILLISECONDS", - DEFAULT_API_TIMEOUT); - - /** - * API token for authentication between App and Dapr's side car. - */ - public static final Property API_TOKEN = new StringProperty( - "dapr.api.token", - "DAPR_API_TOKEN", - null); - - /** - * Determines which string encoding is used in Dapr's Java SDK. - */ - public static final Property STRING_CHARSET = new GenericProperty<>( - "dapr.string.charset", - "DAPR_STRING_CHARSET", - DEFAULT_STRING_CHARSET, - (s) -> Charset.forName(s)); - - /** - * Dapr's timeout in seconds for HTTP client reads. - */ - public static final Property HTTP_CLIENT_READ_TIMEOUT_SECONDS = new IntegerProperty( - "dapr.http.client.readTimeoutSeconds", - "DAPR_HTTP_CLIENT_READ_TIMEOUT_SECONDS", - DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECONDS); - - /** - * Dapr's default maximum number of requests for HTTP client to execute concurrently. - */ - public static final Property HTTP_CLIENT_MAX_REQUESTS = new IntegerProperty( - "dapr.http.client.maxRequests", - "DAPR_HTTP_CLIENT_MAX_REQUESTS", - DEFAULT_HTTP_CLIENT_MAX_REQUESTS); - - /** - * Dapr's default maximum number of idle connections for HTTP connection pool. - */ - public static final Property HTTP_CLIENT_MAX_IDLE_CONNECTIONS = new IntegerProperty( - "dapr.http.client.maxIdleConnections", - "DAPR_HTTP_CLIENT_MAX_IDLE_CONNECTIONS", - DEFAULT_HTTP_CLIENT_MAX_IDLE_CONNECTIONS); - - /** - * Dapr's default maximum inbound message size for GRPC in bytes. - */ - public static final Property GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES = new IntegerProperty( - "dapr.grpc.max.inbound.message.size.bytes", - "DAPR_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES", - 4194304); - - /** - * Dapr's default maximum inbound metadata size for GRPC in bytes. - */ - public static final Property GRPC_MAX_INBOUND_METADATA_SIZE_BYTES = new IntegerProperty( - "dapr.grpc.max.inbound.metadata.size.bytes", - "DAPR_GRPC_MAX_INBOUND_METADATA_SIZE_BYTES", - 8192); - - /** - * Mechanism to override properties set in a static context. - */ - private final Map overrides; - - /** - * Creates a new instance to handle Properties per instance. - */ - public Properties() { - this.overrides = null; - } - - /** - * Creates a new instance to handle Properties per instance. - * @param overridesInput to override static properties - */ - public Properties(Map overridesInput) { - this.overrides = overridesInput == null ? Map.of() : - Map.copyOf(overridesInput.entrySet().stream() - .filter(e -> e.getKey() != null) - .filter(e -> e.getValue() != null) - .collect(Collectors.toMap( - entry -> entry.getKey().toString(), - entry -> entry.getValue() - ))); - } - - /** - * Gets a property value taking in consideration the override values. - * @param type of the property that we want to get the value from - * @param property to override static property value from overrides - * @return the property's value - */ - public T getValue(Property property) { - if (overrides != null) { - String override = overrides.get(property.getName()); - return property.get(override); - } else { - return property.get(); - } - } -} +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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.dapr.config; + +import io.dapr.utils.NetworkUtils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Global properties for Dapr's SDK, using Supplier so they are dynamically resolved. + */ +public class Properties { + + /** + * Dapr's default IP for HTTP and gRPC communication. + */ + private static final String DEFAULT_SIDECAR_IP = NetworkUtils.getHostLoopbackAddress(); + + /** + * Dapr's default HTTP port. + */ + private static final Integer DEFAULT_HTTP_PORT = 3500; + + /** + * Dapr's default gRPC port. + */ + private static final Integer DEFAULT_GRPC_PORT = 50001; + + /** + * Dapr's default max retries. + */ + private static final Integer DEFAULT_API_MAX_RETRIES = 0; + + /** + * Dapr's default timeout in seconds. + */ + private static final Duration DEFAULT_API_TIMEOUT = Duration.ofMillis(0L); + + /** + * Dapr's default String encoding: UTF-8. + */ + private static final Charset DEFAULT_STRING_CHARSET = StandardCharsets.UTF_8; + + /** + * Dapr's default timeout in seconds for HTTP client reads. + */ + private static final Integer DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECONDS = 60; + + /** + * Dapr's default maximum number of requests for HTTP client to execute concurrently. + * + *

    Above this requests queue in memory, waiting for the running calls to complete. + * Default is 64 in okhttp which is OK for most case, but for some special case + * which is slow response and high concurrency, the value should set to a little big. + */ + private static final Integer DEFAULT_HTTP_CLIENT_MAX_REQUESTS = 1024; + + /** + * Dapr's default maximum number of idle connections of HTTP connection pool. + * + *

    Attention! This is max IDLE connection, NOT max connection! + * It is also very important for high concurrency cases. + */ + private static final Integer DEFAULT_HTTP_CLIENT_MAX_IDLE_CONNECTIONS = 128; + + /** + * IP for Dapr's sidecar. + */ + public static final Property SIDECAR_IP = new StringProperty( + "dapr.sidecar.ip", + "DAPR_SIDECAR_IP", + DEFAULT_SIDECAR_IP); + + /** + * HTTP port for Dapr after checking system property and environment variable. + */ + public static final Property HTTP_PORT = new IntegerProperty( + "dapr.http.port", + "DAPR_HTTP_PORT", + DEFAULT_HTTP_PORT); + + /** + * GRPC port for Dapr after checking system property and environment variable. + */ + public static final Property GRPC_PORT = new IntegerProperty( + "dapr.grpc.port", + "DAPR_GRPC_PORT", + DEFAULT_GRPC_PORT); + + /** + * GRPC TLS cert path for Dapr after checking system property and environment variable. + */ + public static final Property GRPC_TLS_CERT_PATH = new StringProperty( + "dapr.grpc.tls.cert.path", + "DAPR_GRPC_TLS_CERT_PATH", + null); + + /** + * GRPC TLS key path for Dapr after checking system property and environment variable. + */ + public static final Property GRPC_TLS_KEY_PATH = new StringProperty( + "dapr.grpc.tls.key.path", + "DAPR_GRPC_TLS_KEY_PATH", + null); + + /** + * GRPC TLS CA cert path for Dapr after checking system property and environment variable. + * This is used for TLS connections to servers with self-signed certificates. + */ + public static final Property GRPC_TLS_CA_PATH = new StringProperty( + "dapr.grpc.tls.ca.path", + "DAPR_GRPC_TLS_CA_PATH", + null); + + /** + * Use insecure TLS mode which still uses TLS but doesn't verify certificates. + * This uses InsecureTrustManagerFactory to trust all certificates. + * This should only be used for testing or in secure environments. + */ + public static final Property GRPC_TLS_INSECURE = new BooleanProperty( + "dapr.grpc.tls.insecure", + "DAPR_GRPC_TLS_INSECURE", + false); + + /** + * GRPC endpoint for remote sidecar connectivity. + */ + public static final Property GRPC_ENDPOINT = new StringProperty( + "dapr.grpc.endpoint", + "DAPR_GRPC_ENDPOINT", + null); + + /** + * GRPC enable keep alive. + * Environment variable: DAPR_GRPC_ENABLE_KEEP_ALIVE + * System property: dapr.grpc.enable.keep.alive + * Default: false + */ + public static final Property GRPC_ENABLE_KEEP_ALIVE = new BooleanProperty( + "dapr.grpc.enable.keep.alive", + "DAPR_GRPC_ENABLE_KEEP_ALIVE", + false); + + /** + * GRPC keep alive time in seconds. + * Environment variable: DAPR_GRPC_KEEP_ALIVE_TIME_SECONDS + * System property: dapr.grpc.keep.alive.time.seconds + * Default: 10 seconds + */ + public static final Property GRPC_KEEP_ALIVE_TIME_SECONDS = new SecondsDurationProperty( + "dapr.grpc.keep.alive.time.seconds", + "DAPR_GRPC_KEEP_ALIVE_TIME_SECONDS", + Duration.ofSeconds(10)); + + /** + * GRPC keep alive timeout in seconds. + * Environment variable: DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDS + * System property: dapr.grpc.keep.alive.timeout.seconds + * Default: 5 seconds + */ + public static final Property GRPC_KEEP_ALIVE_TIMEOUT_SECONDS = new SecondsDurationProperty( + "dapr.grpc.keep.alive.timeout.seconds", + "DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDS", + Duration.ofSeconds(5)); + + /** + * GRPC keep alive without calls. + * Environment variable: DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS + * System property: dapr.grpc.keep.alive.without.calls + * Default: true + */ + public static final Property GRPC_KEEP_ALIVE_WITHOUT_CALLS = new BooleanProperty( + "dapr.grpc.keep.alive.without.calls", + "DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS", + true); + + /** + * Enables the application-level keepalive on the workflow runtime's gRPC channel. + * While the runtime is started, the SDK periodically invokes the sidecar's hello RPC + * so that intermediaries that do not treat HTTP/2 PING frames as connection activity + * (e.g. AWS ALBs) never see the connection as idle and close it. + * Environment variable: DAPR_WORKFLOWS_RUNTIME_APP_KEEP_ALIVE_ENABLED + * System property: dapr.workflows.runtime.app.keep.alive.enabled + * Default: true + */ + public static final Property WORKFLOWS_RUNTIME_APP_KEEP_ALIVE_ENABLED = new BooleanProperty( + "dapr.workflows.runtime.app.keep.alive.enabled", + "DAPR_WORKFLOWS_RUNTIME_APP_KEEP_ALIVE_ENABLED", + true); + + /** + * Enables the virtual-thread-per-task executor that the workflow runtime uses by default when + * it creates its own executor on Java 21 or later. Set to false to keep the cached thread pool + * used on earlier runtimes, for example when activity code holds monitors across blocking calls + * and would pin carrier threads. Has no effect on a runtime given an executor via + * WorkflowRuntimeBuilder.withExecutorService, or on Java 17 through 20. + * Environment variable: DAPR_WORKFLOWS_VIRTUAL_THREADS_ENABLED + * System property: dapr.workflows.virtual.threads.enabled + * Default: true + */ + public static final Property WORKFLOWS_VIRTUAL_THREADS_ENABLED = new BooleanProperty( + "dapr.workflows.virtual.threads.enabled", + "DAPR_WORKFLOWS_VIRTUAL_THREADS_ENABLED", + true); + + /** + * Interval between application-level keepalive pings on the workflow runtime's gRPC channel. + * Environment variable: DAPR_WORKFLOWS_APP_KEEP_ALIVE_INTERVAL_SECONDS + * System property: dapr.workflows.app.keep.alive.interval.seconds + * Default: 30 seconds + */ + public static final Property WORKFLOWS_APP_KEEP_ALIVE_INTERVAL_SECONDS = new SecondsDurationProperty( + "dapr.workflows.app.keep.alive.interval.seconds", + "DAPR_WORKFLOWS_APP_KEEP_ALIVE_INTERVAL_SECONDS", + Duration.ofSeconds(30)); + + /** + * GRPC endpoint for remote sidecar connectivity. + */ + public static final Property HTTP_ENDPOINT = new StringProperty( + "dapr.http.endpoint", + "DAPR_HTTP_ENDPOINT", + null); + + /** + * Maximum number of retries for retriable exceptions. + */ + public static final Property MAX_RETRIES = new IntegerProperty( + "dapr.api.maxRetries", + "DAPR_API_MAX_RETRIES", + DEFAULT_API_MAX_RETRIES); + + /** + * Timeout for API calls. + */ + public static final Property TIMEOUT = new MillisecondsDurationProperty( + "dapr.api.timeoutMilliseconds", + "DAPR_API_TIMEOUT_MILLISECONDS", + DEFAULT_API_TIMEOUT); + + /** + * API token for authentication between App and Dapr's side car. + */ + public static final Property API_TOKEN = new StringProperty( + "dapr.api.token", + "DAPR_API_TOKEN", + null); + + /** + * Determines which string encoding is used in Dapr's Java SDK. + */ + public static final Property STRING_CHARSET = new GenericProperty<>( + "dapr.string.charset", + "DAPR_STRING_CHARSET", + DEFAULT_STRING_CHARSET, + (s) -> Charset.forName(s)); + + /** + * Dapr's timeout in seconds for HTTP client reads. + */ + public static final Property HTTP_CLIENT_READ_TIMEOUT_SECONDS = new IntegerProperty( + "dapr.http.client.readTimeoutSeconds", + "DAPR_HTTP_CLIENT_READ_TIMEOUT_SECONDS", + DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECONDS); + + /** + * Dapr's default maximum number of requests for HTTP client to execute concurrently. + */ + public static final Property HTTP_CLIENT_MAX_REQUESTS = new IntegerProperty( + "dapr.http.client.maxRequests", + "DAPR_HTTP_CLIENT_MAX_REQUESTS", + DEFAULT_HTTP_CLIENT_MAX_REQUESTS); + + /** + * Dapr's default maximum number of idle connections for HTTP connection pool. + */ + public static final Property HTTP_CLIENT_MAX_IDLE_CONNECTIONS = new IntegerProperty( + "dapr.http.client.maxIdleConnections", + "DAPR_HTTP_CLIENT_MAX_IDLE_CONNECTIONS", + DEFAULT_HTTP_CLIENT_MAX_IDLE_CONNECTIONS); + + /** + * Dapr's default maximum inbound message size for GRPC in bytes. + */ + public static final Property GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES = new IntegerProperty( + "dapr.grpc.max.inbound.message.size.bytes", + "DAPR_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES", + 4194304); + + /** + * Dapr's default maximum inbound metadata size for GRPC in bytes. + */ + public static final Property GRPC_MAX_INBOUND_METADATA_SIZE_BYTES = new IntegerProperty( + "dapr.grpc.max.inbound.metadata.size.bytes", + "DAPR_GRPC_MAX_INBOUND_METADATA_SIZE_BYTES", + 8192); + + /** + * Mechanism to override properties set in a static context. + */ + private final Map overrides; + + /** + * Creates a new instance to handle Properties per instance. + */ + public Properties() { + this.overrides = null; + } + + /** + * Creates a new instance to handle Properties per instance. + * @param overridesInput to override static properties + */ + public Properties(Map overridesInput) { + this.overrides = overridesInput == null ? Map.of() : + Map.copyOf(overridesInput.entrySet().stream() + .filter(e -> e.getKey() != null) + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap( + entry -> entry.getKey().toString(), + entry -> entry.getValue() + ))); + } + + /** + * Gets a property value taking in consideration the override values. + * @param type of the property that we want to get the value from + * @param property to override static property value from overrides + * @return the property's value + */ + public T getValue(Property property) { + if (overrides != null) { + String override = overrides.get(property.getName()); + return property.get(override); + } else { + return property.get(); + } + } +} From a61cd1458b30533f2288f0b25ca08869ac2f284e Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Wed, 2 Sep 2026 14:06:57 +0200 Subject: [PATCH 5/6] feat: read the workflow virtual threads property from the Spring Environment The workflow runtime resolves dapr.workflows.virtual.threads.enabled through the SDK Properties, which consults only JVM system properties and environment variables. Setting it in application.properties silently did nothing, because the auto-configuration builds its property overrides from the connection details alone. The auto-configured WorkflowRuntimeBuilder now layers the Spring Environment over those overrides, so the opt-out works like any other Spring property. When the property is set nowhere no override is added and the SDK default still applies. spring.threads.virtual.enabled remains unrelated to the workflow runtime: Spring's virtual-thread applicationTaskExecutor is a SimpleAsyncTaskExecutor, which is not an ExecutorService and cannot be handed to withExecutorService without an adapter. SUPPORT.md is corrected to say so, and to drop the advice to define a custom builder bean, which is unnecessary now that virtual threads are the default on Java 21. Signed-off-by: Javier Aliaga --- SUPPORT.md | 24 +++++++----- .../client/DaprClientAutoConfiguration.java | 38 +++++++++++++++++-- .../DaprClientAutoConfigurationTest.java | 32 ++++++++++++++++ 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/SUPPORT.md b/SUPPORT.md index 70afe3ac1f..6cffbd3392 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -28,16 +28,20 @@ default is a virtual-thread-per-task executor; on Java 17 through 20 it is a cac You can always supply your own with `WorkflowRuntimeBuilder.withExecutorService(...)`, and an executor you supply is never shut down by the runtime. -Virtual threads are on by default there. To opt out and keep the cached thread pool, set -`dapr.workflows.virtual.threads.enabled=false` (or `DAPR_WORKFLOWS_VIRTUAL_THREADS_ENABLED=false`). -The setting only affects the executor the runtime creates for itself; it has no effect on Java 17 -through 20, or on a runtime given an executor via `withExecutorService(...)`. - -Note for Spring Boot users: `spring.threads.virtual.enabled=true` does **not** currently reach the -workflow runtime. It switches Spring's own executors, but the Dapr auto-configuration does not hand -the application's task executor to `WorkflowRuntimeBuilder`, so workflow and activity execution is -unaffected by it. Define your own `WorkflowRuntimeBuilder` bean and call `withExecutorService(...)` -if you want workflows on virtual threads. +Virtual threads are on by default there. To opt out and keep the cached thread pool, set the JVM +system property `dapr.workflows.virtual.threads.enabled=false` or the environment variable +`DAPR_WORKFLOWS_VIRTUAL_THREADS_ENABLED=false`. The setting only affects the executor the runtime +creates for itself; it has no effect on Java 17 through 20, or on a runtime given an executor via +`withExecutorService(...)`. + +Note for Spring Boot users: the auto-configured `WorkflowRuntimeBuilder` also reads +`dapr.workflows.virtual.threads.enabled` from the Spring `Environment`, so you can set it in +`application.properties` alongside your other Dapr settings. `spring.threads.virtual.enabled` is a +separate switch and does **not** reach the workflow runtime: it changes Spring's own executors, and +the auto-configuration does not hand the application task executor to `WorkflowRuntimeBuilder`. You +do not need it, because workflows already run on virtual threads by default on Java 21. To put +workflows on an executor you control, define your own `WorkflowRuntimeBuilder` bean and call +`withExecutorService(...)`. Virtual threads help **activities**, which run your code and typically block on I/O. Workflow code itself is replay-based and does not block, so it sees little benefit. The SDK holds no monitor on diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java index 4fd6f0f889..b0a8c5811d 100644 --- a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; import java.util.HashMap; import java.util.Map; @@ -132,8 +133,9 @@ ActorRuntime daprActorRuntime(DaprConnectionDetails daprConnectionDetails) { @Bean @ConditionalOnMissingBean - WorkflowRuntimeBuilder daprWorkflowRuntimeBuilder(DaprConnectionDetails daprConnectionDetails) { - Properties properties = createPropertiesFromConnectionDetails(daprConnectionDetails); + WorkflowRuntimeBuilder daprWorkflowRuntimeBuilder(DaprConnectionDetails daprConnectionDetails, + Environment environment) { + Properties properties = createWorkflowProperties(daprConnectionDetails, environment); return new WorkflowRuntimeBuilder(properties); } @@ -152,6 +154,36 @@ protected DaprClientBuilder createDaprClientBuilder() { * @return the Properties object */ protected Properties createPropertiesFromConnectionDetails(DaprConnectionDetails daprConnectionDetails) { + return new Properties(createPropertyOverrides(daprConnectionDetails)); + } + + /** + * Creates a Properties object for the workflow runtime, layering the Spring Environment on top of + * the connection details. + * + *

    The runtime resolves {@link Properties#WORKFLOWS_VIRTUAL_THREADS_ENABLED} through the SDK + * Properties, which consults only JVM system properties and environment variables. Reading it + * from the Environment here lets it be set in application.properties like any other Spring + * property. When it is set nowhere the override is omitted, so the SDK default still applies. + * + * @param daprConnectionDetails the DaprConnectionDetails + * @param environment the Spring Environment + * @return the Properties object + */ + protected Properties createWorkflowProperties(DaprConnectionDetails daprConnectionDetails, + Environment environment) { + Map propertyOverrides = createPropertyOverrides(daprConnectionDetails); + String propertyName = Properties.WORKFLOWS_VIRTUAL_THREADS_ENABLED.getName(); + String virtualThreadsEnabled = environment.getProperty(propertyName); + + if (virtualThreadsEnabled != null) { + propertyOverrides.put(propertyName, virtualThreadsEnabled); + } + + return new Properties(propertyOverrides); + } + + private Map createPropertyOverrides(DaprConnectionDetails daprConnectionDetails) { Map propertyOverrides = new HashMap<>(); String httpEndpoint = daprConnectionDetails.getHttpEndpoint(); @@ -182,7 +214,7 @@ protected Properties createPropertiesFromConnectionDetails(DaprConnectionDetails propertyOverrides.put(Properties.API_TOKEN.getName(), apiToken); } - return new Properties(propertyOverrides); + return propertyOverrides; } } diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTest.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTest.java index 4eae48bf30..ad2c12a592 100644 --- a/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTest.java +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTest.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; @@ -187,6 +188,37 @@ void shouldOverrideApiTokenPropertiesIfExists() { assertThat(result.getValue(Properties.API_TOKEN)).isEqualTo(apiToken); } + @Test + @DisplayName("Should read the virtual threads property from the Spring Environment") + void shouldReadVirtualThreadsPropertyFromEnvironment() { + MockEnvironment environment = new MockEnvironment() + .withProperty(Properties.WORKFLOWS_VIRTUAL_THREADS_ENABLED.getName(), "false"); + + Properties result = configuration.createWorkflowProperties(connectionDetails, environment); + + assertThat(result.getValue(Properties.WORKFLOWS_VIRTUAL_THREADS_ENABLED)).isFalse(); + } + + @Test + @DisplayName("Should keep the virtual threads default when the Environment does not set it") + void shouldKeepVirtualThreadsDefaultWhenEnvironmentDoesNotSetIt() { + Properties result = configuration.createWorkflowProperties(connectionDetails, new MockEnvironment()); + + assertThat(result.getValue(Properties.WORKFLOWS_VIRTUAL_THREADS_ENABLED)).isTrue(); + } + + @Test + @DisplayName("Should keep the connection details in the workflow properties") + void shouldKeepConnectionDetailsInWorkflowProperties() { + String grpcEndpoint = "grpc://localhost:5001"; + + when(connectionDetails.getGrpcEndpoint()).thenReturn(grpcEndpoint); + + Properties result = configuration.createWorkflowProperties(connectionDetails, new MockEnvironment()); + + assertThat(result.getValue(Properties.GRPC_ENDPOINT)).isEqualTo(grpcEndpoint); + } + private static class TestDaprClientAutoConfiguration extends DaprClientAutoConfiguration { private final DaprClientBuilder daprClientBuilder; From 492e7ae8aba4be15a9292c21c8c872f3c4041be0 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Wed, 2 Sep 2026 16:03:07 +0200 Subject: [PATCH 6/6] docs: name the SDK line that carries the workflows module unification State in SUPPORT.md that durabletask-client is folded into dapr-sdk-workflows in 1.19.x, so a user reading the version table knows which upgrade requires the source changes described in MIGRATION.md. Signed-off-by: Javier Aliaga --- SUPPORT.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SUPPORT.md b/SUPPORT.md index 6cffbd3392..eb10686a2a 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -14,9 +14,10 @@ at a given version pins the whole stack, core and Spring, to that single version ### Migrating to the unified workflows module -`durabletask-client` has been folded into `dapr-sdk-workflows`, which is a breaking change for -workflow code. See [MIGRATION.md](MIGRATION.md) for the type mapping, the removed API, and what it -means for workflows that are already running when you upgrade. +In **1.19.x**, `durabletask-client` has been folded into `dapr-sdk-workflows`, which is a breaking +change for workflow code — so upgrading from 1.18.x requires source changes. See +[MIGRATION.md](MIGRATION.md) for the type mapping, the removed API, and what it means for workflows +that are already running when you upgrade. ### Java 21 is recommended for workflows