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/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000000..a8745b3f69 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,283 @@ +# 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 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. + +--- + +## 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..eb10686a2a 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -12,7 +12,46 @@ 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 + +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 + +`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. + +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 +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 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; 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..c0e8f89bc6 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/internal/DefaultExecutorService.java @@ -0,0 +1,78 @@ +/* + * 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 io.dapr.config.Properties; +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. + * + *

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. + */ +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. + * + * @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(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"); + 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..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 @@ -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(this.properties); + } 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 77% 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..08c70d61d4 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; @@ -34,7 +36,16 @@ public class CompositeTaskFailedException extends RuntimeException { this.exceptions = exceptions; } - CompositeTaskFailedException(String message, List 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; } @@ -49,8 +60,8 @@ public class CompositeTaskFailedException extends RuntimeException { this.exceptions = exceptions; } - 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/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 76% 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..25a662ee6c 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,25 @@ * 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) { + /** + * Creates a task failure with a message derived from the task and failure details. + * + *

    Public because the workflow executor constructs it from another package. Not intended for + * application code. + * + * @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 taskName, int taskId, WorkflowFailureDetails details) { this(getExceptionMessage(taskName, taskId, details), taskName, taskId, details); } - TaskFailedException(String message, String taskName, int taskId, FailureDetails details) { + TaskFailedException(String message, String taskName, int taskId, WorkflowFailureDetails details) { super(message); this.taskName = taskName; this.taskId = taskId; @@ -64,11 +76,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..18be6404ec --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/internal/DefaultExecutorServiceTest.java @@ -0,0 +1,114 @@ +/* + * 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 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; + +/** + * 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(new Properties()); + + 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(new Properties()); + 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(); + } + } + + /** + * 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. + * + * @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/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(); + } + } +} 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;