├── .dockerignore ├── .github ├── policies │ └── resourceManagement.yml ├── pull_request_template.md └── workflows │ └── build-validation.yml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── azdevops-pipeline └── build-release-artifacts.yml ├── azurefunctions ├── README.md ├── build.gradle ├── spotbugs-exclude.xml └── src │ └── main │ ├── java │ └── com │ │ └── microsoft │ │ └── durabletask │ │ └── azurefunctions │ │ ├── DurableActivityTrigger.java │ │ ├── DurableClientContext.java │ │ ├── DurableClientInput.java │ │ ├── DurableOrchestrationTrigger.java │ │ ├── HttpManagementPayload.java │ │ └── internal │ │ └── middleware │ │ └── OrchestrationMiddleware.java │ └── resources │ └── META-INF │ └── services │ └── com.microsoft.azure.functions.internal.spi.middleware.Middleware ├── azuremanaged ├── build.gradle ├── spotbugs-exclude.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── microsoft │ │ │ └── durabletask │ │ │ └── azuremanaged │ │ │ ├── AccessTokenCache.java │ │ │ ├── DurableTaskSchedulerClientExtensions.java │ │ │ ├── DurableTaskSchedulerClientOptions.java │ │ │ ├── DurableTaskSchedulerConnectionString.java │ │ │ ├── DurableTaskSchedulerWorkerExtensions.java │ │ │ ├── DurableTaskSchedulerWorkerOptions.java │ │ │ └── DurableTaskUserAgentUtil.java │ └── resources │ │ └── VersionInfo.java.template │ └── test │ └── java │ └── com │ └── microsoft │ └── durabletask │ └── azuremanaged │ ├── AccessTokenCacheTest.java │ ├── DurableTaskSchedulerClientExtensionsTest.java │ ├── DurableTaskSchedulerClientExtensionsUserAgentTest.java │ ├── DurableTaskSchedulerClientOptionsTest.java │ ├── DurableTaskSchedulerConnectionStringTest.java │ ├── DurableTaskSchedulerWorkerExtensionsTest.java │ └── DurableTaskSchedulerWorkerOptionsTest.java ├── build.gradle ├── client ├── build.gradle ├── spotbugs-exclude.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── microsoft │ │ └── durabletask │ │ ├── CompositeTaskFailedException.java │ │ ├── DataConverter.java │ │ ├── DurableTaskClient.java │ │ ├── DurableTaskGrpcClient.java │ │ ├── DurableTaskGrpcClientBuilder.java │ │ ├── DurableTaskGrpcWorker.java │ │ ├── DurableTaskGrpcWorkerBuilder.java │ │ ├── FailureDetails.java │ │ ├── Helpers.java │ │ ├── JacksonDataConverter.java │ │ ├── NewOrchestrationInstanceOptions.java │ │ ├── NonDeterministicOrchestratorException.java │ │ ├── OrchestrationMetadata.java │ │ ├── OrchestrationRunner.java │ │ ├── OrchestrationRuntimeStatus.java │ │ ├── OrchestrationStatusQuery.java │ │ ├── OrchestrationStatusQueryResult.java │ │ ├── OrchestratorFunction.java │ │ ├── PurgeInstanceCriteria.java │ │ ├── PurgeResult.java │ │ ├── RetryContext.java │ │ ├── RetryHandler.java │ │ ├── RetryPolicy.java │ │ ├── Task.java │ │ ├── TaskActivity.java │ │ ├── TaskActivityContext.java │ │ ├── TaskActivityExecutor.java │ │ ├── TaskActivityFactory.java │ │ ├── TaskCanceledException.java │ │ ├── TaskFailedException.java │ │ ├── TaskOptions.java │ │ ├── TaskOrchestration.java │ │ ├── TaskOrchestrationContext.java │ │ ├── TaskOrchestrationExecutor.java │ │ ├── TaskOrchestrationFactory.java │ │ ├── TaskOrchestratorResult.java │ │ ├── interruption │ │ ├── ContinueAsNewInterruption.java │ │ └── OrchestratorBlockedException.java │ │ └── util │ │ └── UUIDGenerator.java │ └── test │ └── java │ └── com │ └── microsoft │ └── durabletask │ ├── ErrorHandlingIntegrationTests.java │ ├── IntegrationTestBase.java │ └── IntegrationTests.java ├── endtoendtests ├── Dockerfile ├── build.gradle ├── e2e-test-setup.ps1 ├── host.json ├── local.settings.json └── src │ ├── main │ └── java │ │ └── com │ │ └── functions │ │ ├── AzureFunctions.java │ │ ├── ContinueAsNew.java │ │ ├── DeserializeErrorTest.java │ │ ├── ParallelFunctions.java │ │ ├── ResumeSuspend.java │ │ ├── RetriableTask.java │ │ └── ThenChain.java │ └── test │ └── java │ └── com │ └── functions │ └── EndToEndTests.java ├── eng ├── ci │ ├── code-mirror.yml │ ├── official-build.yml │ └── release.yml └── templates │ └── build.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── internal └── durabletask-protobuf │ ├── PROTO_SOURCE_COMMIT_HASH │ ├── README.md │ └── protos │ └── orchestrator_service.proto ├── libs └── durabletask-grpc-0.1.0.jar ├── samples-azure-functions ├── Dockerfile ├── build.gradle ├── e2e-test-setup.ps1 ├── host.json ├── local.settings.json └── src │ ├── main │ └── java │ │ └── com │ │ └── functions │ │ ├── AzureFunctions.java │ │ ├── ContinueAsNew.java │ │ ├── ParallelFunctions.java │ │ ├── ResumeSuspend.java │ │ ├── RetriableTask.java │ │ ├── SubOrchestrator.java │ │ ├── ThenChain.java │ │ ├── WaitExternalEvent.java │ │ └── model │ │ └── Person.java │ └── test │ └── java │ └── com │ └── functions │ └── EndToEndTests.java ├── samples ├── build.gradle └── src │ └── main │ ├── java │ └── io │ │ └── durabletask │ │ └── samples │ │ ├── ChainingPattern.java │ │ ├── FanOutFanInPattern.java │ │ ├── OrchestrationController.java │ │ ├── WebAppToDurableTaskSchedulerSample.java │ │ └── WebApplication.java │ └── resources │ ├── application.properties │ └── web-app-to-durable-task-scheduler-sample.http └── settings.gradle /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/compose* 19 | **/Dockerfile* 20 | **/node_modules 21 | **/npm-debug.log 22 | **/obj 23 | **/secrets.dev.yaml 24 | **/values.dev.yaml 25 | README.md 26 | -------------------------------------------------------------------------------- /.github/policies/resourceManagement.yml: -------------------------------------------------------------------------------- 1 | id: 2 | name: GitOps.PullRequestIssueManagement 3 | description: GitOps.PullRequestIssueManagement primitive 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | scheduledSearches: 11 | - description: 12 | frequencies: 13 | - hourly: 14 | hour: 3 15 | filters: 16 | - isIssue 17 | - isOpen 18 | - hasLabel: 19 | label: "Needs: Author Feedback" 20 | - hasLabel: 21 | label: no-recent-activity 22 | - noActivitySince: 23 | days: 3 24 | actions: 25 | - closeIssue 26 | - description: 27 | frequencies: 28 | - hourly: 29 | hour: 3 30 | filters: 31 | - isIssue 32 | - isOpen 33 | - hasLabel: 34 | label: "Needs: Author Feedback" 35 | - noActivitySince: 36 | days: 4 37 | - isNotLabeledWith: 38 | label: no-recent-activity 39 | actions: 40 | - addLabel: 41 | label: no-recent-activity 42 | - addReply: 43 | reply: This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**. 44 | - description: 45 | frequencies: 46 | - hourly: 47 | hour: 3 48 | filters: 49 | - isIssue 50 | - isOpen 51 | - hasLabel: 52 | label: duplicate 53 | - noActivitySince: 54 | days: 1 55 | actions: 56 | - addReply: 57 | reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. 58 | - closeIssue 59 | eventResponderTasks: 60 | - if: 61 | - payloadType: Issues 62 | - and: 63 | - isOpen 64 | - not: 65 | and: 66 | - isLabeled 67 | then: 68 | - addLabel: 69 | label: "Needs: Triage :mag:" 70 | - if: 71 | - payloadType: Issue_Comment 72 | - isAction: 73 | action: Created 74 | - isActivitySender: 75 | issueAuthor: True 76 | - hasLabel: 77 | label: "Needs: Author Feedback" 78 | then: 79 | - addLabel: 80 | label: "Needs: Attention :wave:" 81 | - removeLabel: 82 | label: "Needs: Author Feedback" 83 | description: 84 | - if: 85 | - payloadType: Issues 86 | - not: 87 | isAction: 88 | action: Closed 89 | - hasLabel: 90 | label: no-recent-activity 91 | then: 92 | - removeLabel: 93 | label: no-recent-activity 94 | description: 95 | - if: 96 | - payloadType: Issue_Comment 97 | - hasLabel: 98 | label: no-recent-activity 99 | then: 100 | - removeLabel: 101 | label: no-recent-activity 102 | description: 103 | onFailure: 104 | onSuccess: 105 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Issue describing the changes in this PR 4 | 5 | resolves #issue_for_this_pr 6 | 7 | ### Pull request checklist 8 | 9 | * [ ] My changes **do not** require documentation changes 10 | * [ ] Otherwise: Documentation issue linked to PR 11 | * [ ] My changes are added to the `CHANGELOG.md` 12 | * [ ] I have added all required tests (Unit tests, E2E tests) 13 | 14 | 15 | ### Additional information 16 | 17 | Additional PR information -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | .vscode/ 4 | bin/ 5 | build/ 6 | .project 7 | .settings 8 | .classpath 9 | repo/ 10 | 11 | # Ignore sample application properties or any other properties files used for sample 12 | samples/src/main/resources/*.properties -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.5.2 2 | * Add distributed tracing support for Azure Functions client scenarios ([#211](https://github.com/microsoft/durabletask-java/pull/211)) 3 | 4 | 5 | ## v1.5.1 6 | * Improve logging for unexpected connection failures in DurableTaskGrpcWorker ([#216](https://github.com/microsoft/durabletask-java/pull/216/files)) 7 | * Add User-Agent Header to gRPC Metadata ([#213](https://github.com/microsoft/durabletask-java/pull/213)) 8 | * Update protobuf definitions to include new properties in OrchestratorRequest and update source commit hash ([#214](https://github.com/microsoft/durabletask-java/pull/214)) 9 | * DTS Support ([#201](https://github.com/microsoft/durabletask-java/pull/201)) 10 | * Add automatic proto file download and commit hash tracking during build ([#207](https://github.com/microsoft/durabletask-java/pull/207)) 11 | * Fix infinite loop when use continueasnew after wait external event ([#183](https://github.com/microsoft/durabletask-java/pull/183)) 12 | * Fix the issue "Deserialize Exception got swallowed when use anyOf with external event." ([#185](https://github.com/microsoft/durabletask-java/pull/185)) 13 | 14 | ## v1.5.0 15 | * Fix exception type issue when using `RetriableTask` in fan in/out pattern ([#174](https://github.com/microsoft/durabletask-java/pull/174)) 16 | * Add implementation to generate name-based deterministic UUID ([#176](https://github.com/microsoft/durabletask-java/pull/176)) 17 | * Update dependencies to resolve CVEs ([#177](https://github.com/microsoft/durabletask-java/pull/177)) 18 | 19 | 20 | ## v1.4.0 21 | 22 | ### Updates 23 | * Refactor `createTimer` to be non-blocking ([#161](https://github.com/microsoft/durabletask-java/pull/161)) 24 | 25 | ## v1.3.0 26 | * Refactor `RetriableTask` and add new `CompoundTask`, fixing Fan-out/Fan-in stuck when using `RetriableTask` ([#157](https://github.com/microsoft/durabletask-java/pull/157)) 27 | 28 | ## v1.2.0 29 | 30 | ### Updates 31 | * Add `thenAccept` and `thenApply` to `Task` interface ([#148](https://github.com/microsoft/durabletask-java/pull/148)) 32 | * Support Suspend and Resume Client APIs ([#151](https://github.com/microsoft/durabletask-java/pull/151)) 33 | * Support restartInstance and pass restartPostUri in HttpManagementPayload ([#108](https://github.com/microsoft/durabletask-java/issues/108)) 34 | * Improve `TaskOrchestrationContext#continueAsNew` method so it doesn't require `return` statement right after it anymore ([#149](https://github.com/microsoft/durabletask-java/pull/149)) 35 | 36 | ## v1.1.1 37 | 38 | ### Updates 39 | * Fix exception occurring when invoking the `TaskOrchestrationContext#continueAsNew` method ([#118](https://github.com/microsoft/durabletask-java/issues/118)) 40 | 41 | ## v1.1.0 42 | 43 | ### Updates 44 | * Fix the potential NPE issue of `DurableTaskClient#terminate` method ([#104](https://github.com/microsoft/durabletask-java/issues/104)) 45 | * Add waitForCompletionOrCreateCheckStatusResponse client API ([#115](https://github.com/microsoft/durabletask-java/pull/115)) 46 | * Support long timers by breaking up into smaller timers ([#114](https://github.com/microsoft/durabletask-java/issues/114)) 47 | 48 | ## v1.0.0 49 | 50 | ### New 51 | 52 | * Add CHANGELOG.md file to track changes across versions 53 | * Added createHttpManagementPayload API ([#63](https://github.com/microsoft/durabletask-java/issues/63)) 54 | 55 | ### Updates 56 | 57 | * update DataConverterException with detail error message ([#78](https://github.com/microsoft/durabletask-java/issues/78)) 58 | * update OrchestratorBlockedEvent and TaskFailedException to be unchecked exceptions ([#88](https://github.com/microsoft/durabletask-java/issues/88)) 59 | * update dependency azure-functions-java-library to 2.2.0 - include azure-functions-java-spi as `compileOnly` dependency ([#95](https://github.com/microsoft/durabletask-java/pull/95)) 60 | 61 | ### Breaking changes 62 | 63 | * Use java worker middleware to avoid wrapper method when create orchestrator function ([#87](https://github.com/microsoft/durabletask-java/pull/87)) 64 | * Fixed DurableClientContext.createCheckStatusResponse to return 202 ([#92](https://github.com/microsoft/durabletask-java/pull/92)) 65 | * context.allOf() now throws CompositeTaskFailedException(RuntimeException) when one or more tasks fail ([#54](https://github.com/microsoft/durabletask-java/issues/54)) 66 | * Updated `DurableTaskClient.purgeInstances(...)` to take a timeout parameter and throw `TimeoutException` ([#37](https://github.com/microsoft/durabletask-java/issues/37)) 67 | * The `waitForInstanceStart(...)` and `waitForInstanceCompletion(...)` methods of the `DurableTaskClient` interface now throw a checked `TimeoutException` 68 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ 2 | # for more info about CODEOWNERS file 3 | # 4 | # It uses the same pattern rule for gitignore file 5 | # https://git-scm.com/docs/gitignore#_pattern_format 6 | # 7 | 8 | # 9 | # For all file changes, github would automatically include the following people in the PRs. 10 | # 11 | * @microsoft/durabletask 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Onboarding 2 | This contributor guide explains how to make and test changes to Durable Functions in Java. 3 | Thank you for taking the time to contribute to the DurableTask Java SDK! 4 | 5 | ## Table of Contents 6 | 7 | - [Relevant Docs](#relevant-docs) 8 | - [Prerequisites](#prerequisites) 9 | - [Pull Request Change Flow](#pull-request-change-flow) 10 | - [Testing with a Durable Functions app](#testing-with-a-durable-functions-app) 11 | - [Debugging .NET packages from a Durable Functions Java app](#debugging-net-packages-from-a-durable-functions-java-app) 12 | 13 | ## Relevant Docs 14 | - [Durable Functions Overview](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview) 15 | - [Durable Functions Application Patterns](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview?tabs=in-process%2Cnodejs-v3%2Cv1-model&pivots=java#application-patterns) 16 | - [Azure Functions Java Quickstart](https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-java) 17 | 18 | ## Prerequisites 19 | - Visual Studio Code 20 | - [Azure Functions Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-java) 21 | - Apache Maven 3.8.1 or higher (Note: the instructions in this doc were validated using Apache Maven 3.9.9) 22 | - Gradle 7.4 23 | - Java 8 or higher (Note: the instructions in this doc were validated using Java 17) 24 | 25 | ## Pull Request Change Flow 26 | 27 | The general flow for making a change to the library is: 28 | 29 | 1. 🍴 Fork the repo (add the fork via `git remote add me `) 30 | 2. 🌳 Create a branch for your change (generally branch from dev) (`git checkout -b my-change`) 31 | 3. 🛠 Make your change 32 | 4. ✔️ Test your change 33 | 5. ⬆️ Push your changes to your fork (`git push me my-change`) 34 | 6. 💌 Open a PR to the dev branch 35 | 7. 📢 Address feedback and make sure tests pass (yes even if it's an "unrelated" test failure) 36 | 8. 📦 [Rebase](https://git-scm.com/docs/git-rebase) your changes into meaningful commits (`git rebase -i HEAD~N` where `N` is commits you want to squash) 37 | 9. :shipit: Rebase and merge (This will be done for you if you don't have contributor access) 38 | 10. ✂️ Delete your branch (optional) 39 | 40 | ## Testing with a Durable Functions app 41 | 42 | The following instructions explain how to test durabletask-java changes in a Durable Functions Java app. 43 | 44 | 1. After making changes in durabletask-java, you will need to increment the version number in build.gradle. For example, if you make a change in the azurefunctions directory, then you would update the version in `azurefunctions/build.gradle`. 45 | 2. In the durabletask-java repo, from the root of the project, run `./gradlew clean build`. This will create the .jar files with the updated version that you specified. 46 | 3. To get the .jar file that was created, go to the `build/libs` directory. For example, if you made a change in azurefunctions, then go to `durabletask-java/azurefunctions/build/libs`. If you made a change to client, then go to `durabletask-java/client/build/libs`. Add the .jar files that you are testing to a local directory. 47 | 4. [Create a Durable Functions Java app](https://learn.microsoft.com/en-us/azure/azure-functions/durable/quickstart-java?tabs=bash&pivots=create-option-vscode) if you haven't done so already. 48 | 5. In the Durable Functions Java app, run the following command to install the local .jar files that were created in step 2: `mvn install:install-file -Dfile="" -DgroupId="com.microsoft" -DartifactId="" -Dversion="" -Dpackaging="jar" -DlocalRepositoryPath=""`. 49 | 50 | For example, if you created custom `durabletask-client` and `durabletask-azure-functions` packages with version 1.6.0 in step 2, then you would run the following commands: 51 | 52 | ``` 53 | mvn install:install-file -Dfile="C:/Temp/durabletask-client-1.6.0.jar" -DgroupId="com.microsoft" -DartifactId="durabletask-client" -Dversion="1.6.0" -Dpackaging="jar" -DlocalRepositoryPath="C:/df-java-sample-app" 54 | 55 | mvn install:install-file -Dfile="C:/Temp/durabletask-azure-functions-1.6.0.jar" -DgroupId="com.microsoft" -DartifactId="durabletask-azure-functions" -Dversion="1.6.0" -Dpackaging="jar" -DlocalRepositoryPath="C:/df-java-sample-app" 56 | ``` 57 | 58 | 6. Run `mvn clean package` from the Durable Functions app root folder. 59 | 7. Run `mvn azure-functions:run` from the Durable Functions app root folder. 60 | 61 | ## Debugging .NET packages from a Durable Functions Java app 62 | 63 | If you want to debug into the Durable Task or any of the .NET bits, follow the instructions below: 64 | 65 | 1. If you would like to debug a custom local WebJobs extension package then create the custom package, place it in a local directory, and then run `func extensions install --package Microsoft.Azure.WebJobs.Extensions.DurableTask --version `. If you update the version while debugging and the new version doesn't get picked up, then try running `func extensions install` to get the new changes. 66 | 2. Make sure the Durable Functions Java debugging is set up already and the debugger has started the `func` process. 67 | 3. In the VSCode editor for DurableTask, click Debug -> .NET Core Attach Process, search for `func host start` process and attach to it. 68 | 4. Add a breakpoint in both editors and continue debugging. 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Durable Task Client SDK for Java 2 | 3 | [![Build](https://github.com/microsoft/durabletask-java/actions/workflows/build-validation.yml/badge.svg)](https://github.com/microsoft/durabletask-java/actions/workflows/build-validation.yml) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | This repo contains the Java SDK for the Durable Task Framework as well as classes and annotations to support running [Azure Durable Functions](https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-overview?tabs=java) for Java. With this SDK, you can define, schedule, and manage durable orchestrations using ordinary Java code. 7 | 8 | ### Simple, fault-tolerant sequences 9 | 10 | ```java 11 | // *** Simple, fault-tolerant, sequential orchestration *** 12 | String result = ""; 13 | result += ctx.callActivity("SayHello", "Tokyo", String.class).await() + ", "; 14 | result += ctx.callActivity("SayHello", "London", String.class).await() + ", "; 15 | result += ctx.callActivity("SayHello", "Seattle", String.class).await(); 16 | return result; 17 | ``` 18 | 19 | ### Reliable fan-out / fan-in orchestration pattern 20 | 21 | ```java 22 | // Get the list of work-items to process 23 | List batch = ctx.callActivity("GetWorkBatch", List.class).await(); 24 | 25 | // Schedule each task to run in parallel 26 | List> parallelTasks = batch.stream() 27 | .map(item -> ctx.callActivity("ProcessItem", item, Integer.class)) 28 | .collect(Collectors.toList()); 29 | 30 | // Wait for all tasks to complete, then return the aggregated sum of the results 31 | List results = ctx.allOf(parallelTasks).await(); 32 | return results.stream().reduce(0, Integer::sum); 33 | ``` 34 | 35 | ### Long-running human interaction pattern (approval workflow) 36 | 37 | ```java 38 | ApprovalInfo approvalInfo = ctx.getInput(ApprovalInfo.class); 39 | ctx.callActivity("RequestApproval", approvalInfo).await(); 40 | 41 | Duration timeout = Duration.ofHours(72); 42 | try { 43 | // Wait for an approval. A TaskCanceledException will be thrown if the timeout expires. 44 | boolean approved = ctx.waitForExternalEvent("ApprovalEvent", timeout, boolean.class).await(); 45 | approvalInfo.setApproved(approved); 46 | 47 | ctx.callActivity("ProcessApproval", approvalInfo).await(); 48 | } catch (TaskCanceledException timeoutEx) { 49 | ctx.callActivity("Escalate", approvalInfo).await(); 50 | } 51 | ``` 52 | 53 | ### Eternal monitoring orchestration 54 | 55 | ```java 56 | JobInfo jobInfo = ctx.getInput(JobInfo.class); 57 | String jobId = jobInfo.getJobId(); 58 | 59 | String status = ctx.callActivity("GetJobStatus", jobId, String.class).await(); 60 | if (status.equals("Completed")) { 61 | // The job is done - we can exit now 62 | ctx.callActivity("SendAlert", jobId).await(); 63 | } else { 64 | // wait N minutes before doing the next poll 65 | Duration pollingDelay = jobInfo.getPollingDelay(); 66 | ctx.createTimer(pollingDelay).await(); 67 | 68 | // restart from the beginning 69 | ctx.continueAsNew(jobInfo); 70 | } 71 | 72 | return null; 73 | ``` 74 | 75 | ## Maven Central packages 76 | 77 | The following packages are produced from this repo. 78 | 79 | | Package | Latest version | 80 | | - | - | 81 | | Durable Task - Client | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft/durabletask-client?label=durabletask-client)](https://mvnrepository.com/artifact/com.microsoft/durabletask-client/1.0.0) | 82 | | Durable Task - Azure Functions | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft/durabletask-azure-functions?label=durabletask-azure-functions)](https://mvnrepository.com/artifact/com.microsoft/durabletask-azure-functions/1.0.1) | 83 | 84 | ## Getting started with Azure Functions 85 | 86 | For information about how to get started with Durable Functions for Java, see the [Azure Functions README.md](/azurefunctions/README.md) content. 87 | 88 | ## Contributing 89 | 90 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 91 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 92 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 93 | 94 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 95 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 96 | provided by the bot. You will only need to do this once across all repos using our CLA. 97 | 98 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 99 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 100 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 101 | 102 | ## Trademarks 103 | 104 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 105 | trademarks or logos is subject to and must follow 106 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). 107 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 108 | Any use of third-party trademarks or logos are subject to those third-party's policies. 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | ## Microsoft Support Policy 10 | 11 | Support for this project is limited to the resources listed above. 12 | -------------------------------------------------------------------------------- /azdevops-pipeline/build-release-artifacts.yml: -------------------------------------------------------------------------------- 1 | # Gradle 2 | # Build your Java project and run tests with Gradle using a Gradle wrapper script. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/java 5 | 6 | trigger: none 7 | 8 | pool: 9 | vmImage: ubuntu-latest 10 | 11 | steps: 12 | - checkout: self 13 | 14 | - task: Gradle@3 15 | inputs: 16 | # Specifies the working directory to run the Gradle build. The task uses the repository root directory if the working directory is not specified. 17 | workingDirectory: '' 18 | # Specifies the gradlew wrapper's location within the repository that will be used for the build. 19 | gradleWrapperFile: 'gradlew' 20 | # Sets the GRADLE_OPTS environment variable, which is used to send command-line arguments to start the JVM. The xmx flag specifies the maximum memory available to the JVM. 21 | gradleOptions: '-Xmx3072m' 22 | javaHomeOption: 'JDKVersion' 23 | jdkVersionOption: '$(jdkVersion)' 24 | jdkArchitectureOption: 'x64' 25 | publishJUnitResults: false 26 | tasks: clean assemble 27 | displayName: Assemble durabletask-client and durabletask-azure-functions 28 | 29 | # the secring.gpg file is required to sign the artifacts, it's generated from GnuPG, and it's stored in the library of the durabletaskframework ADO 30 | - task: DownloadSecureFile@1 31 | name: gpgSecretFile 32 | displayName: 'Download GPG secret file' 33 | inputs: 34 | secureFile: 'secring.gpg' 35 | 36 | - task: Gradle@3 37 | inputs: 38 | workingDirectory: '' 39 | gradleWrapperFile: 'gradlew' 40 | gradleOptions: '-Xmx3072m' 41 | javaHomeOption: 'JDKVersion' 42 | jdkVersionOption: '$(jdkVersion)' 43 | jdkArchitectureOption: 'x64' 44 | tasks: publish 45 | options: '-Psigning.keyId=$(gpgSignKey) -Psigning.password=$(gpgSignPassword) -Psigning.secretKeyRingFile=$(gpgSecretFile.secureFilePath)' 46 | displayName: Publish durabletask-client and durabletask-azure-functions 47 | 48 | - task: CopyFiles@2 49 | displayName: 'Copy publish file to Artifact Staging Directory' 50 | inputs: 51 | SourceFolder: repo/com/microsoft 52 | Contents: '**/*.*' 53 | TargetFolder: $(Build.ArtifactStagingDirectory) 54 | 55 | - task: PublishPipelineArtifact@1 56 | name: PublishPipelineArtifact1 57 | displayName: 'Publish Artifact: Build Outputs' 58 | inputs: 59 | ArtifactName: BuildOutputs 60 | TargetPath: $(Build.ArtifactStagingDirectory) -------------------------------------------------------------------------------- /azurefunctions/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'maven-publish' 4 | id 'signing' 5 | id 'com.github.spotbugs' version '5.2.1' 6 | } 7 | 8 | group 'com.microsoft' 9 | version = '1.5.1' 10 | archivesBaseName = 'durabletask-azure-functions' 11 | 12 | def protocVersion = '3.12.0' 13 | 14 | repositories { 15 | maven { 16 | url "https://oss.sonatype.org/content/repositories/snapshots/" 17 | } 18 | } 19 | 20 | dependencies { 21 | api project(':client') 22 | implementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '3.0.0' 23 | implementation "com.google.protobuf:protobuf-java:${protocVersion}" 24 | compileOnly "com.microsoft.azure.functions:azure-functions-java-spi:1.0.0" 25 | } 26 | 27 | sourceCompatibility = JavaVersion.VERSION_1_8 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | 30 | publishing { 31 | repositories { 32 | maven { 33 | url "file://$project.rootDir/repo" 34 | } 35 | } 36 | publications { 37 | mavenJava(MavenPublication) { 38 | from components.java 39 | artifactId = archivesBaseName 40 | pom { 41 | name = 'Azure Durable Functions SDK for Java' 42 | description = 'This package contains classes, interfaces, and annotations for developing with Azure Durable Functions in Java.' 43 | url = "https://github.com/microsoft/durabletask-java/tree/main/azurefunctions" 44 | licenses { 45 | license { 46 | name = "MIT License" 47 | url = "https://opensource.org/licenses/MIT" 48 | distribution = "repo" 49 | } 50 | } 51 | developers { 52 | developer { 53 | id = "Microsoft" 54 | name = "Microsoft Corporation" 55 | } 56 | } 57 | scm { 58 | connection = "scm:git:https://github.com/microsoft/durabletask-java" 59 | developerConnection = "scm:git:git@github.com:microsoft/durabletask-java" 60 | url = "https://github.com/microsoft/durabletask-java/tree/main/azurefunctions" 61 | } 62 | // use below script to include compile-only dependencies when generated pom file. 63 | // This is pain point when we onboard API docs as the missing compile-only dependencies crash the 64 | // API doc's team onboarding pipeline. 65 | withXml { 66 | project.configurations.compileOnly.allDependencies.each { dependency -> 67 | asNode().dependencies[0].appendNode("dependency").with { 68 | it.appendNode("groupId", dependency.group) 69 | it.appendNode("artifactId", dependency.name) 70 | it.appendNode("version", dependency.version) 71 | it.appendNode("scope", "provided") 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | signing { 81 | required = !project.hasProperty("skipSigning") 82 | sign publishing.publications.mavenJava 83 | } 84 | 85 | java { 86 | withSourcesJar() 87 | withJavadocJar() 88 | } 89 | 90 | spotbugs { 91 | toolVersion = '4.9.2' 92 | effort = 'max' 93 | reportLevel = 'high' 94 | ignoreFailures = true 95 | excludeFilter = file('spotbugs-exclude.xml') 96 | } 97 | 98 | spotbugsMain { 99 | reports { 100 | html { 101 | required = true 102 | stylesheet = 'fancy-hist.xsl' 103 | } 104 | xml { 105 | required = true 106 | } 107 | } 108 | } 109 | 110 | spotbugsTest { 111 | reports { 112 | html { 113 | required = true 114 | stylesheet = 'fancy-hist.xsl' 115 | } 116 | xml { 117 | required = true 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /azurefunctions/spotbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableActivityTrigger.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for 4 | * license information. 5 | */ 6 | 7 | package com.microsoft.durabletask.azurefunctions; 8 | 9 | import com.microsoft.azure.functions.annotation.CustomBinding; 10 | import com.microsoft.azure.functions.annotation.HasImplicitOutput; 11 | 12 | import java.lang.annotation.ElementType; 13 | import java.lang.annotation.Retention; 14 | import java.lang.annotation.RetentionPolicy; 15 | import java.lang.annotation.Target; 16 | 17 | /** 18 | *

19 | * Azure Functions attribute for binding a function parameter to a Durable Task activity input. 20 | *

21 | * The following is an example of an activity trigger function that accepts a String input and returns a String output. 22 | *

23 | *
24 |  * {@literal @}FunctionName("SayHello")
25 |  * public String sayHello(
26 |  *         {@literal @}DurableActivityTrigger(name = "name") String name,
27 |  *         final ExecutionContext context) {
28 |  *     context.getLogger().info("Saying hello to: " + name);
29 |  *     return String.format("Hello %s!", name);
30 |  * }
31 |  * 
32 | * 33 | * @since 2.0.0 34 | */ 35 | @Retention(RetentionPolicy.RUNTIME) 36 | @Target(ElementType.PARAMETER) 37 | @CustomBinding(direction = "in", name = "", type = "activityTrigger") 38 | @HasImplicitOutput 39 | public @interface DurableActivityTrigger { 40 | /** 41 | *

The name of the activity function.

42 | *

If not specified, the function name is used as the name of the activity.

43 | *

This property supports binding parameters.

44 | * 45 | * @return The name of the orchestrator function. 46 | */ 47 | String activity() default ""; 48 | 49 | /** 50 | * The variable name used in function.json. 51 | * 52 | * @return The variable name used in function.json. 53 | */ 54 | String name(); 55 | 56 | /** 57 | *

58 | * Defines how Functions runtime should treat the parameter value. Possible values are: 59 | *

60 | *
    61 | *
  • "": get the value as a string, and try to deserialize to actual parameter type like POJO
  • 62 | *
  • string: always get the value as a string
  • 63 | *
  • binary: get the value as a binary data, and try to deserialize to actual parameter type byte[]
  • 64 | *
65 | * 66 | * @return The dataType which will be used by the Functions runtime. 67 | */ 68 | String dataType() default ""; 69 | } 70 | -------------------------------------------------------------------------------- /azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientInput.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for 4 | * license information. 5 | */ 6 | 7 | package com.microsoft.durabletask.azurefunctions; 8 | 9 | import com.microsoft.azure.functions.annotation.CustomBinding; 10 | 11 | import java.lang.annotation.ElementType; 12 | import java.lang.annotation.Retention; 13 | import java.lang.annotation.RetentionPolicy; 14 | import java.lang.annotation.Target; 15 | 16 | /** 17 | *

18 | * Azure Functions attribute for binding a function parameter to a {@literal @}DurableClientContext object. 19 | *

20 | * The following is an example of an HTTP-trigger function that uses this input binding to start a new 21 | * orchestration instance. 22 | *

23 | *
24 |  * {@literal @}FunctionName("StartHelloCities")
25 |  * public HttpResponseMessage startHelloCities(
26 |  *         {@literal @}HttpTrigger(name = "request", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage{@literal <}Optional{@literal <}String{@literal >}{@literal >} request,
27 |  *         {@literal @}DurableClientInput(name = "durableContext") DurableClientContext durableContext,
28 |  *         final ExecutionContext context) {
29 |  * 
30 |  *     DurableTaskClient client = durableContext.getClient();
31 |  *     String instanceId = client.scheduleNewOrchestrationInstance("HelloCities");
32 |  *     context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId);
33 |  *     return durableContext.createCheckStatusResponse(request, instanceId);
34 |  * }
35 |  * 
36 | * 37 | * @since 2.0.0 38 | */ 39 | @Retention(RetentionPolicy.RUNTIME) 40 | @Target(ElementType.PARAMETER) 41 | @CustomBinding(direction = "in", name = "", type = "durableClient") 42 | public @interface DurableClientInput { 43 | /** 44 | * The variable name used in function.json. 45 | * 46 | * @return The variable name used in function.json. 47 | */ 48 | String name(); 49 | 50 | /** 51 | *

52 | * Defines how Functions runtime should treat the parameter value. Possible values are: 53 | *

54 | *
    55 | *
  • "": get the value as a string, and try to deserialize to actual parameter type like POJO
  • 56 | *
  • string: always get the value as a string
  • 57 | *
  • binary: get the value as a binary data, and try to deserialize to actual parameter type byte[]
  • 58 | *
59 | * 60 | * @return The dataType which will be used by the Functions runtime. 61 | */ 62 | String dataType() default ""; 63 | 64 | /** 65 | *

66 | * Optional. The name of the task hub in which the orchestration data lives. 67 | *

68 | *

69 | * If not specified, the task hub name used by this binding will be the value specified in host.json. 70 | * If a task hub name is not configured in host.json and if the function app is running in the 71 | * Azure Functions hosted service, then task hub name is derived from the function app's name. 72 | * Otherwise, a constant value is used for the task hub name. 73 | *

74 | *

75 | * In general, you should not set a value for the task hub name here unless you intend to configure 76 | * the client to interact with orchestrations in another app. 77 | *

78 | * 79 | * @return The task hub name to use for the client. 80 | */ 81 | String taskHub() default ""; 82 | } 83 | -------------------------------------------------------------------------------- /azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableOrchestrationTrigger.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for 4 | * license information. 5 | */ 6 | 7 | package com.microsoft.durabletask.azurefunctions; 8 | 9 | import com.microsoft.azure.functions.annotation.CustomBinding; 10 | import com.microsoft.azure.functions.annotation.HasImplicitOutput; 11 | 12 | import java.lang.annotation.ElementType; 13 | import java.lang.annotation.Retention; 14 | import java.lang.annotation.RetentionPolicy; 15 | import java.lang.annotation.Target; 16 | 17 | /** 18 | *

19 | * Azure Functions attribute for binding a function parameter to a Durable Task orchestration request. 20 | *

21 | * The following is an example of an orchestrator function that calls three activity functions in sequence. 22 | *

23 | *
24 |  * {@literal @}FunctionName("HelloCities")
25 |  * public String helloCitiesOrchestrator(
26 |  *         {@literal @}DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
27 |  *     String result = "";
28 |  *     result += ctx.callActivity("SayHello", "Tokyo", String.class).await() + ", ";
29 |  *     result += ctx.callActivity("SayHello", "London", String.class).await() + ", ";
30 |  *     result += ctx.callActivity("SayHello", "Seattle", String.class).await();
31 |  *     return result;
32 |  * }
33 |  * 
34 | * 35 | * @since 2.0.0 36 | */ 37 | @Retention(RetentionPolicy.RUNTIME) 38 | @Target(ElementType.PARAMETER) 39 | @CustomBinding(direction = "in", name = "", type = "orchestrationTrigger") 40 | @HasImplicitOutput 41 | public @interface DurableOrchestrationTrigger { 42 | /** 43 | *

The name of the orchestrator function.

44 | *

If not specified, the function name is used as the name of the orchestration.

45 | *

This property supports binding parameters.

46 | * @return The name of the orchestrator function. 47 | */ 48 | String orchestration() default ""; 49 | 50 | /** 51 | * The variable name used in function.json. 52 | * 53 | * @return The variable name used in function.json. 54 | */ 55 | String name(); 56 | 57 | /** 58 | *

59 | * Defines how Functions runtime should treat the parameter value. Possible values are: 60 | *

61 | *
    62 | *
  • "": get the value as a string, and try to deserialize to actual parameter type like POJO
  • 63 | *
  • string: always get the value as a string
  • 64 | *
  • binary: get the value as a binary data, and try to deserialize to actual parameter type byte[]
  • 65 | *
66 | * 67 | * @return The dataType which will be used by the Functions runtime. 68 | */ 69 | String dataType() default "string"; 70 | } 71 | -------------------------------------------------------------------------------- /azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/HttpManagementPayload.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for 4 | * license information. 5 | */ 6 | 7 | package com.microsoft.durabletask.azurefunctions; 8 | 9 | /** 10 | * The class that holds the webhook URLs to manage orchestration instances. 11 | */ 12 | public class HttpManagementPayload { 13 | private final String id; 14 | private final String purgeHistoryDeleteUri; 15 | private final String restartPostUri; 16 | private final String sendEventPostUri; 17 | private final String statusQueryGetUri; 18 | private final String terminatePostUri; 19 | private final String resumePostUri; 20 | private final String suspendPostUri; 21 | 22 | /** 23 | * Creates a {@link HttpManagementPayload} to manage orchestration instances 24 | * 25 | * @param instanceId The ID of the orchestration instance 26 | * @param instanceStatusURL The base webhook url of the orchestration instance 27 | * @param requiredQueryStringParameters The query parameters to include with the http request 28 | */ 29 | public HttpManagementPayload( 30 | String instanceId, 31 | String instanceStatusURL, 32 | String requiredQueryStringParameters) { 33 | this.id = instanceId; 34 | this.purgeHistoryDeleteUri = instanceStatusURL + "?" + requiredQueryStringParameters; 35 | this.restartPostUri = instanceStatusURL + "/restart?" + requiredQueryStringParameters; 36 | this.sendEventPostUri = instanceStatusURL + "/raiseEvent/{eventName}?" + requiredQueryStringParameters; 37 | this.statusQueryGetUri = instanceStatusURL + "?" + requiredQueryStringParameters; 38 | this.terminatePostUri = instanceStatusURL + "/terminate?reason={text}&" + requiredQueryStringParameters; 39 | this.resumePostUri = instanceStatusURL + "/resume?reason={text}&" + requiredQueryStringParameters; 40 | this.suspendPostUri = instanceStatusURL + "/suspend?reason={text}&" + requiredQueryStringParameters; 41 | } 42 | 43 | /** 44 | * Gets the ID of the orchestration instance. 45 | * 46 | * @return The ID of the orchestration instance. 47 | */ 48 | public String getId() { 49 | return this.id; 50 | } 51 | 52 | /** 53 | * Gets the HTTP GET status query endpoint URL. 54 | * 55 | * @return The HTTP URL for fetching the instance status. 56 | */ 57 | public String getStatusQueryGetUri() { 58 | return this.statusQueryGetUri; 59 | } 60 | 61 | /** 62 | * Gets the HTTP POST external event sending endpoint URL. 63 | * 64 | * @return The HTTP URL for posting external event notifications. 65 | */ 66 | public String getSendEventPostUri() { 67 | return this.sendEventPostUri; 68 | } 69 | 70 | /** 71 | * Gets the HTTP POST instance termination endpoint. 72 | * 73 | * @return The HTTP URL for posting instance termination commands. 74 | */ 75 | public String getTerminatePostUri() { 76 | return this.terminatePostUri; 77 | } 78 | 79 | /** 80 | * Gets the HTTP DELETE purge instance history by instance ID endpoint. 81 | * 82 | * @return The HTTP URL for purging instance history by instance ID. 83 | */ 84 | public String getPurgeHistoryDeleteUri() { 85 | return this.purgeHistoryDeleteUri; 86 | } 87 | 88 | /** 89 | * Gets the HTTP POST instance restart endpoint. 90 | * 91 | * @return The HTTP URL for posting instance restart commands. 92 | */ 93 | public String getRestartPostUri() { 94 | return restartPostUri; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/internal/middleware/OrchestrationMiddleware.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for 4 | * license information. 5 | */ 6 | 7 | package com.microsoft.durabletask.azurefunctions.internal.middleware; 8 | 9 | import com.microsoft.azure.functions.internal.spi.middleware.Middleware; 10 | import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareChain; 11 | import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareContext; 12 | import com.microsoft.durabletask.CompositeTaskFailedException; 13 | import com.microsoft.durabletask.DataConverter; 14 | import com.microsoft.durabletask.OrchestrationRunner; 15 | import com.microsoft.durabletask.TaskFailedException; 16 | import com.microsoft.durabletask.interruption.ContinueAsNewInterruption; 17 | import com.microsoft.durabletask.interruption.OrchestratorBlockedException; 18 | 19 | /** 20 | * Durable Function Orchestration Middleware 21 | * 22 | *

This class is internal and is hence not for public use. Its APIs are unstable and can change 23 | * at any time. 24 | */ 25 | public class OrchestrationMiddleware implements Middleware { 26 | 27 | private static final String ORCHESTRATION_TRIGGER = "DurableOrchestrationTrigger"; 28 | 29 | @Override 30 | public void invoke(MiddlewareContext context, MiddlewareChain chain) throws Exception { 31 | String parameterName = context.getParameterName(ORCHESTRATION_TRIGGER); 32 | if (parameterName == null){ 33 | chain.doNext(context); 34 | return; 35 | } 36 | String orchestratorRequestEncodedProtoBytes = (String) context.getParameterValue(parameterName); 37 | String orchestratorOutputEncodedProtoBytes = OrchestrationRunner.loadAndRun(orchestratorRequestEncodedProtoBytes, taskOrchestrationContext -> { 38 | try { 39 | context.updateParameterValue(parameterName, taskOrchestrationContext); 40 | chain.doNext(context); 41 | return context.getReturnValue(); 42 | } catch (Exception e) { 43 | // The OrchestratorBlockedEvent will be wrapped into InvocationTargetException by using reflection to 44 | // invoke method. Thus get the cause to check if it's OrchestratorBlockedEvent. 45 | Throwable cause = e.getCause(); 46 | if (cause instanceof OrchestratorBlockedException) { 47 | throw (OrchestratorBlockedException) cause; 48 | } 49 | // The ContinueAsNewInterruption will be wrapped into InvocationTargetException by using reflection to 50 | // invoke method. Thus get the cause to check if it's ContinueAsNewInterruption. 51 | if (cause instanceof ContinueAsNewInterruption) { 52 | throw (ContinueAsNewInterruption) cause; 53 | } 54 | // Below types of exception are raised by the client sdk, they data should be correctly pass back to 55 | // durable function host. We need to cast them to the correct type so later when build the FailureDetails 56 | // the correct exception data can be saved and pass back. 57 | if (cause instanceof TaskFailedException) { 58 | throw (TaskFailedException) cause; 59 | } 60 | 61 | if (cause instanceof CompositeTaskFailedException) { 62 | throw (CompositeTaskFailedException) cause; 63 | } 64 | 65 | if (cause instanceof DataConverter.DataConverterException) { 66 | throw (DataConverter.DataConverterException) cause; 67 | } 68 | // e will be InvocationTargetException as using reflection, so we wrap it into a RuntimeException, so it 69 | // won't change the current OrchestratorFunction API. We cannot throw the cause which is a Throwable, it 70 | // requires update on OrchestratorFunction API. 71 | throw new RuntimeException("Unexpected failure in the task execution", e); 72 | } 73 | }); 74 | context.updateReturnValue(orchestratorOutputEncodedProtoBytes); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /azurefunctions/src/main/resources/META-INF/services/com.microsoft.azure.functions.internal.spi.middleware.Middleware: -------------------------------------------------------------------------------- 1 | com.microsoft.durabletask.azurefunctions.internal.middleware.OrchestrationMiddleware -------------------------------------------------------------------------------- /azuremanaged/spotbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/AccessTokenCache.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.durabletask.azuremanaged; 5 | 6 | import com.azure.core.credential.TokenCredential; 7 | import com.azure.core.credential.AccessToken; 8 | import com.azure.core.credential.TokenRequestContext; 9 | 10 | import java.time.Duration; 11 | import java.time.OffsetDateTime; 12 | 13 | /** 14 | * Caches access tokens for Azure authentication. 15 | * This class is used by both client and worker components to authenticate with Azure-managed Durable Task Scheduler. 16 | */ 17 | public final class AccessTokenCache { 18 | private final TokenCredential credential; 19 | private final TokenRequestContext context; 20 | private final Duration margin; 21 | private AccessToken cachedToken; 22 | 23 | /** 24 | * Creates a new instance of the AccessTokenCache. 25 | * 26 | * @param credential The token credential to use for obtaining tokens. 27 | * @param context The token request context specifying the scopes. 28 | * @param margin The time margin before token expiration to refresh the token. 29 | */ 30 | public AccessTokenCache(TokenCredential credential, TokenRequestContext context, Duration margin) { 31 | this.credential = credential; 32 | this.context = context; 33 | this.margin = margin; 34 | } 35 | 36 | /** 37 | * Gets a valid access token, refreshing it if necessary. 38 | * 39 | * @return A valid access token. 40 | */ 41 | public AccessToken getToken() { 42 | OffsetDateTime nowWithMargin = OffsetDateTime.now().plus(margin); 43 | 44 | if (cachedToken == null 45 | || cachedToken.getExpiresAt().isBefore(nowWithMargin)) { 46 | this.cachedToken = credential.getToken(context).block(); 47 | } 48 | 49 | return cachedToken; 50 | } 51 | } -------------------------------------------------------------------------------- /azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensions.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.durabletask.azuremanaged; 5 | 6 | import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; 7 | import com.azure.core.credential.TokenCredential; 8 | import io.grpc.Channel; 9 | import java.util.Objects; 10 | import javax.annotation.Nullable; 11 | 12 | /** 13 | * Extension methods for creating DurableTaskClient instances that connect to Azure-managed Durable Task Scheduler. 14 | * This class provides various methods to create and configure clients using either connection strings or explicit parameters. 15 | */ 16 | public final class DurableTaskSchedulerClientExtensions { 17 | 18 | private DurableTaskSchedulerClientExtensions() {} 19 | 20 | /** 21 | * Configures a DurableTaskGrpcClientBuilder to use Azure-managed Durable Task Scheduler with a connection string. 22 | * 23 | * @param builder The builder to configure. 24 | * @param connectionString The connection string for Azure-managed Durable Task Scheduler. 25 | * @throws NullPointerException if builder or connectionString is null 26 | */ 27 | public static void useDurableTaskScheduler( 28 | DurableTaskGrpcClientBuilder builder, 29 | String connectionString) { 30 | Objects.requireNonNull(builder, "builder must not be null"); 31 | Objects.requireNonNull(connectionString, "connectionString must not be null"); 32 | 33 | configureBuilder(builder, 34 | DurableTaskSchedulerClientOptions.fromConnectionString(connectionString)); 35 | } 36 | 37 | /** 38 | * Configures a DurableTaskGrpcClientBuilder to use Azure-managed Durable Task Scheduler with explicit parameters. 39 | * 40 | * @param builder The builder to configure. 41 | * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. 42 | * @param taskHubName The name of the task hub to connect to. 43 | * @param tokenCredential The token credential for authentication, or null for anonymous access. 44 | * @throws NullPointerException if builder, endpoint, or taskHubName is null 45 | */ 46 | public static void useDurableTaskScheduler( 47 | DurableTaskGrpcClientBuilder builder, 48 | String endpoint, 49 | String taskHubName, 50 | @Nullable TokenCredential tokenCredential) { 51 | Objects.requireNonNull(builder, "builder must not be null"); 52 | Objects.requireNonNull(endpoint, "endpoint must not be null"); 53 | Objects.requireNonNull(taskHubName, "taskHubName must not be null"); 54 | 55 | configureBuilder(builder, new DurableTaskSchedulerClientOptions() 56 | .setEndpointAddress(endpoint) 57 | .setTaskHubName(taskHubName) 58 | .setCredential(tokenCredential)); 59 | } 60 | 61 | /** 62 | * Creates a DurableTaskGrpcClientBuilder configured for Azure-managed Durable Task Scheduler using a connection string. 63 | * 64 | * @param connectionString The connection string for Azure-managed Durable Task Scheduler. 65 | * @return A new configured DurableTaskGrpcClientBuilder instance. 66 | * @throws NullPointerException if connectionString is null 67 | */ 68 | public static DurableTaskGrpcClientBuilder createClientBuilder( 69 | String connectionString) { 70 | Objects.requireNonNull(connectionString, "connectionString must not be null"); 71 | return createBuilderFromOptions( 72 | DurableTaskSchedulerClientOptions.fromConnectionString(connectionString)); 73 | } 74 | 75 | /** 76 | * Creates a DurableTaskGrpcClientBuilder configured for Azure-managed Durable Task Scheduler using explicit parameters. 77 | * 78 | * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. 79 | * @param taskHubName The name of the task hub to connect to. 80 | * @param tokenCredential The token credential for authentication, or null for anonymous access. 81 | * @return A new configured DurableTaskGrpcClientBuilder instance. 82 | * @throws NullPointerException if endpoint or taskHubName is null 83 | */ 84 | public static DurableTaskGrpcClientBuilder createClientBuilder( 85 | String endpoint, 86 | String taskHubName, 87 | @Nullable TokenCredential tokenCredential) { 88 | Objects.requireNonNull(endpoint, "endpoint must not be null"); 89 | Objects.requireNonNull(taskHubName, "taskHubName must not be null"); 90 | 91 | return createBuilderFromOptions(new DurableTaskSchedulerClientOptions() 92 | .setEndpointAddress(endpoint) 93 | .setTaskHubName(taskHubName) 94 | .setCredential(tokenCredential)); 95 | } 96 | 97 | // Private helper methods to reduce code duplication 98 | private static DurableTaskGrpcClientBuilder createBuilderFromOptions(DurableTaskSchedulerClientOptions options) { 99 | Channel grpcChannel = options.createGrpcChannel(); 100 | return new DurableTaskGrpcClientBuilder().grpcChannel(grpcChannel); 101 | } 102 | 103 | private static void configureBuilder(DurableTaskGrpcClientBuilder builder, DurableTaskSchedulerClientOptions options) { 104 | Channel grpcChannel = options.createGrpcChannel(); 105 | builder.grpcChannel(grpcChannel); 106 | } 107 | } -------------------------------------------------------------------------------- /azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerWorkerExtensions.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.durabletask.azuremanaged; 5 | 6 | import com.azure.core.credential.TokenCredential; 7 | import com.microsoft.durabletask.DurableTaskGrpcWorkerBuilder; 8 | 9 | import io.grpc.Channel; 10 | import java.util.Objects; 11 | import javax.annotation.Nullable; 12 | 13 | /** 14 | * Extension methods for creating DurableTaskWorker instances that connect to Azure-managed Durable Task Scheduler. 15 | * This class provides various methods to create and configure workers using either connection strings or explicit parameters. 16 | */ 17 | public final class DurableTaskSchedulerWorkerExtensions { 18 | private DurableTaskSchedulerWorkerExtensions() {} 19 | 20 | /** 21 | * Configures a DurableTaskGrpcWorkerBuilder to use Azure-managed Durable Task Scheduler with a connection string. 22 | * 23 | * @param builder The builder to configure. 24 | * @param connectionString The connection string for Azure-managed Durable Task Scheduler. 25 | * @throws NullPointerException if builder or connectionString is null 26 | */ 27 | public static void useDurableTaskScheduler( 28 | DurableTaskGrpcWorkerBuilder builder, 29 | String connectionString) { 30 | Objects.requireNonNull(builder, "builder must not be null"); 31 | Objects.requireNonNull(connectionString, "connectionString must not be null"); 32 | 33 | configureBuilder(builder, 34 | DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString)); 35 | } 36 | 37 | /** 38 | * Configures a DurableTaskGrpcWorkerBuilder to use Azure-managed Durable Task Scheduler with explicit parameters. 39 | * 40 | * @param builder The builder to configure. 41 | * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. 42 | * @param taskHubName The name of the task hub to connect to. 43 | * @param tokenCredential The token credential for authentication, or null for anonymous access. 44 | * @throws NullPointerException if builder, endpoint, or taskHubName is null 45 | */ 46 | public static void useDurableTaskScheduler( 47 | DurableTaskGrpcWorkerBuilder builder, 48 | String endpoint, 49 | String taskHubName, 50 | @Nullable TokenCredential tokenCredential) { 51 | Objects.requireNonNull(builder, "builder must not be null"); 52 | Objects.requireNonNull(endpoint, "endpoint must not be null"); 53 | Objects.requireNonNull(taskHubName, "taskHubName must not be null"); 54 | 55 | configureBuilder(builder, new DurableTaskSchedulerWorkerOptions() 56 | .setEndpointAddress(endpoint) 57 | .setTaskHubName(taskHubName) 58 | .setCredential(tokenCredential)); 59 | } 60 | 61 | /** 62 | * Creates a DurableTaskGrpcWorkerBuilder configured for Azure-managed Durable Task Scheduler using a connection string. 63 | * 64 | * @param connectionString The connection string for Azure-managed Durable Task Scheduler. 65 | * @return A new configured DurableTaskGrpcWorkerBuilder instance. 66 | * @throws NullPointerException if connectionString is null 67 | */ 68 | public static DurableTaskGrpcWorkerBuilder createWorkerBuilder( 69 | String connectionString) { 70 | Objects.requireNonNull(connectionString, "connectionString must not be null"); 71 | return createBuilderFromOptions( 72 | DurableTaskSchedulerWorkerOptions.fromConnectionString(connectionString)); 73 | } 74 | 75 | /** 76 | * Creates a DurableTaskGrpcWorkerBuilder configured for Azure-managed Durable Task Scheduler using explicit parameters. 77 | * 78 | * @param endpoint The endpoint address for Azure-managed Durable Task Scheduler. 79 | * @param taskHubName The name of the task hub to connect to. 80 | * @param tokenCredential The token credential for authentication, or null for anonymous access. 81 | * @return A new configured DurableTaskGrpcWorkerBuilder instance. 82 | * @throws NullPointerException if endpoint or taskHubName is null 83 | */ 84 | public static DurableTaskGrpcWorkerBuilder createWorkerBuilder( 85 | String endpoint, 86 | String taskHubName, 87 | @Nullable TokenCredential tokenCredential) { 88 | Objects.requireNonNull(endpoint, "endpoint must not be null"); 89 | Objects.requireNonNull(taskHubName, "taskHubName must not be null"); 90 | 91 | return createBuilderFromOptions(new DurableTaskSchedulerWorkerOptions() 92 | .setEndpointAddress(endpoint) 93 | .setTaskHubName(taskHubName) 94 | .setCredential(tokenCredential)); 95 | } 96 | 97 | // Private helper methods to reduce code duplication 98 | private static DurableTaskGrpcWorkerBuilder createBuilderFromOptions(DurableTaskSchedulerWorkerOptions options) { 99 | Channel grpcChannel = options.createGrpcChannel(); 100 | return new DurableTaskGrpcWorkerBuilder().grpcChannel(grpcChannel); 101 | } 102 | 103 | private static void configureBuilder(DurableTaskGrpcWorkerBuilder builder, DurableTaskSchedulerWorkerOptions options) { 104 | Channel grpcChannel = options.createGrpcChannel(); 105 | builder.grpcChannel(grpcChannel); 106 | } 107 | } -------------------------------------------------------------------------------- /azuremanaged/src/main/java/com/microsoft/durabletask/azuremanaged/DurableTaskUserAgentUtil.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.durabletask.azuremanaged; 5 | 6 | /** 7 | * Utility class for generating the user agent string for the Durable Task SDK. 8 | */ 9 | public final class DurableTaskUserAgentUtil { 10 | /** 11 | * The name of the SDK used in the user agent string. 12 | */ 13 | private static final String SDK_NAME = "durabletask-java"; 14 | 15 | private DurableTaskUserAgentUtil() { 16 | // Private constructor to prevent instantiation 17 | } 18 | 19 | /** 20 | * Generates the user agent string for the Durable Task SDK based on a fixed name and the package version. 21 | * 22 | * @return The user agent string. 23 | */ 24 | public static String getUserAgent() { 25 | return String.format("%s/%s", SDK_NAME, VersionInfo.VERSION); 26 | } 27 | } -------------------------------------------------------------------------------- /azuremanaged/src/main/resources/VersionInfo.java.template: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.durabletask.azuremanaged; 5 | 6 | /** 7 | * Auto-generated version information for the Durable Task SDK. 8 | * This file is generated during the build process. 9 | */ 10 | public final class VersionInfo { 11 | /** 12 | * The version of the SDK. 13 | */ 14 | public static final String VERSION = "${version}"; 15 | 16 | private VersionInfo() { 17 | // Private constructor to prevent instantiation 18 | } 19 | } -------------------------------------------------------------------------------- /azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/AccessTokenCacheTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.durabletask.azuremanaged; 5 | 6 | import com.azure.core.credential.AccessToken; 7 | import com.azure.core.credential.TokenCredential; 8 | import com.azure.core.credential.TokenRequestContext; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import reactor.core.publisher.Mono; 16 | 17 | import java.time.Duration; 18 | import java.time.OffsetDateTime; 19 | 20 | import static org.junit.jupiter.api.Assertions.*; 21 | import static org.mockito.ArgumentMatchers.any; 22 | import static org.mockito.Mockito.*; 23 | 24 | /** 25 | * Unit tests for {@link AccessTokenCache}. 26 | */ 27 | @ExtendWith(MockitoExtension.class) 28 | public class AccessTokenCacheTest { 29 | 30 | @Mock 31 | private TokenCredential mockCredential; 32 | 33 | private TokenRequestContext context; 34 | private Duration margin; 35 | private AccessTokenCache tokenCache; 36 | 37 | @BeforeEach 38 | public void setup() { 39 | context = new TokenRequestContext().addScopes("https://durabletask.io/.default"); 40 | margin = Duration.ofMinutes(5); 41 | tokenCache = new AccessTokenCache(mockCredential, context, margin); 42 | } 43 | 44 | @Test 45 | @DisplayName("getToken should fetch a new token when cache is empty") 46 | public void getToken_WhenCacheEmpty_FetchesNewToken() { 47 | // Arrange 48 | AccessToken expectedToken = new AccessToken("token1", OffsetDateTime.now().plusHours(1)); 49 | when(mockCredential.getToken(any(TokenRequestContext.class))).thenReturn(Mono.just(expectedToken)); 50 | 51 | // Act 52 | AccessToken result = tokenCache.getToken(); 53 | 54 | // Assert 55 | assertEquals(expectedToken, result); 56 | verify(mockCredential, times(1)).getToken(context); 57 | } 58 | 59 | @Test 60 | @DisplayName("getToken should reuse cached token when not expired") 61 | public void getToken_WhenTokenNotExpired_ReusesCachedToken() { 62 | // Arrange 63 | AccessToken expectedToken = new AccessToken("token1", OffsetDateTime.now().plusHours(1)); 64 | when(mockCredential.getToken(any(TokenRequestContext.class))).thenReturn(Mono.just(expectedToken)); 65 | 66 | // Act 67 | tokenCache.getToken(); // First call to cache the token 68 | AccessToken result = tokenCache.getToken(); // Second call should use cached token 69 | 70 | // Assert 71 | assertEquals(expectedToken, result); 72 | verify(mockCredential, times(1)).getToken(context); // Should only be called once 73 | } 74 | 75 | @Test 76 | @DisplayName("getToken should fetch a new token when current token is expired") 77 | public void getToken_WhenTokenExpired_FetchesNewToken() { 78 | // Arrange 79 | AccessToken expiredToken = new AccessToken("expired", OffsetDateTime.now().minusMinutes(1)); 80 | AccessToken newToken = new AccessToken("new", OffsetDateTime.now().plusHours(1)); 81 | 82 | when(mockCredential.getToken(any(TokenRequestContext.class))) 83 | .thenReturn(Mono.just(expiredToken)) 84 | .thenReturn(Mono.just(newToken)); 85 | 86 | // Act 87 | AccessToken firstResult = tokenCache.getToken(); 88 | AccessToken secondResult = tokenCache.getToken(); 89 | 90 | // Assert 91 | assertEquals(expiredToken, firstResult); 92 | assertEquals(newToken, secondResult); 93 | verify(mockCredential, times(2)).getToken(context); 94 | } 95 | 96 | @Test 97 | @DisplayName("getToken should fetch a new token when current token is about to expire within margin") 98 | public void getToken_WhenTokenAboutToExpire_FetchesNewToken() { 99 | // Arrange 100 | AccessToken expiringToken = new AccessToken("expiring", OffsetDateTime.now().plus(margin.minusMinutes(1))); 101 | AccessToken newToken = new AccessToken("new", OffsetDateTime.now().plusHours(1)); 102 | 103 | when(mockCredential.getToken(any(TokenRequestContext.class))) 104 | .thenReturn(Mono.just(expiringToken)) 105 | .thenReturn(Mono.just(newToken)); 106 | 107 | // Act 108 | AccessToken firstResult = tokenCache.getToken(); 109 | AccessToken secondResult = tokenCache.getToken(); 110 | 111 | // Assert 112 | assertEquals(expiringToken, firstResult); 113 | assertEquals(newToken, secondResult); 114 | verify(mockCredential, times(2)).getToken(context); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientExtensionsUserAgentTest.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.durabletask.azuremanaged; 2 | 3 | import com.microsoft.durabletask.DurableTaskClient; 4 | import com.microsoft.durabletask.DurableTaskGrpcClientBuilder; 5 | import com.microsoft.durabletask.NewOrchestrationInstanceOptions; 6 | import io.grpc.*; 7 | import io.grpc.stub.ServerCalls; 8 | import io.grpc.stub.StreamObserver; 9 | import io.grpc.netty.NettyServerBuilder; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.io.IOException; 15 | import java.util.concurrent.atomic.AtomicReference; 16 | 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | public class DurableTaskSchedulerClientExtensionsUserAgentTest { 20 | private Server server; 21 | private Channel channel; 22 | private final AtomicReference capturedUserAgent = new AtomicReference<>(); 23 | private static final String EXPECTED_USER_AGENT_PREFIX = "durabletask-java/"; 24 | 25 | // Dummy gRPC service definition 26 | public static class DummyService implements io.grpc.BindableService { 27 | @Override 28 | public ServerServiceDefinition bindService() { 29 | return ServerServiceDefinition.builder("TaskHubSidecarService") 30 | .addMethod( 31 | MethodDescriptor.newBuilder() 32 | .setType(MethodDescriptor.MethodType.UNARY) 33 | .setFullMethodName(MethodDescriptor.generateFullMethodName("TaskHubSidecarService", "StartInstance")) 34 | .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(com.google.protobuf.Empty.getDefaultInstance())) 35 | .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(com.google.protobuf.Empty.getDefaultInstance())) 36 | .build(), 37 | ServerCalls.asyncUnaryCall( 38 | new ServerCalls.UnaryMethod() { 39 | @Override 40 | public void invoke(com.google.protobuf.Empty request, StreamObserver responseObserver) { 41 | // Mock response for StartInstance 42 | responseObserver.onNext(com.google.protobuf.Empty.getDefaultInstance()); 43 | responseObserver.onCompleted(); 44 | } 45 | } 46 | ) 47 | ) 48 | .build(); 49 | } 50 | } 51 | 52 | @BeforeEach 53 | public void setUp() throws IOException { 54 | // Use NettyServerBuilder to expose the server via HTTP 55 | server = NettyServerBuilder.forPort(0) 56 | .addService(new DummyService()) // Register DummyService 57 | .intercept(new ServerInterceptor() { 58 | @Override 59 | public ServerCall.Listener interceptCall( 60 | ServerCall call, Metadata headers, ServerCallHandler next) { 61 | String userAgent = headers.get(Metadata.Key.of("x-user-agent", Metadata.ASCII_STRING_MARSHALLER)); 62 | capturedUserAgent.set(userAgent); 63 | return next.startCall(call, headers); 64 | } 65 | }) 66 | .directExecutor() 67 | .build() 68 | .start(); 69 | int port = server.getPort(); 70 | String endpoint = "localhost:" + port; 71 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); 72 | options.setEndpointAddress(endpoint); 73 | options.setTaskHubName("testHub"); 74 | options.setAllowInsecureCredentials(true); // Netty is insecure for localhost 75 | channel = options.createGrpcChannel(); 76 | } 77 | 78 | @AfterEach 79 | public void tearDown() { 80 | if (server != null) server.shutdownNow(); 81 | if (channel != null && channel instanceof ManagedChannel) { 82 | ((ManagedChannel) channel).shutdownNow(); 83 | } 84 | } 85 | 86 | @Test 87 | public void testUserAgentHeaderIsSet() { 88 | DurableTaskGrpcClientBuilder builder = new DurableTaskGrpcClientBuilder(); 89 | builder.grpcChannel(channel); 90 | DurableTaskClient client = builder.build(); 91 | 92 | // Schedule a new orchestration instance 93 | String instanceId = client.scheduleNewOrchestrationInstance( 94 | "TestOrchestration", 95 | new NewOrchestrationInstanceOptions().setInput("TestInput")); 96 | 97 | // Make a dummy call to trigger the request 98 | String userAgent = capturedUserAgent.get(); 99 | assertNotNull(userAgent, "X-User-Agent header should be set"); 100 | assertTrue(userAgent.startsWith(EXPECTED_USER_AGENT_PREFIX), "X-User-Agent should start with durabletask-java/"); 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /azuremanaged/src/test/java/com/microsoft/durabletask/azuremanaged/DurableTaskSchedulerClientOptionsTest.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.durabletask.azuremanaged; 2 | 3 | import com.azure.core.credential.TokenCredential; 4 | import io.grpc.Channel; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.jupiter.MockitoExtension; 10 | 11 | import java.time.Duration; 12 | 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | /** 16 | * Unit tests for {@link DurableTaskSchedulerClientOptions}. 17 | */ 18 | @ExtendWith(MockitoExtension.class) 19 | public class DurableTaskSchedulerClientOptionsTest { 20 | 21 | private static final String VALID_CONNECTION_STRING = 22 | "Endpoint=https://example.com;Authentication=ManagedIdentity;TaskHub=myTaskHub"; 23 | private static final String VALID_ENDPOINT = "https://example.com"; 24 | private static final String VALID_TASKHUB = "myTaskHub"; 25 | private static final String CUSTOM_RESOURCE_ID = "https://custom.resource"; 26 | private static final Duration CUSTOM_REFRESH_MARGIN = Duration.ofMinutes(10); 27 | 28 | @Mock 29 | private TokenCredential mockCredential; 30 | 31 | @Test 32 | @DisplayName("fromConnectionString should create valid options") 33 | public void fromConnectionString_CreatesValidOptions() { 34 | // Act 35 | DurableTaskSchedulerClientOptions options = 36 | DurableTaskSchedulerClientOptions.fromConnectionString(VALID_CONNECTION_STRING); 37 | 38 | // Assert 39 | assertNotNull(options); 40 | assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); 41 | assertEquals(VALID_TASKHUB, options.getTaskHubName()); 42 | } 43 | 44 | @Test 45 | @DisplayName("setEndpointAddress should update endpoint") 46 | public void setEndpointAddress_UpdatesEndpoint() { 47 | // Arrange 48 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); 49 | 50 | // Act 51 | options.setEndpointAddress(VALID_ENDPOINT); 52 | 53 | // Assert 54 | assertEquals(VALID_ENDPOINT, options.getEndpointAddress()); 55 | } 56 | 57 | @Test 58 | @DisplayName("setTaskHubName should update task hub name") 59 | public void setTaskHubName_UpdatesTaskHubName() { 60 | // Arrange 61 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); 62 | 63 | // Act 64 | options.setTaskHubName(VALID_TASKHUB); 65 | 66 | // Assert 67 | assertEquals(VALID_TASKHUB, options.getTaskHubName()); 68 | } 69 | 70 | @Test 71 | @DisplayName("setCredential should update credential") 72 | public void setCredential_UpdatesCredential() { 73 | // Arrange 74 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); 75 | 76 | // Act 77 | options.setCredential(mockCredential); 78 | 79 | // Assert 80 | assertEquals(mockCredential, options.getCredential()); 81 | } 82 | 83 | @Test 84 | @DisplayName("setResourceId should update resource ID") 85 | public void setResourceId_UpdatesResourceId() { 86 | // Arrange 87 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); 88 | 89 | // Act 90 | options.setResourceId(CUSTOM_RESOURCE_ID); 91 | 92 | // Assert 93 | assertEquals(CUSTOM_RESOURCE_ID, options.getResourceId()); 94 | } 95 | 96 | @Test 97 | @DisplayName("setAllowInsecureCredentials should update insecure credentials flag") 98 | public void setAllowInsecureCredentials_UpdatesFlag() { 99 | // Arrange 100 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); 101 | 102 | // Act 103 | options.setAllowInsecureCredentials(true); 104 | 105 | // Assert 106 | assertTrue(options.isAllowInsecureCredentials()); 107 | } 108 | 109 | @Test 110 | @DisplayName("setTokenRefreshMargin should update token refresh margin") 111 | public void setTokenRefreshMargin_UpdatesMargin() { 112 | // Arrange 113 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); 114 | 115 | // Act 116 | options.setTokenRefreshMargin(CUSTOM_REFRESH_MARGIN); 117 | 118 | // Assert 119 | assertEquals(CUSTOM_REFRESH_MARGIN, options.getTokenRefreshMargin()); 120 | } 121 | 122 | @Test 123 | @DisplayName("createGrpcChannel should create valid channel") 124 | public void createGrpcChannel_CreatesValidChannel() { 125 | // Arrange 126 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() 127 | .setEndpointAddress(VALID_ENDPOINT) 128 | .setTaskHubName(VALID_TASKHUB); 129 | 130 | // Act 131 | Channel channel = options.createGrpcChannel(); 132 | 133 | // Assert 134 | assertNotNull(channel); 135 | } 136 | 137 | @Test 138 | @DisplayName("createGrpcChannel should handle endpoint without protocol") 139 | public void createGrpcChannel_HandlesEndpointWithoutProtocol() { 140 | // Arrange 141 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions() 142 | .setEndpointAddress("example.com") 143 | .setTaskHubName(VALID_TASKHUB); 144 | 145 | // Act 146 | Channel channel = options.createGrpcChannel(); 147 | 148 | // Assert 149 | assertNotNull(channel); 150 | } 151 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | subprojects { 2 | repositories { 3 | mavenLocal() 4 | mavenCentral() 5 | } 6 | } -------------------------------------------------------------------------------- /client/spotbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/CompositeTaskFailedException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Exception that gets thrown when multiple {@link Task}s for an activity or sub-orchestration fails with an 10 | * unhandled exception. 11 | *

12 | * Detailed information associated with each task failure can be retrieved using the {@link #getExceptions()} 13 | * method. 14 | */ 15 | public class CompositeTaskFailedException extends RuntimeException { 16 | private final List exceptions; 17 | 18 | CompositeTaskFailedException() { 19 | this.exceptions = new ArrayList<>(); 20 | } 21 | 22 | CompositeTaskFailedException(List exceptions) { 23 | this.exceptions = exceptions; 24 | } 25 | 26 | CompositeTaskFailedException(String message, List exceptions) { 27 | super(message); 28 | this.exceptions = exceptions; 29 | } 30 | 31 | CompositeTaskFailedException(String message, Throwable cause, List exceptions) { 32 | super(message, cause); 33 | this.exceptions = exceptions; 34 | } 35 | 36 | CompositeTaskFailedException(Throwable cause, List exceptions) { 37 | super(cause); 38 | this.exceptions = exceptions; 39 | } 40 | 41 | CompositeTaskFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, List exceptions) { 42 | super(message, cause, enableSuppression, writableStackTrace); 43 | this.exceptions = exceptions; 44 | } 45 | 46 | /** 47 | * Gets a list of exceptions that occurred during execution of a group of {@link Task} 48 | * These exceptions include details of the task failure and exception information 49 | * 50 | * @return a list of exceptions 51 | */ 52 | public List getExceptions() { 53 | return new ArrayList<>(this.exceptions); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/DataConverter.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import com.google.protobuf.Timestamp; 6 | 7 | import javax.annotation.Nullable; 8 | import java.time.Instant; 9 | import java.time.temporal.ChronoUnit; 10 | 11 | /** 12 | * Interface for serializing and deserializing data that gets passed to and from orchestrators and activities. 13 | *

14 | * Implementations of this abstract class are free to use any serialization method. Currently, only strings are 15 | * supported as the serialized representation of data. Byte array payloads and streams are not supported by this 16 | * abstraction. Note that these methods all accept null values, in which case the return value should also be null. 17 | */ 18 | public interface DataConverter { 19 | /** 20 | * Serializes the input into a text representation. 21 | * 22 | * @param value the value to be serialized 23 | * @return a serialized text representation of the value or null if the value is null 24 | */ 25 | @Nullable 26 | String serialize(@Nullable Object value); 27 | 28 | /** 29 | * Deserializes the given text data into an object of the specified type. 30 | * 31 | * @param data the text data to deserialize into an object 32 | * @param target the target class to deserialize the input into 33 | * @param the generic parameter type representing the target class to deserialize the input into 34 | * @return a deserialized object of type T 35 | * @throws DataConverterException if the text data cannot be deserialized 36 | */ 37 | @Nullable 38 | T deserialize(@Nullable String data, Class target); 39 | 40 | // Data conversion errors are expected to be unrecoverable in most cases, hence an unchecked runtime exception 41 | class DataConverterException extends RuntimeException { 42 | public DataConverterException(String message, Throwable cause) { 43 | super(message, cause); 44 | } 45 | } 46 | 47 | static Instant getInstantFromTimestamp(Timestamp ts) { 48 | if (ts == null) { 49 | return null; 50 | } 51 | 52 | // We don't include nanoseconds because of serialization round-trip issues 53 | return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()).truncatedTo(ChronoUnit.MILLIS); 54 | } 55 | 56 | static Timestamp getTimestampFromInstant(Instant instant) { 57 | if (instant == null) { 58 | return null; 59 | } 60 | 61 | return Timestamp.newBuilder() 62 | .setSeconds(instant.getEpochSecond()) 63 | .setNanos(instant.getNano()) 64 | .build(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClientBuilder.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import io.grpc.Channel; 6 | 7 | /** 8 | * Builder class for constructing new {@link DurableTaskClient} objects that communicate with a sidecar process 9 | * over gRPC. 10 | */ 11 | public final class DurableTaskGrpcClientBuilder { 12 | DataConverter dataConverter; 13 | int port; 14 | Channel channel; 15 | 16 | /** 17 | * Sets the {@link DataConverter} to use for converting serializable data payloads. 18 | * 19 | * @param dataConverter the {@link DataConverter} to use for converting serializable data payloads 20 | * @return this builder object 21 | */ 22 | public DurableTaskGrpcClientBuilder dataConverter(DataConverter dataConverter) { 23 | this.dataConverter = dataConverter; 24 | return this; 25 | } 26 | 27 | /** 28 | * Sets the gRPC channel to use for communicating with the sidecar process. 29 | *

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

34 | * If not specified, a gRPC channel will be created automatically for each constructed 35 | * {@link DurableTaskClient}. 36 | * 37 | * @param channel the gRPC channel to use 38 | * @return this builder object 39 | */ 40 | public DurableTaskGrpcClientBuilder grpcChannel(Channel channel) { 41 | this.channel = channel; 42 | return this; 43 | } 44 | 45 | /** 46 | * Sets the gRPC endpoint port to connect to. If not specified, the default Durable Task port number will be used. 47 | * 48 | * @param port the gRPC endpoint port to connect to 49 | * @return this builder object 50 | */ 51 | public DurableTaskGrpcClientBuilder port(int port) { 52 | this.port = port; 53 | return this; 54 | } 55 | 56 | /** 57 | * Initializes a new {@link DurableTaskClient} object with the settings specified in the current builder object. 58 | * @return a new {@link DurableTaskClient} object 59 | */ 60 | public DurableTaskClient build() { 61 | return new DurableTaskGrpcClient(this); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorkerBuilder.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import io.grpc.Channel; 6 | 7 | import java.time.Duration; 8 | import java.util.HashMap; 9 | 10 | /** 11 | * Builder object for constructing customized {@link DurableTaskGrpcWorker} instances. 12 | */ 13 | public final class DurableTaskGrpcWorkerBuilder { 14 | final HashMap orchestrationFactories = new HashMap<>(); 15 | final HashMap activityFactories = new HashMap<>(); 16 | int port; 17 | Channel channel; 18 | DataConverter dataConverter; 19 | Duration maximumTimerInterval; 20 | 21 | /** 22 | * Adds an orchestration factory to be used by the constructed {@link DurableTaskGrpcWorker}. 23 | * 24 | * @param factory an orchestration factory to be used by the constructed {@link DurableTaskGrpcWorker} 25 | * @return this builder object 26 | */ 27 | public DurableTaskGrpcWorkerBuilder addOrchestration(TaskOrchestrationFactory factory) { 28 | String key = factory.getName(); 29 | if (key == null || key.length() == 0) { 30 | throw new IllegalArgumentException("A non-empty task orchestration name is required."); 31 | } 32 | 33 | if (this.orchestrationFactories.containsKey(key)) { 34 | throw new IllegalArgumentException( 35 | String.format("A task orchestration factory named %s is already registered.", key)); 36 | } 37 | 38 | this.orchestrationFactories.put(key, factory); 39 | return this; 40 | } 41 | 42 | /** 43 | * Adds an activity factory to be used by the constructed {@link DurableTaskGrpcWorker}. 44 | * 45 | * @param factory an activity factory to be used by the constructed {@link DurableTaskGrpcWorker} 46 | * @return this builder object 47 | */ 48 | public DurableTaskGrpcWorkerBuilder addActivity(TaskActivityFactory factory) { 49 | // TODO: Input validation 50 | String key = factory.getName(); 51 | if (key == null || key.length() == 0) { 52 | throw new IllegalArgumentException("A non-empty task activity name is required."); 53 | } 54 | 55 | if (this.activityFactories.containsKey(key)) { 56 | throw new IllegalArgumentException( 57 | String.format("A task activity factory named %s is already registered.", key)); 58 | } 59 | 60 | this.activityFactories.put(key, factory); 61 | return this; 62 | } 63 | 64 | /** 65 | * Sets the gRPC channel to use for communicating with the sidecar process. 66 | *

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

71 | * If not specified, a gRPC channel will be created automatically for each constructed 72 | * {@link DurableTaskGrpcWorker}. 73 | * 74 | * @param channel the gRPC channel to use 75 | * @return this builder object 76 | */ 77 | public DurableTaskGrpcWorkerBuilder grpcChannel(Channel channel) { 78 | this.channel = channel; 79 | return this; 80 | } 81 | 82 | /** 83 | * Sets the gRPC endpoint port to connect to. If not specified, the default Durable Task port number will be used. 84 | * 85 | * @param port the gRPC endpoint port to connect to 86 | * @return this builder object 87 | */ 88 | public DurableTaskGrpcWorkerBuilder port(int port) { 89 | this.port = port; 90 | return this; 91 | } 92 | 93 | /** 94 | * Sets the {@link DataConverter} to use for converting serializable data payloads. 95 | * 96 | * @param dataConverter the {@link DataConverter} to use for converting serializable data payloads 97 | * @return this builder object 98 | */ 99 | public DurableTaskGrpcWorkerBuilder dataConverter(DataConverter dataConverter) { 100 | this.dataConverter = dataConverter; 101 | return this; 102 | } 103 | 104 | /** 105 | * Sets the maximum timer interval. If not specified, the default maximum timer interval duration will be used. 106 | * The default maximum timer interval duration is 3 days. 107 | * 108 | * @param maximumTimerInterval the maximum timer interval 109 | * @return this builder object 110 | */ 111 | public DurableTaskGrpcWorkerBuilder maximumTimerInterval(Duration maximumTimerInterval) { 112 | this.maximumTimerInterval = maximumTimerInterval; 113 | return this; 114 | } 115 | 116 | /** 117 | * Initializes a new {@link DurableTaskGrpcWorker} object with the settings specified in the current builder object. 118 | * @return a new {@link DurableTaskGrpcWorker} object 119 | */ 120 | public DurableTaskGrpcWorker build() { 121 | return new DurableTaskGrpcWorker(this); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/Helpers.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | import java.time.Duration; 8 | 9 | final class Helpers { 10 | final static Duration maxDuration = Duration.ofSeconds(Long.MAX_VALUE, 999999999L); 11 | 12 | static @Nonnull V throwIfArgumentNull(@Nullable V argValue, String argName) { 13 | if (argValue == null) { 14 | throw new IllegalArgumentException("The argument '" + argName + "' was null."); 15 | } 16 | 17 | return argValue; 18 | } 19 | 20 | static @Nonnull String throwIfArgumentNullOrWhiteSpace(String argValue, String argName) { 21 | throwIfArgumentNull(argValue, argName); 22 | if (argValue.trim().length() == 0){ 23 | throw new IllegalArgumentException("The argument '" + argName + "' was empty or contained only whitespace."); 24 | } 25 | 26 | return argValue; 27 | } 28 | 29 | static void throwIfOrchestratorComplete(boolean isComplete) { 30 | if (isComplete) { 31 | throw new IllegalStateException("The orchestrator has already completed"); 32 | } 33 | } 34 | 35 | static boolean isInfiniteTimeout(Duration timeout) { 36 | return timeout == null || timeout.isNegative() || timeout.equals(maxDuration); 37 | } 38 | 39 | static double powExact(double base, double exponent) throws ArithmeticException { 40 | if (base == 0.0) { 41 | return 0.0; 42 | } 43 | 44 | double result = Math.pow(base, exponent); 45 | 46 | if (result == Double.POSITIVE_INFINITY) { 47 | throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY"); 48 | } else if (result == Double.NEGATIVE_INFINITY) { 49 | throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY"); 50 | } else if (Double.compare(-0.0f, result) == 0) { 51 | throw new ArithmeticException("Double overflow resulting in negative zero"); 52 | } else if (Double.compare(+0.0f, result) == 0) { 53 | throw new ArithmeticException("Double overflow resulting in positive zero"); 54 | } 55 | 56 | return result; 57 | } 58 | 59 | static boolean isNullOrEmpty(String s) { 60 | return s == null || s.isEmpty(); 61 | } 62 | 63 | // Cannot be instantiated 64 | private Helpers() { 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/JacksonDataConverter.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.json.JsonMapper; 8 | 9 | /** 10 | * An implementation of {@link DataConverter} that uses Jackson APIs for data serialization. 11 | */ 12 | public final class JacksonDataConverter implements DataConverter { 13 | // Static singletons are recommended by the Jackson documentation 14 | private static final ObjectMapper jsonObjectMapper = JsonMapper.builder() 15 | .findAndAddModules() 16 | .build(); 17 | 18 | @Override 19 | public String serialize(Object value) { 20 | if (value == null) { 21 | return null; 22 | } 23 | 24 | try { 25 | return jsonObjectMapper.writeValueAsString(value); 26 | } catch (JsonProcessingException e) { 27 | throw new DataConverterException( 28 | String.format("Failed to serialize argument of type '%s'. Detailed error message: %s", value.getClass().getName(), e.getMessage()), 29 | e); 30 | } 31 | } 32 | 33 | @Override 34 | public T deserialize(String jsonText, Class targetType) { 35 | if (jsonText == null || jsonText.length() == 0 || targetType == Void.class) { 36 | return null; 37 | } 38 | 39 | try { 40 | return jsonObjectMapper.readValue(jsonText, targetType); 41 | } catch (JsonProcessingException e) { 42 | throw new DataConverterException(String.format("Failed to deserialize the JSON text to %s. Detailed error message: %s", targetType.getName(), e.getMessage()), e); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/NewOrchestrationInstanceOptions.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import java.time.Instant; 6 | 7 | /** 8 | * Options for starting a new instance of an orchestration. 9 | */ 10 | public final class NewOrchestrationInstanceOptions { 11 | private String version; 12 | private String instanceId; 13 | private Object input; 14 | private Instant startTime; 15 | 16 | /** 17 | * Default constructor for the {@link NewOrchestrationInstanceOptions} class. 18 | */ 19 | public NewOrchestrationInstanceOptions() { 20 | } 21 | 22 | /** 23 | * Sets the version of the orchestration to start. 24 | * 25 | * @param version the user-defined version of the orchestration 26 | * @return this {@link NewOrchestrationInstanceOptions} object 27 | */ 28 | public NewOrchestrationInstanceOptions setVersion(String version) { 29 | this.version = version; 30 | return this; 31 | } 32 | 33 | /** 34 | * Sets the instance ID of the orchestration to start. 35 | *

36 | * If no instance ID is configured, the orchestration will be created with a randomly generated instance ID. 37 | * 38 | * @param instanceId the ID of the new orchestration instance 39 | * @return this {@link NewOrchestrationInstanceOptions} object 40 | */ 41 | public NewOrchestrationInstanceOptions setInstanceId(String instanceId) { 42 | this.instanceId = instanceId; 43 | return this; 44 | } 45 | 46 | /** 47 | * Sets the input of the orchestration to start. 48 | *

49 | * There are no restrictions on the type of inputs that can be used except that they must be serializable using 50 | * the {@link DataConverter} that was configured for the {@link DurableTaskClient} at creation time. 51 | * 52 | * @param input the input of the new orchestration instance 53 | * @return this {@link NewOrchestrationInstanceOptions} object 54 | */ 55 | public NewOrchestrationInstanceOptions setInput(Object input) { 56 | this.input = input; 57 | return this; 58 | } 59 | 60 | /** 61 | * Sets the start time of the new orchestration instance. 62 | *

63 | * By default, new orchestration instances start executing immediately. This method can be used 64 | * to start them at a specific time in the future. 65 | * 66 | * @param startTime the start time of the new orchestration instance 67 | * @return this {@link NewOrchestrationInstanceOptions} object 68 | */ 69 | public NewOrchestrationInstanceOptions setStartTime(Instant startTime) { 70 | this.startTime = startTime; 71 | return this; 72 | } 73 | 74 | /** 75 | * Gets the user-specified version of the new orchestration. 76 | * 77 | * @return the user-specified version of the new orchestration. 78 | */ 79 | public String getVersion() { 80 | return this.version; 81 | } 82 | 83 | /** 84 | * Gets the instance ID of the new orchestration. 85 | * 86 | * @return the instance ID of the new orchestration. 87 | */ 88 | public String getInstanceId() { 89 | return this.instanceId; 90 | } 91 | 92 | /** 93 | * Gets the input of the new orchestration. 94 | * 95 | * @return the input of the new orchestration. 96 | */ 97 | public Object getInput() { 98 | return this.input; 99 | } 100 | 101 | /** 102 | * Gets the configured start time of the new orchestration instance. 103 | * 104 | * @return the configured start time of the new orchestration instance. 105 | */ 106 | public Instant getStartTime() { 107 | return this.startTime; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/NonDeterministicOrchestratorException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | final class NonDeterministicOrchestratorException extends RuntimeException { 6 | public NonDeterministicOrchestratorException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/OrchestrationRuntimeStatus.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.*; 6 | import static com.microsoft.durabletask.implementation.protobuf.OrchestratorService.OrchestrationStatus.*; 7 | 8 | /** 9 | * Enum describing the runtime status of the orchestration. 10 | */ 11 | public enum OrchestrationRuntimeStatus { 12 | /** 13 | * The orchestration started running. 14 | */ 15 | RUNNING, 16 | 17 | /** 18 | * The orchestration completed normally. 19 | */ 20 | COMPLETED, 21 | 22 | /** 23 | * The orchestration is transitioning into a new instance. 24 | *

25 | * This status value is obsolete and exists only for compatibility reasons. 26 | */ 27 | CONTINUED_AS_NEW, 28 | 29 | /** 30 | * The orchestration completed with an unhandled exception. 31 | */ 32 | FAILED, 33 | 34 | /** 35 | * The orchestration canceled gracefully. 36 | *

37 | * The Canceled status is not currently used and exists only for compatibility reasons. 38 | */ 39 | CANCELED, 40 | 41 | /** 42 | * The orchestration was abruptly terminated via a management API call. 43 | */ 44 | TERMINATED, 45 | 46 | /** 47 | * The orchestration was scheduled but hasn't started running. 48 | */ 49 | PENDING, 50 | 51 | /** 52 | * The orchestration is in a suspended state. 53 | */ 54 | SUSPENDED; 55 | 56 | static OrchestrationRuntimeStatus fromProtobuf(OrchestrationStatus status) { 57 | switch (status) { 58 | case ORCHESTRATION_STATUS_RUNNING: 59 | return RUNNING; 60 | case ORCHESTRATION_STATUS_COMPLETED: 61 | return COMPLETED; 62 | case ORCHESTRATION_STATUS_CONTINUED_AS_NEW: 63 | return CONTINUED_AS_NEW; 64 | case ORCHESTRATION_STATUS_FAILED: 65 | return FAILED; 66 | case ORCHESTRATION_STATUS_CANCELED: 67 | return CANCELED; 68 | case ORCHESTRATION_STATUS_TERMINATED: 69 | return TERMINATED; 70 | case ORCHESTRATION_STATUS_PENDING: 71 | return PENDING; 72 | case ORCHESTRATION_STATUS_SUSPENDED: 73 | return SUSPENDED; 74 | default: 75 | throw new IllegalArgumentException(String.format("Unknown status value: %s", status)); 76 | } 77 | } 78 | 79 | static OrchestrationStatus toProtobuf(OrchestrationRuntimeStatus status){ 80 | switch (status) { 81 | case RUNNING: 82 | return ORCHESTRATION_STATUS_RUNNING; 83 | case COMPLETED: 84 | return ORCHESTRATION_STATUS_COMPLETED; 85 | case CONTINUED_AS_NEW: 86 | return ORCHESTRATION_STATUS_CONTINUED_AS_NEW; 87 | case FAILED: 88 | return ORCHESTRATION_STATUS_FAILED; 89 | case CANCELED: 90 | return ORCHESTRATION_STATUS_CANCELED; 91 | case TERMINATED: 92 | return ORCHESTRATION_STATUS_TERMINATED; 93 | case PENDING: 94 | return ORCHESTRATION_STATUS_PENDING; 95 | case SUSPENDED: 96 | return ORCHESTRATION_STATUS_SUSPENDED; 97 | default: 98 | throw new IllegalArgumentException(String.format("Unknown status value: %s", status)); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/OrchestrationStatusQueryResult.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import javax.annotation.Nullable; 6 | import java.util.List; 7 | 8 | /** 9 | * Class representing the results of a filtered orchestration metadata query. 10 | *

11 | * Orchestration metadata can be queried with filters using the {@link DurableTaskClient#queryInstances} method. 12 | */ 13 | public final class OrchestrationStatusQueryResult { 14 | private final List orchestrationStates; 15 | private final String continuationToken; 16 | 17 | OrchestrationStatusQueryResult(List orchestrationStates, @Nullable String continuationToken) { 18 | this.orchestrationStates = orchestrationStates; 19 | this.continuationToken = continuationToken; 20 | } 21 | 22 | /** 23 | * Gets the list of orchestration metadata records that matched the {@link DurableTaskClient#queryInstances} query. 24 | * @return the list of orchestration metadata records that matched the {@link DurableTaskClient#queryInstances} query. 25 | */ 26 | public List getOrchestrationState() { 27 | return this.orchestrationStates; 28 | } 29 | 30 | /** 31 | * Gets the continuation token to use with the next query or {@code null} if no more metadata records are found. 32 | *

33 | * Note that a non-null value does not always mean that there are more metadata records that can be returned by a query. 34 | * 35 | * @return the continuation token to use with the next query or {@code null} if no more metadata records are found. 36 | */ 37 | public String getContinuationToken() { 38 | return this.continuationToken; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/OrchestratorFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Functional interface for inline orchestrator functions. 7 | *

8 | * See the description of {@link TaskOrchestration} for more information about how to correctly implement orchestrators. 9 | */ 10 | @FunctionalInterface 11 | public interface OrchestratorFunction { 12 | /** 13 | * Executes an orchestrator function and returns a result to use as the orchestration output. 14 | *

15 | * This functional interface is designed to support implementing orchestrators as lambda functions. It's intended to 16 | * be very similar to {@link java.util.function.Function}, but with a signature that's specific to orchestrators. 17 | * 18 | * @param ctx the orchestration context, which provides access to additional context for the current orchestration 19 | * execution 20 | * @return the serializable output of the orchestrator function 21 | */ 22 | R apply(TaskOrchestrationContext ctx); 23 | } -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/PurgeInstanceCriteria.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import javax.annotation.Nullable; 6 | import java.time.Duration; 7 | import java.time.Instant; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * Class used for constructing orchestration instance purge selection criteria. 13 | */ 14 | public final class PurgeInstanceCriteria { 15 | 16 | private Instant createdTimeFrom; 17 | private Instant createdTimeTo; 18 | private List runtimeStatusList = new ArrayList<>(); 19 | private Duration timeout; 20 | 21 | /** 22 | * Creates a new, default instance of the {@code PurgeInstanceCriteria} class. 23 | */ 24 | public PurgeInstanceCriteria() { 25 | } 26 | 27 | /** 28 | * Purge orchestration instances that were created after the specified instant. 29 | * 30 | * @param createdTimeFrom the minimum orchestration creation time to use as a selection criteria or {@code null} to 31 | * disable this selection criteria 32 | * @return this criteria object 33 | */ 34 | public PurgeInstanceCriteria setCreatedTimeFrom(Instant createdTimeFrom) { 35 | this.createdTimeFrom = createdTimeFrom; 36 | return this; 37 | } 38 | 39 | /** 40 | * Purge orchestration instances that were created before the specified instant. 41 | * 42 | * @param createdTimeTo the maximum orchestration creation time to use as a selection criteria or {@code null} to 43 | * disable this selection criteria 44 | * @return this criteria object 45 | */ 46 | public PurgeInstanceCriteria setCreatedTimeTo(Instant createdTimeTo) { 47 | this.createdTimeTo = createdTimeTo; 48 | return this; 49 | } 50 | 51 | /** 52 | * Sets the list of runtime status values to use as a selection criteria. Only orchestration instances that have a 53 | * matching runtime status will be purged. An empty list is the same as selecting for all runtime status values. 54 | * 55 | * @param runtimeStatusList the list of runtime status values to use as a selection criteria 56 | * @return this criteria object 57 | */ 58 | public PurgeInstanceCriteria setRuntimeStatusList(List runtimeStatusList) { 59 | this.runtimeStatusList = runtimeStatusList; 60 | return this; 61 | } 62 | 63 | /** 64 | * Sets a timeout duration for the purge operation. Setting to {@code null} will reset the timeout to be the default value. 65 | * 66 | * @param timeout the amount of time to wait for the purge instance operation to complete 67 | * @return this criteria object 68 | */ 69 | public PurgeInstanceCriteria setTimeout(Duration timeout) { 70 | this.timeout = timeout; 71 | return this; 72 | } 73 | 74 | /** 75 | * Gets the configured minimum orchestration creation time or {@code null} if none was configured. 76 | * @return the configured minimum orchestration creation time or {@code null} if none was configured 77 | */ 78 | @Nullable 79 | public Instant getCreatedTimeFrom() { 80 | return this.createdTimeFrom; 81 | } 82 | 83 | /** 84 | * Gets the configured maximum orchestration creation time or {@code null} if none was configured. 85 | * @return the configured maximum orchestration creation time or {@code null} if none was configured 86 | */ 87 | @Nullable 88 | public Instant getCreatedTimeTo() { 89 | return this.createdTimeTo; 90 | } 91 | 92 | /** 93 | * Gets the configured runtime status selection criteria. 94 | * @return the configured runtime status filter as a list of values 95 | */ 96 | public List getRuntimeStatusList() { 97 | return this.runtimeStatusList; 98 | } 99 | 100 | /** 101 | * Gets the configured timeout duration or {@code null} if none was configured. 102 | * @return the configured timeout 103 | */ 104 | @Nullable 105 | public Duration getTimeout() { 106 | return this.timeout; 107 | } 108 | 109 | } -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/PurgeResult.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Class representing the results of an orchestration state purge operation. 7 | *

8 | * Orchestration state can be purged using any of the {@link DurableTaskClient#purgeInstances} method overloads. 9 | */ 10 | public final class PurgeResult { 11 | 12 | private final int deletedInstanceCount; 13 | 14 | PurgeResult(int deletedInstanceCount) { 15 | this.deletedInstanceCount = deletedInstanceCount; 16 | } 17 | 18 | /** 19 | * Gets the number of purged orchestration instances. 20 | * @return the number of purged orchestration instances 21 | */ 22 | public int getDeletedInstanceCount() { 23 | return this.deletedInstanceCount; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/RetryContext.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import java.time.Duration; 6 | 7 | /** 8 | * Context data that's provided to {@link RetryHandler} implementations. 9 | */ 10 | public final class RetryContext { 11 | private final TaskOrchestrationContext orchestrationContext; 12 | private final int lastAttemptNumber; 13 | private final FailureDetails lastFailure; 14 | private final Duration totalRetryTime; 15 | 16 | RetryContext( 17 | TaskOrchestrationContext orchestrationContext, 18 | int lastAttemptNumber, 19 | FailureDetails lastFailure, 20 | Duration totalRetryTime) { 21 | this.orchestrationContext = orchestrationContext; 22 | this.lastAttemptNumber = lastAttemptNumber; 23 | this.lastFailure = lastFailure; 24 | this.totalRetryTime = totalRetryTime; 25 | } 26 | 27 | /** 28 | * Gets the context of the current orchestration. 29 | *

30 | * The orchestration context can be used in retry handlers to schedule timers (via the 31 | * {@link TaskOrchestrationContext#createTimer} methods) for implementing delays between retries. It can also be 32 | * used to implement time-based retry logic by using the {@link TaskOrchestrationContext#getCurrentInstant} method. 33 | * 34 | * @return the context of the parent orchestration 35 | */ 36 | public TaskOrchestrationContext getOrchestrationContext() { 37 | return this.orchestrationContext; 38 | } 39 | 40 | /** 41 | * Gets the details of the previous task failure, including the exception type, message, and callstack. 42 | * 43 | * @return the details of the previous task failure 44 | */ 45 | public FailureDetails getLastFailure() { 46 | return this.lastFailure; 47 | } 48 | 49 | /** 50 | * Gets the previous retry attempt number. This number starts at 1 and increments each time the retry handler 51 | * is invoked for a particular task failure. 52 | * 53 | * @return the previous retry attempt number 54 | */ 55 | public int getLastAttemptNumber() { 56 | return this.lastAttemptNumber; 57 | } 58 | 59 | /** 60 | * Gets the total amount of time spent in a retry loop for the current task. 61 | * 62 | * @return the total amount of time spent in a retry loop for the current task 63 | */ 64 | public Duration getTotalRetryTime() { 65 | return this.totalRetryTime; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/RetryHandler.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Functional interface for implementing custom task retry handlers. 7 | *

8 | * It's important to remember that retry handler code is an extension of the orchestrator code and must therefore comply 9 | * with all the determinism requirements of orchestrator code. 10 | */ 11 | @FunctionalInterface 12 | public interface RetryHandler { 13 | /** 14 | * Invokes the retry handler logic and returns a value indicating whether to continue retrying. 15 | * 16 | * @param context retry context that's updated between each retry attempt 17 | * @return {@code true} to continue retrying or {@code false} to stop retrying. 18 | */ 19 | boolean handle(RetryContext context); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/Task.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import com.microsoft.durabletask.interruption.OrchestratorBlockedException; 6 | 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.function.Consumer; 9 | import java.util.function.Function; 10 | 11 | /** 12 | * Represents an asynchronous operation in a durable orchestration. 13 | *

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

17 |  * Task{@literal <}int{@literal >} activityTask = ctx.callActivity("MyActivity", someInput, int.class);
18 |  * 
19 | *

20 | * Orchestrator code uses the {@link #await()} method to block on the completion of the task and retrieve the result. 21 | * If the task is not yet complete, the {@code await()} method will throw an {@link OrchestratorBlockedException}, which 22 | * pauses the orchestrator's execution so that it can save its progress into durable storage and schedule any 23 | * outstanding work. When the task is complete, the orchestrator will run again from the beginning and the next time 24 | * the task's {@code await()} method is called, the result will be returned, or a {@link TaskFailedException} will be 25 | * thrown if the result of the task was an unhandled exception. 26 | *

27 | * Note that orchestrator code must never catch {@code OrchestratorBlockedException} because doing so can cause the 28 | * orchestration instance to get permanently stuck. 29 | * 30 | * @param the return type of the task 31 | */ 32 | public abstract class Task { 33 | final CompletableFuture future; 34 | Task(CompletableFuture future) { 35 | this.future = future; 36 | } 37 | 38 | /** 39 | * Returns {@code true} if completed in any fashion: normally, with an exception, or via cancellation. 40 | * @return {@code true} if completed, otherwise {@code false} 41 | */ 42 | public boolean isDone() { 43 | return this.future.isDone(); 44 | } 45 | 46 | /** 47 | * Returns {@code true} if the task was cancelled. 48 | * @return {@code true} if the task was cancelled, otherwise {@code false} 49 | */ 50 | public boolean isCancelled() { 51 | return this.future.isCancelled(); 52 | } 53 | 54 | /** 55 | * Blocks the orchestrator until this task to complete, and then returns its result. 56 | * 57 | * @return the result of the task 58 | */ 59 | public abstract V await(); 60 | 61 | /** 62 | * Returns a new {@link Task} that, when this Task completes normally, 63 | * is executed with this Task's result as the argument to the supplied function. 64 | * @param fn the function to use to compute the value of the returned Task 65 | * @return the new Task 66 | * @param the function's return type 67 | */ 68 | public abstract Task thenApply(Function fn); 69 | 70 | /** 71 | *Returns a new {@link Task} that, when this Task completes normally, 72 | * is executed with this Task's result as the argument to the supplied action. 73 | * @param fn the function to use to compute the value of the returned Task 74 | * @return the new Task 75 | */ 76 | public abstract Task thenAccept(Consumer fn); 77 | } -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskActivity.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Common interface for task activity implementations. 7 | *

8 | * Activities are the basic unit of work in a durable task orchestration. Activities are the tasks that are 9 | * orchestrated in the business process. For example, you might create an orchestrator to process an order. The tasks 10 | * ay involve checking the inventory, charging the customer, and creating a shipment. Each task would be a separate 11 | * activity. These activities may be executed serially, in parallel, or some combination of both. 12 | *

13 | * Unlike task orchestrators, activities aren't restricted in the type of work you can do in them. Activity functions 14 | * are frequently used to make network calls or run CPU intensive operations. An activity can also return data back to 15 | * the orchestrator function. The Durable Task runtime guarantees that each called activity function will be executed 16 | * at least once during an orchestration's execution. 17 | *

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

21 | * Activities are scheduled by orchestrators using one of the {@link TaskOrchestrationContext#callActivity} method 22 | * overloads. 23 | */ 24 | @FunctionalInterface 25 | public interface TaskActivity { 26 | /** 27 | * Executes the activity logic and returns a value which will be serialized and returned to the calling orchestrator. 28 | * 29 | * @param ctx provides information about the current activity execution, like the activity's name and the input 30 | * data provided to it by the orchestrator. 31 | * @return any serializable value to be returned to the calling orchestrator. 32 | */ 33 | Object run(TaskActivityContext ctx); 34 | } 35 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskActivityContext.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Interface that provides {@link TaskActivity} implementations with activity context, such as an activity's name and 7 | * its input. 8 | */ 9 | public interface TaskActivityContext { 10 | /** 11 | * Gets the name of the current task activity. 12 | * @return the name of the current task activity 13 | */ 14 | String getName(); 15 | 16 | /** 17 | * Gets the deserialized activity input. 18 | * 19 | * @param targetType the {@link Class} object associated with {@code T} 20 | * @param the target type to deserialize the input into 21 | * @return the deserialized activity input value 22 | */ 23 | T getInput(Class targetType); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskActivityExecutor.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | import java.util.HashMap; 6 | import java.util.logging.Logger; 7 | 8 | final class TaskActivityExecutor { 9 | private final HashMap activityFactories; 10 | private final DataConverter dataConverter; 11 | private final Logger logger; 12 | 13 | public TaskActivityExecutor( 14 | HashMap activityFactories, 15 | DataConverter dataConverter, 16 | Logger logger) { 17 | this.activityFactories = activityFactories; 18 | this.dataConverter = dataConverter; 19 | this.logger = logger; 20 | } 21 | 22 | public String execute(String taskName, String input, int taskId) throws Throwable { 23 | TaskActivityFactory factory = this.activityFactories.get(taskName); 24 | if (factory == null) { 25 | throw new IllegalStateException( 26 | String.format("No activity task named '%s' is registered.", taskName)); 27 | } 28 | 29 | TaskActivity activity = factory.create(); 30 | if (activity == null) { 31 | throw new IllegalStateException( 32 | String.format("The task factory '%s' returned a null TaskActivity object.", taskName)); 33 | } 34 | 35 | TaskActivityContextImpl context = new TaskActivityContextImpl(taskName, input); 36 | 37 | // Unhandled exceptions are allowed to escape 38 | Object output = activity.run(context); 39 | if (output != null) { 40 | return this.dataConverter.serialize(output); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | private class TaskActivityContextImpl implements TaskActivityContext { 47 | private final String name; 48 | private final String rawInput; 49 | 50 | private final DataConverter dataConverter = TaskActivityExecutor.this.dataConverter; 51 | 52 | public TaskActivityContextImpl(String activityName, String rawInput) { 53 | this.name = activityName; 54 | this.rawInput = rawInput; 55 | } 56 | 57 | @Override 58 | public String getName() { 59 | return this.name; 60 | } 61 | 62 | @Override 63 | public T getInput(Class targetType) { 64 | if (this.rawInput == null) { 65 | return null; 66 | } 67 | 68 | return this.dataConverter.deserialize(this.rawInput, targetType); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskActivityFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Factory interface for producing {@link TaskActivity} implementations. 7 | */ 8 | public interface TaskActivityFactory { 9 | /** 10 | * Gets the name of the activity this factory creates. 11 | * @return the name of the activity 12 | */ 13 | String getName(); 14 | 15 | /** 16 | * Creates a new instance of {@link TaskActivity} 17 | * @return the created activity instance 18 | */ 19 | TaskActivity create(); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskCanceledException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | // TODO: This should inherit from Exception, not TaskFailedException 6 | /** 7 | * Represents a task cancellation, either because of a timeout or because of an explicit cancellation operation. 8 | */ 9 | public final class TaskCanceledException extends TaskFailedException { 10 | // Only intended to be created within this package 11 | TaskCanceledException(String message, String taskName, int taskId) { 12 | super(message, taskName, taskId, new FailureDetails(TaskCanceledException.class.getName(), message, "", true)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskFailedException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Exception that gets thrown when awaiting a {@link Task} for an activity or sub-orchestration that fails with an 7 | * unhandled exception. 8 | *

9 | * Detailed information associated with a particular task failure can be retrieved using the {@link #getErrorDetails()} 10 | * method. 11 | */ 12 | public class TaskFailedException extends RuntimeException { 13 | private final FailureDetails details; 14 | private final String taskName; 15 | private final int taskId; 16 | 17 | TaskFailedException(String taskName, int taskId, FailureDetails details) { 18 | this(getExceptionMessage(taskName, taskId, details), taskName, taskId, details); 19 | } 20 | 21 | TaskFailedException(String message, String taskName, int taskId, FailureDetails details) { 22 | super(message); 23 | this.taskName = taskName; 24 | this.taskId = taskId; 25 | this.details = details; 26 | } 27 | 28 | /** 29 | * Gets the ID of the failed task. 30 | *

31 | * Each durable task (activities, timers, sub-orchestrations, etc.) scheduled by a task orchestrator has an 32 | * auto-incrementing ID associated with it. This ID is used to distinguish tasks from one another, even if, for 33 | * example, they are tasks that call the same activity. This ID can therefore be used to more easily correlate a 34 | * specific task failure to a specific task. 35 | * 36 | * @return the ID of the failed task 37 | */ 38 | public int getTaskId() { 39 | return this.taskId; 40 | } 41 | 42 | /** 43 | * Gets the name of the failed task. 44 | * 45 | * @return the name of the failed task 46 | */ 47 | public String getTaskName() { 48 | return this.taskName; 49 | } 50 | 51 | /** 52 | * Gets the details of the task failure, including exception information. 53 | * @return the details of the task failure 54 | */ 55 | public FailureDetails getErrorDetails() { 56 | return this.details; 57 | } 58 | 59 | private static String getExceptionMessage(String taskName, int taskId, FailureDetails details) { 60 | return String.format("Task '%s' (#%d) failed with an unhandled exception: %s", 61 | taskName, 62 | taskId, 63 | details.getErrorMessage()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskOptions.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Options that can be used to control the behavior of orchestrator and activity task execution. 7 | */ 8 | public final class TaskOptions { 9 | private final RetryPolicy retryPolicy; 10 | private final RetryHandler retryHandler; 11 | 12 | private TaskOptions(RetryPolicy retryPolicy, RetryHandler retryHandler) { 13 | this.retryPolicy = retryPolicy; 14 | this.retryHandler = retryHandler; 15 | } 16 | 17 | /** 18 | * Creates a new {@code TaskOptions} object from a {@link RetryPolicy}. 19 | * @param retryPolicy the retry policy to use in the new {@code TaskOptions} object. 20 | */ 21 | public TaskOptions(RetryPolicy retryPolicy) { 22 | this(retryPolicy, null); 23 | } 24 | 25 | /** 26 | * Creates a new {@code TaskOptions} object from a {@link RetryHandler}. 27 | * @param retryHandler the retry handler to use in the new {@code TaskOptions} object. 28 | */ 29 | public TaskOptions(RetryHandler retryHandler) { 30 | this(null, retryHandler); 31 | } 32 | 33 | boolean hasRetryPolicy() { 34 | return this.retryPolicy != null; 35 | } 36 | 37 | /** 38 | * Gets the configured {@link RetryPolicy} value or {@code null} if none was configured. 39 | * @return the configured retry policy 40 | */ 41 | public RetryPolicy getRetryPolicy() { 42 | return this.retryPolicy; 43 | } 44 | 45 | boolean hasRetryHandler() { 46 | return this.retryHandler != null; 47 | } 48 | 49 | /** 50 | * Gets the configured {@link RetryHandler} value or {@code null} if none was configured. 51 | * @return the configured retry handler. 52 | */ 53 | public RetryHandler getRetryHandler() { 54 | return this.retryHandler; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskOrchestration.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Common interface for task orchestrator implementations. 7 | *

8 | * Task orchestrators describe how actions are executed and the order in which actions are executed. Orchestrators 9 | * don't call into external services or do complex computation directly. Rather, they delegate these tasks to 10 | * activities, which perform the actual work. 11 | *

12 | * Orchestrators can be scheduled using the {@link DurableTaskClient#scheduleNewOrchestrationInstance} method overloads. 13 | * Orchestrators can also invoke child orchestrators using the {@link TaskOrchestrationContext#callSubOrchestrator} 14 | * method overloads. 15 | *

16 | * Orchestrators may be replayed multiple times to rebuild their local state after being reloaded into memory. 17 | * Orchestrator code must therefore be deterministic to ensure no unexpected side effects from execution 18 | * replay. To account for this behavior, there are several coding constraints to be aware of: 19 | *

    20 | *
  • 21 | * An orchestrator must not generate random numbers or random UUIDs, get the current date, read environment 22 | * variables, or do anything else that might result in a different value if the code is replayed in the future. 23 | * Activities and built-in methods on the {@link TaskOrchestrationContext} parameter, like 24 | * {@link TaskOrchestrationContext#getCurrentInstant()}, can be used to work around these restrictions. 25 | *
  • 26 | *
  • 27 | * Orchestrator logic must be executed on the orchestrator thread. Creating new threads or scheduling callbacks 28 | * onto background threads is forbidden and may result in failures or other unexpected behavior. 29 | *
  • 30 | *
  • 31 | * Avoid infinite loops as they could cause the application to run out of memory. Instead, ensure that loops are 32 | * bounded or use {@link TaskOrchestrationContext#continueAsNew} to restart an orchestrator with a new input. 33 | *
  • 34 | *
  • 35 | * Avoid logging directly in the orchestrator code because log messages will be duplicated on each replay. 36 | * Instead, check the value of the {@link TaskOrchestrationContext#getIsReplaying} method and write log messages 37 | * only when it is {@code false}. 38 | *
  • 39 | *
40 | *

41 | * Orchestrator code is tightly coupled with its execution history so special care must be taken when making changes 42 | * to orchestrator code. For example, adding or removing activity tasks to an orchestrator's code may cause a 43 | * mismatch between code and history for in-flight orchestrations. To avoid potential issues related to orchestrator 44 | * versioning, consider applying the following strategies: 45 | *

    46 | *
  • 47 | * Deploy multiple versions of applications side-by-side allowing new code to run independently of old code. 48 | *
  • 49 | *
  • 50 | * Rather than changing existing orchestrators, create new orchestrators that implement the modified behavior. 51 | *
  • 52 | *
  • 53 | * Ensure all in-flight orchestrations are complete before applying code changes to existing orchestrator code. 54 | *
  • 55 | *
  • 56 | * If possible, only make changes to orchestrator code that won't impact its history or execution path. For 57 | * example, renaming variables or adding log statements have no impact on an orchestrator's execution path and 58 | * are safe to apply to existing orchestrations. 59 | *
  • 60 | *
61 | */ 62 | @FunctionalInterface 63 | public interface TaskOrchestration { 64 | /** 65 | * Executes the orchestrator logic. 66 | * 67 | * @param ctx provides access to methods for scheduling durable tasks and getting information about the current 68 | * orchestration instance. 69 | */ 70 | void run(TaskOrchestrationContext ctx); 71 | } 72 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskOrchestrationFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask; 4 | 5 | /** 6 | * Factory interface for producing {@link TaskOrchestration} implementations. 7 | */ 8 | public interface TaskOrchestrationFactory { 9 | /** 10 | * Gets the name of the orchestration this factory creates. 11 | * @return the name of the orchestration 12 | */ 13 | String getName(); 14 | 15 | /** 16 | * Creates a new instance of {@link TaskOrchestration} 17 | * @return the created orchestration instance 18 | */ 19 | TaskOrchestration create(); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/TaskOrchestratorResult.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.durabletask; 2 | 3 | import com.microsoft.durabletask.implementation.protobuf.OrchestratorService; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | 8 | final class TaskOrchestratorResult { 9 | 10 | private final Collection actions; 11 | 12 | private final String customStatus; 13 | 14 | public TaskOrchestratorResult(Collection actions, String customStatus) { 15 | this.actions = Collections.unmodifiableCollection(actions);; 16 | this.customStatus = customStatus; 17 | } 18 | 19 | public Collection getActions() { 20 | return this.actions; 21 | } 22 | 23 | public String getCustomStatus() { 24 | return this.customStatus; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/interruption/ContinueAsNewInterruption.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.durabletask.interruption; 2 | 3 | import com.microsoft.durabletask.TaskOrchestrationContext; 4 | 5 | /** 6 | * Control flow {@code Throwable} class for orchestrator when invoke {@link TaskOrchestrationContext#continueAsNew}. 7 | * This {@code Throwable} must never be caught by user 8 | * code. 9 | *

10 | * {@code ContinueAsNewInterruption} is thrown when an orchestrator calls {@link TaskOrchestrationContext#continueAsNew}. 11 | * Catching {@code ContinueAsNewInterruption} in user code could prevent the orchestration from saving 12 | * state and scheduling new tasks, resulting in the orchestration getting stuck. 13 | */ 14 | public class ContinueAsNewInterruption extends RuntimeException { 15 | public ContinueAsNewInterruption(String message) { 16 | super(message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/interruption/OrchestratorBlockedException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package com.microsoft.durabletask.interruption; 4 | 5 | import com.microsoft.durabletask.Task; 6 | 7 | /** 8 | * Control flow {@code Throwable} class for orchestrator functions. This {@code Throwable} must never be caught by user 9 | * code. 10 | *

11 | * {@code OrchestratorBlockedException} is thrown when an orchestrator calls {@link Task#await} on an uncompleted task. The 12 | * purpose of throwing in this way is to halt execution of the orchestrator to save the current state and commit any 13 | * side effects. Catching {@code OrchestratorBlockedException} in user code could prevent the orchestration from saving 14 | * state and scheduling new tasks, resulting in the orchestration getting stuck. 15 | */ 16 | public final class OrchestratorBlockedException extends RuntimeException { 17 | public OrchestratorBlockedException(String message) { 18 | super(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/main/java/com/microsoft/durabletask/util/UUIDGenerator.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.durabletask.util; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.charset.StandardCharsets; 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.util.UUID; 8 | 9 | public class UUIDGenerator { 10 | public static UUID generate(int version, String algorithm, UUID namespace, String name) { 11 | 12 | MessageDigest hasher = hasher(algorithm); 13 | 14 | if (namespace != null) { 15 | ByteBuffer ns = ByteBuffer.allocate(16); 16 | ns.putLong(namespace.getMostSignificantBits()); 17 | ns.putLong(namespace.getLeastSignificantBits()); 18 | hasher.update(ns.array()); 19 | } 20 | 21 | hasher.update(name.getBytes(StandardCharsets.UTF_8)); 22 | ByteBuffer hash = ByteBuffer.wrap(hasher.digest()); 23 | 24 | final long msb = (hash.getLong() & 0xffffffffffff0fffL) | (version & 0x0f) << 12; 25 | final long lsb = (hash.getLong() & 0x3fffffffffffffffL) | 0x8000000000000000L; 26 | 27 | return new UUID(msb, lsb); 28 | } 29 | 30 | private static MessageDigest hasher(String algorithm) { 31 | try { 32 | return MessageDigest.getInstance(algorithm); 33 | } catch (NoSuchAlgorithmException e) { 34 | throw new RuntimeException(String.format("%s not supported.", algorithm)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/src/test/java/com/microsoft/durabletask/IntegrationTestBase.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.durabletask; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | 8 | import java.time.Duration; 9 | 10 | public class IntegrationTestBase { 11 | protected static final Duration defaultTimeout = Duration.ofSeconds(10); 12 | 13 | // All tests that create a server should save it to this variable for proper shutdown 14 | private DurableTaskGrpcWorker server; 15 | 16 | @AfterEach 17 | private void shutdown() { 18 | if (this.server != null) { 19 | this.server.stop(); 20 | } 21 | } 22 | 23 | protected TestDurableTaskWorkerBuilder createWorkerBuilder() { 24 | return new TestDurableTaskWorkerBuilder(); 25 | } 26 | 27 | public class TestDurableTaskWorkerBuilder { 28 | final DurableTaskGrpcWorkerBuilder innerBuilder; 29 | 30 | private TestDurableTaskWorkerBuilder() { 31 | this.innerBuilder = new DurableTaskGrpcWorkerBuilder(); 32 | } 33 | 34 | public DurableTaskGrpcWorker buildAndStart() { 35 | DurableTaskGrpcWorker server = this.innerBuilder.build(); 36 | IntegrationTestBase.this.server = server; 37 | server.start(); 38 | return server; 39 | } 40 | 41 | public TestDurableTaskWorkerBuilder setMaximumTimerInterval(Duration maximumTimerInterval) { 42 | this.innerBuilder.maximumTimerInterval(maximumTimerInterval); 43 | return this; 44 | } 45 | 46 | public TestDurableTaskWorkerBuilder addOrchestrator( 47 | String name, 48 | TaskOrchestration implementation) { 49 | this.innerBuilder.addOrchestration(new TaskOrchestrationFactory() { 50 | @Override 51 | public String getName() { return name; } 52 | 53 | @Override 54 | public TaskOrchestration create() { return implementation; } 55 | }); 56 | return this; 57 | } 58 | 59 | public IntegrationTests.TestDurableTaskWorkerBuilder addActivity( 60 | String name, 61 | TaskActivity implementation) 62 | { 63 | this.innerBuilder.addActivity(new TaskActivityFactory() { 64 | @Override 65 | public String getName() { return name; } 66 | 67 | @Override 68 | public TaskActivity create() { return implementation; } 69 | }); 70 | return this; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /endtoendtests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/azure-functions/java:4-java11 2 | 3 | COPY endtoendtests/build/azure-functions/azure-functions-sample/ /home/site/wwwroot/ 4 | ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ 5 | AzureFunctionsJobHost__Logging__Console__IsEnabled=true 6 | -------------------------------------------------------------------------------- /endtoendtests/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.microsoft.azure.azurefunctions" version "1.11.1" 3 | } 4 | apply plugin: 'java' 5 | apply plugin: "com.microsoft.azure.azurefunctions" 6 | 7 | group 'com.durabletask.endtoend' 8 | version '0.0.0-SNAPSHOT' 9 | 10 | repositories { 11 | mavenLocal() 12 | maven { 13 | url "https://oss.sonatype.org/content/repositories/snapshots/" 14 | } 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | implementation project(':client') 20 | implementation project(':azurefunctions') 21 | 22 | implementation 'com.microsoft.azure.functions:azure-functions-java-library:3.0.0' 23 | testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2' 24 | testImplementation 'io.rest-assured:rest-assured:5.3.0' 25 | testImplementation 'io.rest-assured:json-path:5.3.0' 26 | 27 | } 28 | 29 | sourceCompatibility = '1.8' 30 | targetCompatibility = '1.8' 31 | 32 | compileJava.options.encoding = 'UTF-8' 33 | 34 | task endToEndTest(type: Test) { 35 | useJUnitPlatform { 36 | includeTags 'e2e' 37 | } 38 | dependsOn build 39 | testLogging.showStandardStreams = true 40 | } 41 | 42 | 43 | azurefunctions { 44 | resourceGroup = 'java-functions-group' 45 | appName = 'azure-functions-sample' 46 | pricingTier = 'Consumption' 47 | region = 'westus' 48 | runtime { 49 | os = 'Windows' 50 | javaVersion = 'Java 8' 51 | } 52 | auth { 53 | type = 'azure_cli' 54 | } 55 | localDebug = "transport=dt_socket,server=y,suspend=n,address=5005" 56 | } 57 | -------------------------------------------------------------------------------- /endtoendtests/e2e-test-setup.ps1: -------------------------------------------------------------------------------- 1 | # Installing PowerShell: https://docs.microsoft.com/powershell/scripting/install/installing-powershell 2 | 3 | param( 4 | [Parameter(Mandatory=$true)] 5 | [string]$DockerfilePath, 6 | [string]$ImageName="dfapp", 7 | [string]$ContainerName="app", 8 | [switch]$NoSetup=$false, 9 | [switch]$NoValidation=$false, 10 | [string]$AzuriteVersion="3.34.0", 11 | [int]$Sleep=30 12 | ) 13 | 14 | $ErrorActionPreference = "Stop" 15 | 16 | if ($NoSetup -eq $false) { 17 | # Build the docker image first, since that's the most critical step 18 | Write-Host "Building sample app Docker container from '$DockerfilePath'..." -ForegroundColor Yellow 19 | docker build -f $DockerfilePath -t $ImageName --progress plain . 20 | 21 | # Next, download and start the Azurite emulator Docker image 22 | Write-Host "Pulling down the mcr.microsoft.com/azure-storage/azurite:$AzuriteVersion image..." -ForegroundColor Yellow 23 | docker pull "mcr.microsoft.com/azure-storage/azurite:${AzuriteVersion}" 24 | 25 | Write-Host "Starting Azurite storage emulator using default ports..." -ForegroundColor Yellow 26 | docker run --name 'azurite' -p 10000:10000 -p 10001:10001 -p 10002:10002 -d "mcr.microsoft.com/azure-storage/azurite:${AzuriteVersion}" 27 | 28 | # Finally, start up the smoke test container, which will connect to the Azurite container 29 | docker run --name $ContainerName -p 8080:80 -it --add-host=host.docker.internal:host-gateway -d ` 30 | --env 'AzureWebJobsStorage=UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://host.docker.internal' ` 31 | --env 'WEBSITE_HOSTNAME=localhost:8080' ` 32 | $ImageName 33 | } 34 | 35 | if ($sleep -gt 0) { 36 | # The container needs a bit more time before it can start receiving requests 37 | Write-Host "Sleeping for $Sleep seconds to let the container finish initializing..." -ForegroundColor Yellow 38 | Start-Sleep -Seconds $Sleep 39 | } 40 | 41 | # Check to see what containers are running 42 | docker ps -------------------------------------------------------------------------------- /endtoendtests/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "logLevel": { 5 | "DurableTask.AzureStorage": "Warning", 6 | "DurableTask.Core": "Warning" 7 | }, 8 | "applicationInsights": { 9 | "samplingSettings": { 10 | "isEnabled": false 11 | } 12 | } 13 | }, 14 | "extensions": { 15 | "durableTask": { 16 | "hubName": "DFJavaSmokeTest" 17 | } 18 | }, 19 | "extensionBundle": { 20 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 21 | "version": "[4.*, 5.0.0)" 22 | } 23 | } -------------------------------------------------------------------------------- /endtoendtests/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "java" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /endtoendtests/src/main/java/com/functions/AzureFunctions.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.annotation.*; 4 | import com.microsoft.azure.functions.*; 5 | import java.util.*; 6 | 7 | import com.microsoft.durabletask.*; 8 | import com.microsoft.durabletask.azurefunctions.DurableActivityTrigger; 9 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 10 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 11 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 12 | 13 | /** 14 | * Azure Durable Functions with HTTP trigger. 15 | */ 16 | public class AzureFunctions { 17 | /** 18 | * This HTTP-triggered function starts the orchestration. 19 | */ 20 | @FunctionName("StartOrchestration") 21 | public HttpResponseMessage startOrchestration( 22 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 23 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 24 | final ExecutionContext context) { 25 | context.getLogger().info("Java HTTP trigger processed a request."); 26 | 27 | DurableTaskClient client = durableContext.getClient(); 28 | String instanceId = client.scheduleNewOrchestrationInstance("Cities"); 29 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 30 | return durableContext.createCheckStatusResponse(request, instanceId); 31 | } 32 | 33 | /** 34 | * This is the orchestrator function, which can schedule activity functions, create durable timers, 35 | * or wait for external events in a way that's completely fault-tolerant. 36 | */ 37 | @FunctionName("Cities") 38 | public String citiesOrchestrator( 39 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 40 | String result = ""; 41 | result += ctx.callActivity("Capitalize", "Tokyo", String.class).await() + ", "; 42 | result += ctx.callActivity("Capitalize", "London", String.class).await() + ", "; 43 | result += ctx.callActivity("Capitalize", "Seattle", String.class).await() + ", "; 44 | result += ctx.callActivity("Capitalize", "Austin", String.class).await(); 45 | return result; 46 | } 47 | 48 | /** 49 | * This is the activity function that gets invoked by the orchestration. 50 | */ 51 | @FunctionName("Capitalize") 52 | public String capitalize( 53 | @DurableActivityTrigger(name = "name") String name, 54 | final ExecutionContext context) { 55 | context.getLogger().info("Capitalizing: " + name); 56 | return name.toUpperCase(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /endtoendtests/src/main/java/com/functions/ContinueAsNew.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.Task; 12 | import com.microsoft.durabletask.TaskOrchestrationContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 14 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 15 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 16 | 17 | import java.time.Duration; 18 | import java.util.Optional; 19 | 20 | public class ContinueAsNew { 21 | @FunctionName("ContinueAsNew") 22 | public HttpResponseMessage continueAsNew( 23 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 24 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 25 | final ExecutionContext context) { 26 | context.getLogger().info("Java HTTP trigger processed a request."); 27 | 28 | DurableTaskClient client = durableContext.getClient(); 29 | String instanceId = client.scheduleNewOrchestrationInstance("EternalOrchestrator"); 30 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 31 | return durableContext.createCheckStatusResponse(request, instanceId); 32 | } 33 | 34 | @FunctionName("EternalOrchestrator") 35 | public void eternalOrchestrator(@DurableOrchestrationTrigger(name = "runtimeState") TaskOrchestrationContext ctx) 36 | { 37 | System.out.println("Processing stuff..."); 38 | ctx.createTimer(Duration.ofSeconds(2)).await(); 39 | ctx.continueAsNew(null); 40 | } 41 | 42 | @FunctionName("ContinueAsNewExternalEvent") 43 | public HttpResponseMessage continueAsNewExternalEvent( 44 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 45 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 46 | final ExecutionContext context) { 47 | context.getLogger().info("Java HTTP trigger processed a request."); 48 | 49 | DurableTaskClient client = durableContext.getClient(); 50 | String instanceId = client.scheduleNewOrchestrationInstance("EternalEvent"); 51 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 52 | return durableContext.createCheckStatusResponse(request, instanceId); 53 | } 54 | 55 | @FunctionName("EternalEvent") 56 | public void eternalEvent(@DurableOrchestrationTrigger(name = "runtimeState") TaskOrchestrationContext ctx) 57 | { 58 | System.out.println("Waiting external event..."); 59 | Task event = ctx.waitForExternalEvent("event"); 60 | Task timer = ctx.createTimer(Duration.ofSeconds(10)); 61 | Task result = ctx.anyOf(event, timer).await(); 62 | if (result == event) { 63 | ctx.continueAsNew(null); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /endtoendtests/src/main/java/com/functions/DeserializeErrorTest.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.Task; 12 | import com.microsoft.durabletask.TaskOrchestrationContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 14 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 15 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 16 | 17 | import java.util.Optional; 18 | 19 | public class DeserializeErrorTest { 20 | @FunctionName("DeserializeErrorHttp") 21 | public HttpResponseMessage deserializeErrorHttp( 22 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) 23 | HttpRequestMessage> request, 24 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 25 | final ExecutionContext context) { 26 | context.getLogger().info("Java HTTP trigger processed a request."); 27 | 28 | DurableTaskClient client = durableContext.getClient(); 29 | String instanceId = client.scheduleNewOrchestrationInstance("DeserializeErrorOrchestrator"); 30 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 31 | return durableContext.createCheckStatusResponse(request, instanceId); 32 | } 33 | 34 | @FunctionName("DeserializeErrorOrchestrator") 35 | public String deserializeErrorOrchestrator( 36 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 37 | // cause deserialize error 38 | Person result = ctx.callActivity("Capitalize", "Austin", Person.class).await(); 39 | return result.getName(); 40 | } 41 | 42 | @FunctionName("SubCompletedErrorHttp") 43 | public HttpResponseMessage subCompletedErrorHttp( 44 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 45 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 46 | final ExecutionContext context) { 47 | context.getLogger().info("Java HTTP trigger processed a request."); 48 | 49 | DurableTaskClient client = durableContext.getClient(); 50 | String instanceId = client.scheduleNewOrchestrationInstance("CompletedErrorOrchestrator"); 51 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 52 | return durableContext.createCheckStatusResponse(request, instanceId); 53 | } 54 | 55 | @FunctionName("CompletedErrorOrchestrator") 56 | public String completedErrorOrchestrator( 57 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 58 | // cause deserialize issue 59 | Person result = ctx.callSubOrchestrator("CompletedErrorSubOrchestrator", "Austin", Person.class).await(); 60 | return result.getName(); 61 | } 62 | 63 | @FunctionName("CompletedErrorSubOrchestrator") 64 | public String completedErrorSubOrchestrator( 65 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 66 | return "test"; 67 | } 68 | 69 | @FunctionName("ExternalEventHttp") 70 | public HttpResponseMessage externalEventHttp( 71 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 72 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 73 | final ExecutionContext context) { 74 | context.getLogger().info("Java HTTP trigger processed a request."); 75 | 76 | DurableTaskClient client = durableContext.getClient(); 77 | String instanceId = client.scheduleNewOrchestrationInstance("ExternalEventActivity"); 78 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 79 | return durableContext.createCheckStatusResponse(request, instanceId); 80 | } 81 | 82 | @FunctionName("ExternalEventActivity") 83 | public void externalEventActivity(@DurableOrchestrationTrigger(name = "runtimeState") TaskOrchestrationContext ctx) 84 | { 85 | System.out.println("Waiting external event..."); 86 | Task event = ctx.waitForExternalEvent("event", String.class); 87 | Task result = ctx.anyOf(event).await(); 88 | Object input = result.await(); 89 | System.out.println(input); 90 | } 91 | 92 | static class Person { 93 | String name; 94 | 95 | public String getName() { 96 | return name; 97 | } 98 | 99 | public void setName(String name) { 100 | this.name = name; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /endtoendtests/src/main/java/com/functions/ResumeSuspend.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.TaskOrchestrationContext; 12 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 14 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 15 | 16 | import java.util.Optional; 17 | 18 | public class ResumeSuspend { 19 | 20 | @FunctionName("StartResumeSuspendOrchestration") 21 | public HttpResponseMessage startResumeSuspendOrchestration( 22 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 23 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 24 | final ExecutionContext context) { 25 | context.getLogger().info("Java HTTP trigger processed a request."); 26 | 27 | DurableTaskClient client = durableContext.getClient(); 28 | String instanceId = client.scheduleNewOrchestrationInstance("ResumeSuspend"); 29 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 30 | return durableContext.createCheckStatusResponse(request, instanceId); 31 | } 32 | 33 | @FunctionName("ResumeSuspend") 34 | public void resumeSuspend( 35 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 36 | ctx.waitForExternalEvent("test").await(); 37 | return; 38 | } 39 | } -------------------------------------------------------------------------------- /endtoendtests/src/main/java/com/functions/ThenChain.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.TaskOrchestrationContext; 12 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 14 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 15 | 16 | import java.util.Optional; 17 | 18 | public class ThenChain { 19 | @FunctionName("StartOrchestrationThenChain") 20 | public HttpResponseMessage startOrchestrationThenChain( 21 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 22 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 23 | final ExecutionContext context) { 24 | context.getLogger().info("Java HTTP trigger processed a request."); 25 | 26 | DurableTaskClient client = durableContext.getClient(); 27 | String instanceId = client.scheduleNewOrchestrationInstance("ThenChain"); 28 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 29 | return durableContext.createCheckStatusResponse(request, instanceId); 30 | } 31 | 32 | @FunctionName("ThenChain") 33 | public String thenChain( 34 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 35 | String result = ""; 36 | result += ctx.callActivity("Capitalize", "Austin", String.class).thenApply(s -> s + "-test").await(); 37 | return result; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /eng/ci/code-mirror.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | # These are the branches we'll mirror to our internal ADO instance 5 | # Keep this set limited as appropriate (don't mirror individual user branches). 6 | - main 7 | 8 | resources: 9 | repositories: 10 | - repository: eng 11 | type: git 12 | name: engineering 13 | ref: refs/tags/release 14 | 15 | variables: 16 | - template: ci/variables/cfs.yml@eng 17 | 18 | extends: 19 | template: ci/code-mirror.yml@eng 20 | -------------------------------------------------------------------------------- /eng/ci/official-build.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | - template: ci/variables/cfs.yml@eng 3 | 4 | trigger: 5 | batch: true 6 | branches: 7 | include: 8 | - main 9 | 10 | # CI only, does not trigger on PRs. 11 | pr: none 12 | 13 | resources: 14 | repositories: 15 | - repository: 1es 16 | type: git 17 | name: 1ESPipelineTemplates/1ESPipelineTemplates 18 | ref: refs/tags/release 19 | - repository: eng 20 | type: git 21 | name: engineering 22 | ref: refs/tags/release 23 | 24 | extends: 25 | template: v1/1ES.Official.PipelineTemplate.yml@1es 26 | parameters: 27 | pool: 28 | name: 1es-pool-azfunc 29 | image: 1es-windows-2022 30 | os: windows 31 | sdl: 32 | spotBugs: 33 | enabled: false 34 | stages: 35 | - stage: BuildAndSign 36 | dependsOn: [] 37 | jobs: 38 | - template: /eng/templates/build.yml@self -------------------------------------------------------------------------------- /eng/ci/release.yml: -------------------------------------------------------------------------------- 1 | pr: none 2 | trigger: none 3 | 4 | resources: 5 | repositories: 6 | - repository: 1ESPipelineTemplates 7 | type: git 8 | name: 1ESPipelineTemplates/1ESPipelineTemplates 9 | ref: refs/tags/release 10 | pipelines: 11 | - pipeline: DurableTaskJavaBuildPipeline 12 | source: durabletask-java.official 13 | branch: main 14 | 15 | extends: 16 | template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates 17 | parameters: 18 | pool: 19 | name: 1es-pool-azfunc 20 | image: 1es-ubuntu-22.04 21 | os: linux 22 | 23 | stages: 24 | - stage: release 25 | jobs: 26 | - job: durabletask_azuremanaged 27 | templateContext: 28 | type: releaseJob 29 | isProduction: true 30 | inputs: 31 | # Declare inputs to be released here to ensure they receive relevant checks 32 | - input: pipelineArtifact 33 | pipeline: DurableTaskJavaBuildPipeline 34 | artifactName: drop 35 | targetPath: $(System.DefaultWorkingDirectory)/drop 36 | 37 | steps: 38 | - task: SFP.release-tasks.custom-build-release-task.EsrpRelease@9 39 | displayName: 'Release durabletask-azuremanaged' 40 | inputs: 41 | connectedservicename: 'dtfx-internal-esrp-prod' 42 | usemanagedidentity: true 43 | keyvaultname: 'durable-esrp-akv' 44 | signcertname: 'dts-esrp-cert' 45 | clientid: '0b3ed1a4-0727-4a50-b82a-02c2bd9dec89' 46 | folderlocation: '$(System.DefaultWorkingDirectory)/drop/durabletask-azuremanaged' 47 | owners: 'wangbill@microsoft.com' 48 | approvers: 'kaibocai@microsoft.com' 49 | mainpublisher: 'durabletask-java' 50 | 51 | - job: durabletask_client 52 | templateContext: 53 | type: releaseJob 54 | isProduction: true 55 | inputs: 56 | # Declare inputs to be released here to ensure they receive relevant checks 57 | - input: pipelineArtifact 58 | pipeline: DurableTaskJavaBuildPipeline 59 | artifactName: drop 60 | targetPath: $(System.DefaultWorkingDirectory)/drop 61 | 62 | steps: 63 | - task: SFP.release-tasks.custom-build-release-task.EsrpRelease@9 64 | displayName: 'Release durabletask-client' 65 | inputs: 66 | connectedservicename: 'dtfx-internal-esrp-prod' 67 | usemanagedidentity: true 68 | keyvaultname: 'durable-esrp-akv' 69 | signcertname: 'dts-esrp-cert' 70 | clientid: '0b3ed1a4-0727-4a50-b82a-02c2bd9dec89' 71 | folderlocation: '$(System.DefaultWorkingDirectory)/drop/durabletask-client' 72 | owners: 'wangbill@microsoft.com' 73 | approvers: 'kaibocai@microsoft.com' 74 | mainpublisher: 'durabletask-java' 75 | 76 | - job: durabletask_azure_functions 77 | templateContext: 78 | type: releaseJob 79 | isProduction: true 80 | inputs: 81 | # Declare inputs to be released here to ensure they receive relevant checks 82 | - input: pipelineArtifact 83 | pipeline: DurableTaskJavaBuildPipeline 84 | artifactName: drop 85 | targetPath: $(System.DefaultWorkingDirectory)/drop 86 | 87 | steps: 88 | - task: SFP.release-tasks.custom-build-release-task.EsrpRelease@9 89 | displayName: 'Release durabletask-azure-functions' 90 | inputs: 91 | connectedservicename: 'dtfx-internal-esrp-prod' 92 | usemanagedidentity: true 93 | keyvaultname: 'durable-esrp-akv' 94 | signcertname: 'dts-esrp-cert' 95 | clientid: '0b3ed1a4-0727-4a50-b82a-02c2bd9dec89' 96 | folderlocation: '$(System.DefaultWorkingDirectory)/drop/durabletask-azure-functions' 97 | owners: 'wangbill@microsoft.com' 98 | approvers: 'kaibocai@microsoft.com' 99 | mainpublisher: 'durabletask-java' 100 | -------------------------------------------------------------------------------- /eng/templates/build.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: Build 3 | 4 | templateContext: 5 | outputs: 6 | - output: pipelineArtifact 7 | path: $(build.artifactStagingDirectory) 8 | artifact: drop 9 | sbomBuildDropPath: $(System.DefaultWorkingDirectory) 10 | sbomPackageName: 'Durable Task / Durable Functions Java SBOM' 11 | steps: 12 | - checkout: self 13 | 14 | - task: Gradle@3 15 | inputs: 16 | # Specifies the working directory to run the Gradle build. The task uses the repository root directory if the working directory is not specified. 17 | workingDirectory: '' 18 | # Specifies the gradlew wrapper's location within the repository that will be used for the build. 19 | gradleWrapperFile: 'gradlew' 20 | # Sets the GRADLE_OPTS environment variable, which is used to send command-line arguments to start the JVM. The xmx flag specifies the maximum memory available to the JVM. 21 | gradleOptions: '-Xmx3072m' 22 | javaHomeOption: 'JDKVersion' 23 | jdkVersionOption: 1.11 24 | jdkArchitectureOption: 'x64' 25 | publishJUnitResults: false 26 | tasks: clean assemble 27 | displayName: Assemble durabletask-client and durabletask-azure-functions and durabletask-azuremanaged 28 | 29 | # the secring.gpg file is required to sign the artifacts, it's generated from GnuPG, and it's stored in the library of the durabletaskframework ADO 30 | - task: DownloadSecureFile@1 31 | name: gpgSecretFile 32 | displayName: 'Download GPG secret file' 33 | inputs: 34 | secureFile: 'secring.gpg' 35 | 36 | - task: Gradle@3 37 | inputs: 38 | workingDirectory: '' 39 | gradleWrapperFile: 'gradlew' 40 | gradleOptions: '-Xmx3072m' 41 | javaHomeOption: 'JDKVersion' 42 | jdkVersionOption: 1.11 43 | jdkArchitectureOption: 'x64' 44 | tasks: publish 45 | options: '-Psigning.keyId=$(gpgSignKey) -Psigning.password=$(gpgSignPassword) -Psigning.secretKeyRingFile=$(gpgSecretFile.secureFilePath)' 46 | displayName: Publish durabletask-client and durabletask-azure-functions and durabletask-azuremanaged 47 | 48 | - task: CopyFiles@2 49 | displayName: 'Copy publish file to Artifact Staging Directory' 50 | inputs: 51 | SourceFolder: $(System.DefaultWorkingDirectory)/repo/com/microsoft 52 | Contents: '**/*.*' 53 | TargetFolder: $(Build.ArtifactStagingDirectory) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/durabletask-java/140f9349365142a6a2311a273d81b68b353f0bde/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH: -------------------------------------------------------------------------------- 1 | fbe5bb20835678099fc51a44993ed9b045dee5a6 -------------------------------------------------------------------------------- /internal/durabletask-protobuf/README.md: -------------------------------------------------------------------------------- 1 | # Durable Task Protobuf Files 2 | 3 | This directory contains the protocol buffer definitions used by the Durable Task Framework Java SDK. The files in this directory are automatically downloaded and updated during the build process from the [microsoft/durabletask-protobuf](https://github.com/microsoft/durabletask-protobuf) repository. 4 | 5 | ## Directory Structure 6 | 7 | - `protos/` - Contains the downloaded proto files 8 | - `PROTO_SOURCE_COMMIT_HASH` - Contains the commit hash of the latest proto file version 9 | 10 | ## Auto-Update Process 11 | 12 | The proto files are automatically downloaded and updated when running Gradle builds. This is handled by the `downloadProtoFiles` task in the `client/build.gradle` file. The task: 13 | 14 | 1. Downloads the latest version of `orchestrator_service.proto` 15 | 2. Saves the current commit hash for tracking purposes 16 | 3. Updates these files before proto compilation begins 17 | 18 | ## Manual Update 19 | 20 | If you need to manually update the proto files, you can run: 21 | 22 | ```bash 23 | ./gradlew downloadProtoFiles -PprotoBranch= 24 | ``` -------------------------------------------------------------------------------- /libs/durabletask-grpc-0.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/durabletask-java/140f9349365142a6a2311a273d81b68b353f0bde/libs/durabletask-grpc-0.1.0.jar -------------------------------------------------------------------------------- /samples-azure-functions/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/azure-functions/java:4-java11 2 | 3 | COPY samples-azure-functions/build/azure-functions/azure-functions-sample/ /home/site/wwwroot/ 4 | ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ 5 | AzureFunctionsJobHost__Logging__Console__IsEnabled=true 6 | -------------------------------------------------------------------------------- /samples-azure-functions/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.microsoft.azure.azurefunctions" version "1.11.1" 3 | } 4 | apply plugin: 'java' 5 | apply plugin: "com.microsoft.azure.azurefunctions" 6 | 7 | group 'com.functions' 8 | version '0.1.0-SNAPSHOT' 9 | 10 | repositories { 11 | mavenLocal() 12 | maven { 13 | url "https://oss.sonatype.org/content/repositories/snapshots/" 14 | } 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | implementation project(':client') 20 | implementation project(':azurefunctions') 21 | 22 | implementation 'com.microsoft.azure.functions:azure-functions-java-library:3.0.0' 23 | testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2' 24 | testImplementation 'io.rest-assured:rest-assured:5.3.0' 25 | testImplementation 'io.rest-assured:json-path:5.3.0' 26 | 27 | } 28 | 29 | sourceCompatibility = '1.8' 30 | targetCompatibility = '1.8' 31 | 32 | compileJava.options.encoding = 'UTF-8' 33 | 34 | task sampleTest(type: Test) { 35 | useJUnitPlatform { 36 | includeTags 'sampleTest' 37 | } 38 | dependsOn build 39 | testLogging.showStandardStreams = true 40 | } 41 | 42 | azurefunctions { 43 | resourceGroup = 'java-functions-group' 44 | appName = 'azure-functions-sample' 45 | pricingTier = 'Consumption' 46 | region = 'westus' 47 | runtime { 48 | os = 'Windows' 49 | javaVersion = 'Java 8' 50 | } 51 | auth { 52 | type = 'azure_cli' 53 | } 54 | localDebug = "transport=dt_socket,server=y,suspend=n,address=5005" 55 | } 56 | -------------------------------------------------------------------------------- /samples-azure-functions/e2e-test-setup.ps1: -------------------------------------------------------------------------------- 1 | # Installing PowerShell: https://docs.microsoft.com/powershell/scripting/install/installing-powershell 2 | 3 | param( 4 | [Parameter(Mandatory=$true)] 5 | [string]$DockerfilePath, 6 | [string]$ImageName="dfapp", 7 | [string]$ContainerName="app", 8 | [switch]$NoSetup=$false, 9 | [switch]$NoValidation=$false, 10 | [string]$AzuriteVersion="3.34.0", 11 | [int]$Sleep=30 12 | ) 13 | 14 | $ErrorActionPreference = "Stop" 15 | 16 | if ($NoSetup -eq $false) { 17 | # Build the docker image first, since that's the most critical step 18 | Write-Host "Building sample app Docker container from '$DockerfilePath'..." -ForegroundColor Yellow 19 | docker build -f $DockerfilePath -t $ImageName --progress plain . 20 | 21 | # Next, download and start the Azurite emulator Docker image 22 | Write-Host "Pulling down the mcr.microsoft.com/azure-storage/azurite:$AzuriteVersion image..." -ForegroundColor Yellow 23 | docker pull "mcr.microsoft.com/azure-storage/azurite:${AzuriteVersion}" 24 | 25 | Write-Host "Starting Azurite storage emulator using default ports..." -ForegroundColor Yellow 26 | docker run --name 'azurite' -p 10000:10000 -p 10001:10001 -p 10002:10002 -d "mcr.microsoft.com/azure-storage/azurite:${AzuriteVersion}" 27 | 28 | # Finally, start up the smoke test container, which will connect to the Azurite container 29 | docker run --name $ContainerName -p 8080:80 -it --add-host=host.docker.internal:host-gateway -d ` 30 | --env 'AzureWebJobsStorage=UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://host.docker.internal' ` 31 | --env 'WEBSITE_HOSTNAME=localhost:8080' ` 32 | $ImageName 33 | } 34 | 35 | if ($sleep -gt 0) { 36 | # The container needs a bit more time before it can start receiving requests 37 | Write-Host "Sleeping for $Sleep seconds to let the container finish initializing..." -ForegroundColor Yellow 38 | Start-Sleep -Seconds $Sleep 39 | } 40 | 41 | # Check to see what containers are running 42 | docker ps -------------------------------------------------------------------------------- /samples-azure-functions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "logLevel": { 5 | "DurableTask.AzureStorage": "Warning", 6 | "DurableTask.Core": "Warning" 7 | }, 8 | "applicationInsights": { 9 | "samplingSettings": { 10 | "isEnabled": false 11 | } 12 | } 13 | }, 14 | "extensions": { 15 | "durableTask": { 16 | "hubName": "DFJavaSmokeTest" 17 | } 18 | }, 19 | "extensionBundle": { 20 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 21 | "version": "[4.*, 5.0.0)" 22 | } 23 | } -------------------------------------------------------------------------------- /samples-azure-functions/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "java" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples-azure-functions/src/main/java/com/functions/AzureFunctions.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.functions.model.Person; 4 | import com.microsoft.azure.functions.annotation.*; 5 | import com.microsoft.azure.functions.*; 6 | import java.util.*; 7 | 8 | import com.microsoft.durabletask.*; 9 | import com.microsoft.durabletask.azurefunctions.DurableActivityTrigger; 10 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 11 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 12 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 13 | 14 | /** 15 | * Azure Durable Functions with HTTP trigger. 16 | */ 17 | public class AzureFunctions { 18 | /** 19 | * This HTTP-triggered function starts the orchestration. 20 | */ 21 | @FunctionName("StartOrchestration") 22 | public HttpResponseMessage startOrchestration( 23 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 24 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 25 | final ExecutionContext context) { 26 | context.getLogger().info("Java HTTP trigger processed a request."); 27 | 28 | DurableTaskClient client = durableContext.getClient(); 29 | String instanceId = client.scheduleNewOrchestrationInstance("Cities"); 30 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 31 | return durableContext.createCheckStatusResponse(request, instanceId); 32 | } 33 | 34 | /** 35 | * This is the orchestrator function, which can schedule activity functions, create durable timers, 36 | * or wait for external events in a way that's completely fault-tolerant. 37 | */ 38 | @FunctionName("Cities") 39 | public String citiesOrchestrator( 40 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 41 | String result = ""; 42 | result += ctx.callActivity("Capitalize", "Tokyo", String.class).await() + ", "; 43 | result += ctx.callActivity("Capitalize", "London", String.class).await() + ", "; 44 | result += ctx.callActivity("Capitalize", "Seattle", String.class).await() + ", "; 45 | result += ctx.callActivity("Capitalize", "Austin", String.class).await(); 46 | return result; 47 | } 48 | 49 | /** 50 | * This is the activity function that gets invoked by the orchestration. 51 | */ 52 | @FunctionName("Capitalize") 53 | public String capitalize( 54 | @DurableActivityTrigger(name = "name") String name, 55 | final ExecutionContext context) { 56 | context.getLogger().info("Capitalizing: " + name); 57 | return name.toUpperCase(); 58 | } 59 | 60 | // Orchestration with POJO input 61 | @FunctionName("StartOrchestrationPOJO") 62 | public HttpResponseMessage startOrchestrationPOJO( 63 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 64 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 65 | final ExecutionContext context) { 66 | context.getLogger().info("Java HTTP trigger processed a request."); 67 | 68 | Person person = new Person(); 69 | person.setName("testname"); 70 | 71 | DurableTaskClient client = durableContext.getClient(); 72 | String instanceId = client.scheduleNewOrchestrationInstance("Person", person); 73 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 74 | return durableContext.createCheckStatusResponse(request, instanceId); 75 | } 76 | 77 | @FunctionName("Person") 78 | public Person personOrchestrator( 79 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 80 | Person person = ctx.getInput(Person.class); 81 | person.setName(ctx.callActivity("Capitalize", person.getName(), String.class).await()); 82 | return person; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /samples-azure-functions/src/main/java/com/functions/ContinueAsNew.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.Task; 12 | import com.microsoft.durabletask.TaskOrchestrationContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 14 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 15 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 16 | 17 | import java.time.Duration; 18 | import java.util.Optional; 19 | 20 | public class ContinueAsNew { 21 | @FunctionName("ContinueAsNew") 22 | public HttpResponseMessage continueAsNew( 23 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 24 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 25 | final ExecutionContext context) { 26 | context.getLogger().info("Java HTTP trigger processed a request."); 27 | 28 | DurableTaskClient client = durableContext.getClient(); 29 | String instanceId = client.scheduleNewOrchestrationInstance("EternalOrchestrator"); 30 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 31 | return durableContext.createCheckStatusResponse(request, instanceId); 32 | } 33 | 34 | @FunctionName("EternalOrchestrator") 35 | public void eternalOrchestrator(@DurableOrchestrationTrigger(name = "runtimeState") TaskOrchestrationContext ctx) 36 | { 37 | System.out.println("Processing stuff..."); 38 | ctx.createTimer(Duration.ofSeconds(2)).await(); 39 | ctx.continueAsNew(null); 40 | } 41 | 42 | @FunctionName("ContinueAsNewExternalEvent") 43 | public HttpResponseMessage continueAsNewExternalEvent( 44 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 45 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 46 | final ExecutionContext context) { 47 | context.getLogger().info("Java HTTP trigger processed a request."); 48 | 49 | DurableTaskClient client = durableContext.getClient(); 50 | String instanceId = client.scheduleNewOrchestrationInstance("EternalEvent"); 51 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 52 | return durableContext.createCheckStatusResponse(request, instanceId); 53 | } 54 | 55 | @FunctionName("EternalEvent") 56 | public void eternalEvent(@DurableOrchestrationTrigger(name = "runtimeState") TaskOrchestrationContext ctx) 57 | { 58 | System.out.println("Waiting external event..."); 59 | Task event = ctx.waitForExternalEvent("event"); 60 | Task timer = ctx.createTimer(Duration.ofSeconds(10)); 61 | Task result = ctx.anyOf(event, timer).await(); 62 | if (result == event) { 63 | ctx.continueAsNew(null); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples-azure-functions/src/main/java/com/functions/ResumeSuspend.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.TaskOrchestrationContext; 12 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 14 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 15 | 16 | import java.util.Optional; 17 | 18 | public class ResumeSuspend { 19 | 20 | @FunctionName("StartResumeSuspendOrchestration") 21 | public HttpResponseMessage startResumeSuspendOrchestration( 22 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 23 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 24 | final ExecutionContext context) { 25 | context.getLogger().info("Java HTTP trigger processed a request."); 26 | 27 | DurableTaskClient client = durableContext.getClient(); 28 | String instanceId = client.scheduleNewOrchestrationInstance("ResumeSuspend"); 29 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 30 | return durableContext.createCheckStatusResponse(request, instanceId); 31 | } 32 | 33 | @FunctionName("ResumeSuspend") 34 | public void resumeSuspend( 35 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 36 | ctx.waitForExternalEvent("test").await(); 37 | return; 38 | } 39 | } -------------------------------------------------------------------------------- /samples-azure-functions/src/main/java/com/functions/SubOrchestrator.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.functions.model.Person; 4 | import com.microsoft.azure.functions.ExecutionContext; 5 | import com.microsoft.azure.functions.HttpMethod; 6 | import com.microsoft.azure.functions.HttpRequestMessage; 7 | import com.microsoft.azure.functions.HttpResponseMessage; 8 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 9 | import com.microsoft.azure.functions.annotation.FunctionName; 10 | import com.microsoft.azure.functions.annotation.HttpTrigger; 11 | import com.microsoft.durabletask.DurableTaskClient; 12 | import com.microsoft.durabletask.TaskOrchestrationContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 14 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 15 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 16 | 17 | import java.util.Optional; 18 | 19 | public class SubOrchestrator { 20 | 21 | @FunctionName("SubOrchestratorHttp") 22 | public HttpResponseMessage subOrchestratorHttp( 23 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 24 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 25 | final ExecutionContext context) { 26 | context.getLogger().info("Java HTTP trigger processed a request."); 27 | 28 | DurableTaskClient client = durableContext.getClient(); 29 | String instanceId = client.scheduleNewOrchestrationInstance("RootOrchestrator"); 30 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 31 | return durableContext.createCheckStatusResponse(request, instanceId); 32 | } 33 | 34 | @FunctionName("RootOrchestrator") 35 | public String rootOrchestrator( 36 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 37 | return ctx.callSubOrchestrator("SubOrchestrator", "Austin", String.class).await(); 38 | } 39 | 40 | @FunctionName("SubOrchestrator") 41 | public String subOrchestrator( 42 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 43 | String input = ctx.getInput(String.class); 44 | return ctx.callSubOrchestrator("Capitalize", input, String.class).await(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples-azure-functions/src/main/java/com/functions/ThenChain.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.TaskOrchestrationContext; 12 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 14 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 15 | 16 | import java.util.Optional; 17 | 18 | public class ThenChain { 19 | @FunctionName("StartOrchestrationThenChain") 20 | public HttpResponseMessage startOrchestrationThenChain( 21 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 22 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 23 | final ExecutionContext context) { 24 | context.getLogger().info("Java HTTP trigger processed a request."); 25 | 26 | DurableTaskClient client = durableContext.getClient(); 27 | String instanceId = client.scheduleNewOrchestrationInstance("ThenChain"); 28 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 29 | return durableContext.createCheckStatusResponse(request, instanceId); 30 | } 31 | 32 | @FunctionName("ThenChain") 33 | public String thenChain( 34 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { 35 | String result = ""; 36 | result += ctx.callActivity("Capitalize", "Austin", String.class).thenApply(s -> s + "-test").await(); 37 | return result; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /samples-azure-functions/src/main/java/com/functions/WaitExternalEvent.java: -------------------------------------------------------------------------------- 1 | package com.functions; 2 | 3 | import com.microsoft.azure.functions.ExecutionContext; 4 | import com.microsoft.azure.functions.HttpMethod; 5 | import com.microsoft.azure.functions.HttpRequestMessage; 6 | import com.microsoft.azure.functions.HttpResponseMessage; 7 | import com.microsoft.azure.functions.annotation.AuthorizationLevel; 8 | import com.microsoft.azure.functions.annotation.FunctionName; 9 | import com.microsoft.azure.functions.annotation.HttpTrigger; 10 | import com.microsoft.durabletask.DurableTaskClient; 11 | import com.microsoft.durabletask.Task; 12 | import com.microsoft.durabletask.TaskOrchestrationContext; 13 | import com.microsoft.durabletask.azurefunctions.DurableClientContext; 14 | import com.microsoft.durabletask.azurefunctions.DurableClientInput; 15 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger; 16 | 17 | import java.time.Duration; 18 | import java.util.Optional; 19 | 20 | public class WaitExternalEvent { 21 | @FunctionName("ExternalEventHttp") 22 | public HttpResponseMessage externalEventHttp( 23 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, 24 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext, 25 | final ExecutionContext context) { 26 | context.getLogger().info("Java HTTP trigger processed a request."); 27 | 28 | DurableTaskClient client = durableContext.getClient(); 29 | String instanceId = client.scheduleNewOrchestrationInstance("ExternalEventOrchestrator"); 30 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId); 31 | return durableContext.createCheckStatusResponse(request, instanceId); 32 | } 33 | 34 | @FunctionName("ExternalEventOrchestrator") 35 | public void externalEventOrchestrator(@DurableOrchestrationTrigger(name = "runtimeState") TaskOrchestrationContext ctx) 36 | { 37 | System.out.println("Waiting external event..."); 38 | Task event = ctx.waitForExternalEvent("event", String.class); 39 | Task timer = ctx.createTimer(Duration.ofSeconds(10)); 40 | Task winner = ctx.anyOf(event, timer).await(); 41 | if (winner == event) { 42 | String eventResult = event.await(); 43 | ctx.complete(eventResult); 44 | } else { 45 | ctx.complete("time out"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples-azure-functions/src/main/java/com/functions/model/Person.java: -------------------------------------------------------------------------------- 1 | package com.functions.model; 2 | 3 | public class Person { 4 | String name; 5 | 6 | public String getName() { 7 | return name; 8 | } 9 | 10 | public void setName(String name) { 11 | this.name = name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.5.2' 3 | id 'java' 4 | } 5 | 6 | group 'io.durabletask' 7 | version = '0.1.0' 8 | def grpcVersion = '1.59.0' 9 | archivesBaseName = 'durabletask-samples' 10 | 11 | bootJar { 12 | mainClass = 'io.durabletask.samples.WebApplication' 13 | } 14 | 15 | task runWebAppToDurableTaskSchedulerSample(type: JavaExec) { 16 | classpath = sourceSets.main.runtimeClasspath 17 | mainClass = 'io.durabletask.samples.WebAppToDurableTaskSchedulerSample' 18 | } 19 | 20 | dependencies { 21 | implementation project(':client') 22 | implementation project(':azuremanaged') 23 | 24 | implementation 'org.springframework.boot:spring-boot-starter-web' 25 | implementation platform("org.springframework.boot:spring-boot-dependencies:2.5.2") 26 | implementation 'org.springframework.boot:spring-boot-starter' 27 | 28 | // https://github.com/grpc/grpc-java#download 29 | implementation "io.grpc:grpc-protobuf:${grpcVersion}" 30 | implementation "io.grpc:grpc-stub:${grpcVersion}" 31 | runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" 32 | implementation 'com.azure:azure-identity:1.15.0' 33 | 34 | // install lombok 35 | annotationProcessor 'org.projectlombok:lombok:1.18.22' 36 | compileOnly 'org.projectlombok:lombok:1.18.22' 37 | } -------------------------------------------------------------------------------- /samples/src/main/java/io/durabletask/samples/ChainingPattern.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package io.durabletask.samples; 4 | 5 | import com.microsoft.durabletask.*; 6 | 7 | import java.io.IOException; 8 | import java.time.Duration; 9 | import java.util.concurrent.TimeoutException; 10 | 11 | final class ChainingPattern { 12 | public static void main(String[] args) throws IOException, InterruptedException, TimeoutException { 13 | // The TaskHubServer listens over gRPC for new orchestration and activity execution requests 14 | final DurableTaskGrpcWorker server = createTaskHubServer(); 15 | 16 | // Start the server to begin processing orchestration and activity requests 17 | server.start(); 18 | 19 | // Start a new instance of the registered "ActivityChaining" orchestration 20 | final DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); 21 | String instanceId = client.scheduleNewOrchestrationInstance( 22 | "ActivityChaining", 23 | new NewOrchestrationInstanceOptions().setInput("Hello, world!")); 24 | System.out.printf("Started new orchestration instance: %s%n", instanceId); 25 | 26 | // Block until the orchestration completes. Then print the final status, which includes the output. 27 | OrchestrationMetadata completedInstance = client.waitForInstanceCompletion( 28 | instanceId, 29 | Duration.ofSeconds(30), 30 | true); 31 | System.out.printf("Orchestration completed: %s%n", completedInstance); 32 | System.out.printf("Output: %s%n", completedInstance.readOutputAs(String.class)); 33 | 34 | // Shutdown the server and exit 35 | server.stop(); 36 | } 37 | 38 | // class OrderInfo {} 39 | 40 | // class ServicesAPIs { 41 | // public static final String UpdateInventory = ""; 42 | // } 43 | 44 | // class RetryPolicy { 45 | // public RetryPolicy(int maxAttempts, Duration retryInterval) { 46 | 47 | // } 48 | // } 49 | 50 | private static DurableTaskGrpcWorker createTaskHubServer() { 51 | DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); 52 | 53 | // Orchestrations can be defined inline as anonymous classes or as concrete classes 54 | builder.addOrchestration(new TaskOrchestrationFactory() { 55 | @Override 56 | public String getName() { return "ActivityChaining"; } 57 | 58 | @Override 59 | public TaskOrchestration create() { 60 | return ctx -> { 61 | // the input is JSON and can be deserialized into the specified type 62 | String input = ctx.getInput(String.class); 63 | 64 | String x = ctx.callActivity("Reverse", input, String.class).await(); 65 | String y = ctx.callActivity("Capitalize", x, String.class).await(); 66 | String z = ctx.callActivity("ReplaceWhitespace", y, String.class).await(); 67 | 68 | // Save the output of the orchestration 69 | ctx.complete(z); 70 | 71 | // var order = ctx.getInput(OrderInfo.class); // deserialize order info from JSON to an object 72 | // var retryPolicy = RetryPolicy.newBuilder() 73 | // .setMaxRetries(100) 74 | // .setRetryInterval(Duration.ofSeconds(30)); 75 | 76 | // // Remove the inventory 77 | // var itemDetails = ctx.callActivity("UpdateInventory", order.ShoppingCart, retryPolicy).await(); 78 | // var paymentSummary = ctx.callActivity("ProcessPayment", order.PaymentDetails, retryPolicy).await(); 79 | // ctx.callActivity("ShipInventory", itemDetails, retryPolicy).await(); 80 | 81 | // var orderSummary = getOrderSummary(itemDetails, paymentSummary); 82 | // ctx.callActivity("SendConfirmationEmail", itemDetails, retryPolicy).await(); 83 | }; 84 | } 85 | }); 86 | 87 | // Activities can be defined inline as anonymous classes or as concrete classes 88 | builder.addActivity(new TaskActivityFactory() { 89 | @Override 90 | public String getName() { return "Reverse"; } 91 | 92 | @Override 93 | public TaskActivity create() { 94 | return ctx -> { 95 | String input = ctx.getInput(String.class); 96 | StringBuilder builder = new StringBuilder(input); 97 | builder.reverse(); 98 | return builder.toString(); 99 | }; 100 | } 101 | }); 102 | 103 | builder.addActivity(new TaskActivityFactory() { 104 | @Override 105 | public String getName() { return "Capitalize"; } 106 | 107 | @Override 108 | public TaskActivity create() { 109 | return ctx -> ctx.getInput(String.class).toUpperCase(); 110 | } 111 | }); 112 | 113 | builder.addActivity(new TaskActivityFactory() { 114 | @Override 115 | public String getName() { return "ReplaceWhitespace"; } 116 | 117 | @Override 118 | public TaskActivity create() { 119 | return ctx -> { 120 | String input = ctx.getInput(String.class); 121 | return input.trim().replaceAll("\\s", "-"); 122 | }; 123 | } 124 | }); 125 | 126 | return builder.build(); 127 | } 128 | } -------------------------------------------------------------------------------- /samples/src/main/java/io/durabletask/samples/FanOutFanInPattern.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package io.durabletask.samples; 4 | 5 | import com.microsoft.durabletask.*; 6 | 7 | import java.io.IOException; 8 | import java.time.Duration; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.StringTokenizer; 12 | import java.util.concurrent.TimeoutException; 13 | import java.util.stream.Collectors; 14 | 15 | class FanOutFanInPattern { 16 | 17 | public static void main(String[] args) throws IOException, InterruptedException, TimeoutException { 18 | // The TaskHubServer listens over gRPC for new orchestration and activity execution requests 19 | final DurableTaskGrpcWorker worker = createWorker(); 20 | 21 | // Start the server to begin processing orchestration and activity requests 22 | worker.start(); 23 | 24 | // Start a new instance of the registered "ActivityChaining" orchestration 25 | final DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); 26 | 27 | // The input is an arbitrary list of strings. 28 | List listOfStrings = Arrays.asList( 29 | "Hello, world!", 30 | "The quick brown fox jumps over the lazy dog.", 31 | "If a tree falls in the forest and there is no one there to hear it, does it make a sound?", 32 | "The greatest glory in living lies not in never falling, but in rising every time we fall.", 33 | "Always remember that you are absolutely unique. Just like everyone else."); 34 | 35 | // Schedule an orchestration which will reliably count the number of words in all the given sentences. 36 | String instanceId = client.scheduleNewOrchestrationInstance( 37 | "FanOutFanIn_WordCount", 38 | new NewOrchestrationInstanceOptions().setInput(listOfStrings)); 39 | System.out.printf("Started new orchestration instance: %s%n", instanceId); 40 | 41 | // Block until the orchestration completes. Then print the final status, which includes the output. 42 | OrchestrationMetadata completedInstance = client.waitForInstanceCompletion( 43 | instanceId, 44 | Duration.ofSeconds(30), 45 | true); 46 | System.out.printf("Orchestration completed: %s%n", completedInstance); 47 | System.out.printf("Output: %d%n", completedInstance.readOutputAs(int.class)); 48 | 49 | // Shutdown the server and exit 50 | worker.stop(); 51 | } 52 | 53 | private static DurableTaskGrpcWorker createWorker() { 54 | DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); 55 | 56 | // Orchestrations can be defined inline as anonymous classes or as concrete classes 57 | builder.addOrchestration(new TaskOrchestrationFactory() { 58 | @Override 59 | public String getName() { return "FanOutFanIn_WordCount"; } 60 | 61 | @Override 62 | public TaskOrchestration create() { 63 | return ctx -> { 64 | // The input is a list of objects that need to be operated on. 65 | // In this example, inputs are expected to be strings. 66 | List inputs = ctx.getInput(List.class); 67 | 68 | // Fan-out to multiple concurrent activity invocations, each of which does a word count. 69 | List> tasks = inputs.stream() 70 | .map(input -> ctx.callActivity("CountWords", input.toString(), Integer.class)) 71 | .collect(Collectors.toList()); 72 | 73 | // Fan-in to get the total word count from all the individual activity results. 74 | List allWordCountResults = ctx.allOf(tasks).await(); 75 | int totalWordCount = allWordCountResults.stream().mapToInt(Integer::intValue).sum(); 76 | 77 | // Save the final result as the orchestration output. 78 | ctx.complete(totalWordCount); 79 | }; 80 | } 81 | }); 82 | 83 | // Activities can be defined inline as anonymous classes or as concrete classes 84 | builder.addActivity(new TaskActivityFactory() { 85 | @Override 86 | public String getName() { return "CountWords"; } 87 | 88 | @Override 89 | public TaskActivity create() { 90 | return ctx -> { 91 | // Take the string input and count the number of words (tokens) it contains. 92 | String input = ctx.getInput(String.class); 93 | StringTokenizer tokenizer = new StringTokenizer(input); 94 | return tokenizer.countTokens(); 95 | }; 96 | } 97 | }); 98 | 99 | return builder.build(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /samples/src/main/java/io/durabletask/samples/OrchestrationController.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package io.durabletask.samples; 4 | 5 | import com.microsoft.durabletask.*; 6 | 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | public class OrchestrationController { 13 | 14 | final DurableTaskClient client; 15 | 16 | public OrchestrationController() { 17 | this.client = new DurableTaskGrpcClientBuilder().build(); 18 | } 19 | 20 | @GetMapping("/hello") 21 | public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) { 22 | return String.format("Hello, %s!", name); 23 | } 24 | 25 | @GetMapping("/placeOrder") 26 | public NewOrderResponse placeOrder(@RequestParam(value = "item") String item) { 27 | String instanceId = this.client.scheduleNewOrchestrationInstance( 28 | "ProcessOrderOrchestration", 29 | new NewOrchestrationInstanceOptions().setInput(item)); 30 | return new NewOrderResponse(instanceId); 31 | } 32 | 33 | private class NewOrderResponse { 34 | private final String instanceId; 35 | 36 | public NewOrderResponse(String instanceId) { 37 | this.instanceId = instanceId; 38 | } 39 | 40 | public String getInstanceId() { 41 | return this.instanceId; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /samples/src/main/java/io/durabletask/samples/WebApplication.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | package io.durabletask.samples; 4 | 5 | import com.microsoft.durabletask.*; 6 | 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | 10 | @SpringBootApplication 11 | public class WebApplication { 12 | 13 | public static void main(String[] args) throws InterruptedException { 14 | DurableTaskGrpcWorker server = createTaskHubServer(); 15 | server.start(); 16 | 17 | System.out.println("Starting up Spring web API..."); 18 | SpringApplication.run(WebApplication.class, args); 19 | } 20 | 21 | private static DurableTaskGrpcWorker createTaskHubServer() { 22 | DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder(); 23 | 24 | // Orchestrations can be defined inline as anonymous classes or as concrete classes 25 | builder.addOrchestration(new TaskOrchestrationFactory() { 26 | @Override 27 | public String getName() { return "ProcessOrderOrchestration"; } 28 | 29 | @Override 30 | public TaskOrchestration create() { 31 | return ctx -> { 32 | // the input is JSON and can be deserialized into the specified type 33 | String input = ctx.getInput(String.class); 34 | 35 | String x = ctx.callActivity("Task1", input, String.class).await(); 36 | String y = ctx.callActivity("Task2", x, String.class).await(); 37 | String z = ctx.callActivity("Task3", y, String.class).await(); 38 | 39 | ctx.complete(z); 40 | }; 41 | } 42 | }); 43 | 44 | // Activities can be defined inline as anonymous classes or as concrete classes 45 | builder.addActivity(new TaskActivityFactory() { 46 | @Override 47 | public String getName() { return "Task1"; } 48 | 49 | @Override 50 | public TaskActivity create() { 51 | return ctx -> { 52 | String input = ctx.getInput(String.class); 53 | sleep(10000); 54 | return input + "|" + ctx.getName(); 55 | }; 56 | } 57 | }); 58 | 59 | builder.addActivity(new TaskActivityFactory() { 60 | @Override 61 | public String getName() { return "Task2"; } 62 | 63 | @Override 64 | public TaskActivity create() { 65 | return ctx -> { 66 | String input = ctx.getInput(String.class); 67 | sleep(10000); 68 | return input + "|" + ctx.getName(); 69 | }; 70 | } 71 | }); 72 | 73 | builder.addActivity(new TaskActivityFactory() { 74 | @Override 75 | public String getName() { return "Task3"; } 76 | 77 | @Override 78 | public TaskActivity create() { 79 | return ctx -> { 80 | String input = ctx.getInput(String.class); 81 | sleep(10000); 82 | return input + "|" + ctx.getName(); 83 | }; 84 | } 85 | }); 86 | 87 | return builder.build(); 88 | } 89 | 90 | private static void sleep(int millis) { 91 | try { 92 | Thread.sleep(millis); 93 | } catch (InterruptedException e) { 94 | // ignore 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /samples/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | durable.task.endpoint=https://dtwbwuks01-gqa9c3ccgbgd.uksouth.durabletask.io 2 | durable.task.taskHubName=dtwbwuks01th01 3 | durable.task.connection-string=Endpoint=https://dtwbwuks01-gqa9c3ccgbgd.uksouth.durabletask.io;TaskHub=dtwbwuks01th01;Authentication=DefaultAzure; 4 | # Add logging configuration 5 | logging.level.com.microsoft.durabletask=DEBUG 6 | logging.level.root=INFO 7 | 8 | server.port=8082 -------------------------------------------------------------------------------- /samples/src/main/resources/web-app-to-durable-task-scheduler-sample.http: -------------------------------------------------------------------------------- 1 | ### Variables 2 | @instanceId = c32752e9-ae3e-42f7-9dc3-0e91277b1286 3 | 4 | ### Create a new order 5 | POST http://localhost:8082/api/orders 6 | Content-Type: application/json 7 | 8 | { 9 | "orderId": "ORD123456", 10 | "customerId": "CUST789", 11 | "amount": 125.50, 12 | "items": [ 13 | { 14 | "productId": "PROD001", 15 | "quantity": 2, 16 | "price": 49.99 17 | }, 18 | { 19 | "productId": "PROD002", 20 | "quantity": 1, 21 | "price": 25.52 22 | } 23 | ], 24 | "shippingAddress": { 25 | "street": "123 Main St", 26 | "city": "Seattle", 27 | "state": "WA", 28 | "zipCode": "98101", 29 | "country": "USA" 30 | } 31 | } 32 | 33 | ### Get order by instance ID 34 | # Replace the instanceId variable with the actual value returned from the create order request 35 | GET http://localhost:8082/api/orders/{{instanceId}} 36 | Content-Type: application/json 37 | 38 | ### Example with invalid order (amount is 0) 39 | POST http://localhost:8082/api/orders 40 | Content-Type: application/json 41 | 42 | { 43 | "orderId": "ORD123457", 44 | "customerId": "CUST789", 45 | "amount": 0, 46 | "items": [ 47 | { 48 | "productId": "PROD003", 49 | "quantity": 1, 50 | "price": 0 51 | } 52 | ], 53 | "shippingAddress": { 54 | "street": "123 Main St", 55 | "city": "Seattle", 56 | "state": "WA", 57 | "zipCode": "98101", 58 | "country": "USA" 59 | } 60 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'durabletask-java' 2 | 3 | include ":client" 4 | include ":azurefunctions" 5 | include ":azuremanaged" 6 | include ":samples" 7 | include ":samples-azure-functions" 8 | include ":endtoendtests" 9 | 10 | --------------------------------------------------------------------------------