├── internal
└── durabletask-protobuf
│ ├── PROTO_SOURCE_COMMIT_HASH
│ └── README.md
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── libs
└── durabletask-grpc-0.1.0.jar
├── azurefunctions
├── src
│ └── main
│ │ ├── resources
│ │ └── META-INF
│ │ │ └── services
│ │ │ └── com.microsoft.azure.functions.internal.spi.middleware.Middleware
│ │ └── java
│ │ └── com
│ │ └── microsoft
│ │ └── durabletask
│ │ └── azurefunctions
│ │ ├── DurableActivityTrigger.java
│ │ ├── DurableOrchestrationTrigger.java
│ │ ├── DurableClientInput.java
│ │ ├── HttpManagementPayload.java
│ │ └── internal
│ │ └── middleware
│ │ └── OrchestrationMiddleware.java
├── spotbugs-exclude.xml
└── build.gradle
├── endtoendtests
├── local.settings.json
├── Dockerfile
├── host.json
├── build.gradle
├── e2e-test-setup.ps1
└── src
│ └── main
│ └── java
│ └── com
│ └── functions
│ ├── ResumeSuspend.java
│ ├── ThenChain.java
│ ├── AzureFunctions.java
│ ├── ContinueAsNew.java
│ ├── DeserializeErrorTest.java
│ └── Versioning.java
├── samples-azure-functions
├── local.settings.json
├── Dockerfile
├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── functions
│ │ ├── model
│ │ └── Person.java
│ │ ├── ResumeSuspend.java
│ │ ├── ThenChain.java
│ │ ├── SubOrchestrator.java
│ │ ├── WaitExternalEvent.java
│ │ ├── ContinueAsNew.java
│ │ └── AzureFunctions.java
├── host.json
├── build.gradle
└── e2e-test-setup.ps1
├── settings.gradle
├── .gitignore
├── CODEOWNERS
├── SUPPORT.md
├── client
├── src
│ ├── main
│ │ └── java
│ │ │ └── com
│ │ │ └── microsoft
│ │ │ └── durabletask
│ │ │ ├── NonDeterministicOrchestratorException.java
│ │ │ ├── TaskActivityFactory.java
│ │ │ ├── TaskOrchestrationFactory.java
│ │ │ ├── TaskCanceledException.java
│ │ │ ├── PurgeResult.java
│ │ │ ├── RetryHandler.java
│ │ │ ├── interruption
│ │ │ ├── ContinueAsNewInterruption.java
│ │ │ └── OrchestratorBlockedException.java
│ │ │ ├── TaskActivityContext.java
│ │ │ ├── TaskOrchestratorResult.java
│ │ │ ├── OrchestratorFunction.java
│ │ │ ├── util
│ │ │ ├── UUIDGenerator.java
│ │ │ └── VersionUtils.java
│ │ │ ├── OrchestrationStatusQueryResult.java
│ │ │ ├── JacksonDataConverter.java
│ │ │ ├── TaskOptions.java
│ │ │ ├── TaskActivity.java
│ │ │ ├── CompositeTaskFailedException.java
│ │ │ ├── NewSubOrchestrationInstanceOptions.java
│ │ │ ├── TaskFailedException.java
│ │ │ ├── Helpers.java
│ │ │ ├── RetryContext.java
│ │ │ ├── TaskActivityExecutor.java
│ │ │ ├── DataConverter.java
│ │ │ ├── DurableTaskGrpcClientBuilder.java
│ │ │ ├── DurableTaskGrpcWorkerVersioningOptions.java
│ │ │ ├── Task.java
│ │ │ ├── OrchestrationRuntimeStatus.java
│ │ │ ├── TaskOrchestration.java
│ │ │ ├── PurgeInstanceCriteria.java
│ │ │ └── NewOrchestrationInstanceOptions.java
│ └── test
│ │ └── java
│ │ └── com
│ │ └── microsoft
│ │ └── durabletask
│ │ └── IntegrationTestBase.java
└── spotbugs-exclude.xml
├── samples
├── src
│ └── main
│ │ ├── resources
│ │ ├── application.properties
│ │ └── web-app-to-durable-task-scheduler-sample.http
│ │ └── java
│ │ └── io
│ │ └── durabletask
│ │ └── samples
│ │ ├── OrchestrationController.java
│ │ ├── WebApplication.java
│ │ └── FanOutFanInPattern.java
└── build.gradle
├── .dockerignore
├── CODE_OF_CONDUCT.md
├── azuremanaged
├── spotbugs-exclude.xml
└── src
│ ├── main
│ ├── resources
│ │ └── VersionInfo.java.template
│ └── java
│ │ └── com
│ │ └── microsoft
│ │ └── durabletask
│ │ └── azuremanaged
│ │ ├── DurableTaskUserAgentUtil.java
│ │ ├── AccessTokenCache.java
│ │ └── DurableTaskSchedulerClientExtensions.java
│ └── test
│ └── java
│ └── com
│ └── microsoft
│ └── durabletask
│ └── azuremanaged
│ ├── AccessTokenCacheTest.java
│ └── DurableTaskSchedulerClientExtensionsUserAgentTest.java
├── eng
├── ci
│ ├── code-mirror.yml
│ ├── official-build.yml
│ └── release.yml
└── templates
│ └── build.yml
├── .github
├── pull_request_template.md
└── policies
│ └── resourceManagement.yml
├── LICENSE
├── azdevops-pipeline
└── build-release-artifacts.yml
├── SECURITY.md
├── gradlew.bat
├── CHANGELOG.md
├── CONTRIBUTING.md
└── README.md
/internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH:
--------------------------------------------------------------------------------
1 | fbe5bb20835678099fc51a44993ed9b045dee5a6
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/durabletask-java/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/libs/durabletask-grpc-0.1.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/durabletask-java/HEAD/libs/durabletask-grpc-0.1.0.jar
--------------------------------------------------------------------------------
/azurefunctions/src/main/resources/META-INF/services/com.microsoft.azure.functions.internal.spi.middleware.Middleware:
--------------------------------------------------------------------------------
1 | com.microsoft.durabletask.azurefunctions.internal.middleware.OrchestrationMiddleware
--------------------------------------------------------------------------------
/endtoendtests/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true",
5 | "FUNCTIONS_WORKER_RUNTIME": "java"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/samples-azure-functions/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true",
5 | "FUNCTIONS_WORKER_RUNTIME": "java"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/azurefunctions/spotbugs-exclude.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/azuremanaged/spotbugs-exclude.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 | "defaultVersion": "1.0"
18 | }
19 | },
20 | "extensionBundle": {
21 | "id": "Microsoft.Azure.Functions.ExtensionBundle",
22 | "version": "[4.26.0, 5.0.0)"
23 | }
24 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/endtoendtests/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id "com.microsoft.azure.azurefunctions" version "1.11.1"
4 | }
5 |
6 | group 'com.durabletask.endtoend'
7 | version '0.0.0-SNAPSHOT'
8 |
9 | repositories {
10 | mavenLocal()
11 | maven {
12 | url "https://oss.sonatype.org/content/repositories/snapshots/"
13 | }
14 | mavenCentral()
15 | }
16 |
17 | dependencies {
18 | implementation project(':client')
19 | implementation project(':azurefunctions')
20 |
21 | implementation 'com.microsoft.azure.functions:azure-functions-java-library:3.1.0'
22 | testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2'
23 | testImplementation 'io.rest-assured:rest-assured:5.3.0'
24 | testImplementation 'io.rest-assured:json-path:5.3.0'
25 |
26 | }
27 |
28 | sourceCompatibility = '1.8'
29 | targetCompatibility = '1.8'
30 |
31 | compileJava.options.encoding = 'UTF-8'
32 |
33 | task endToEndTest(type: Test) {
34 | useJUnitPlatform {
35 | includeTags 'e2e'
36 | }
37 | dependsOn build
38 | testLogging.showStandardStreams = true
39 | }
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/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id "com.microsoft.azure.azurefunctions" version "1.11.1"
4 | }
5 |
6 | group 'com.functions'
7 | version '0.1.0-SNAPSHOT'
8 |
9 | repositories {
10 | mavenLocal()
11 | maven {
12 | url "https://oss.sonatype.org/content/repositories/snapshots/"
13 | }
14 | mavenCentral()
15 | }
16 |
17 | dependencies {
18 | implementation project(':client')
19 | implementation project(':azurefunctions')
20 |
21 | implementation 'com.microsoft.azure.functions:azure-functions-java-library:3.0.0'
22 | testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2'
23 | testImplementation 'io.rest-assured:rest-assured:5.3.0'
24 | testImplementation 'io.rest-assured:json-path:5.3.0'
25 |
26 | }
27 |
28 | sourceCompatibility = '1.8'
29 | targetCompatibility = '1.8'
30 |
31 | compileJava.options.encoding = 'UTF-8'
32 |
33 | task sampleTest(type: Test) {
34 | useJUnitPlatform {
35 | includeTags 'sampleTest'
36 | }
37 | dependsOn build
38 | testLogging.showStandardStreams = true
39 | }
40 |
41 | azurefunctions {
42 | resourceGroup = 'java-functions-group'
43 | appName = 'azure-functions-sample'
44 | pricingTier = 'Consumption'
45 | region = 'westus'
46 | runtime {
47 | os = 'Windows'
48 | javaVersion = 'Java 8'
49 | }
50 | auth {
51 | type = 'azure_cli'
52 | }
53 | localDebug = "transport=dt_socket,server=y,suspend=n,address=5005"
54 | }
55 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | import org.springframework.web.util.HtmlUtils;
12 |
13 | @RestController
14 | public class OrchestrationController {
15 |
16 | final DurableTaskClient client;
17 |
18 | public OrchestrationController() {
19 | this.client = new DurableTaskGrpcClientBuilder().build();
20 | }
21 |
22 | @GetMapping("/hello")
23 | public String greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
24 | return String.format("Hello, %s!", HtmlUtils.htmlEscape(name));
25 | }
26 |
27 | @GetMapping("/placeOrder")
28 | public NewOrderResponse placeOrder(@RequestParam(value = "item") String item) {
29 | String instanceId = this.client.scheduleNewOrchestrationInstance(
30 | "ProcessOrderOrchestration",
31 | new NewOrchestrationInstanceOptions().setInput(item));
32 | return new NewOrderResponse(instanceId);
33 | }
34 |
35 | private class NewOrderResponse {
36 | private final String instanceId;
37 |
38 | public NewOrderResponse(String instanceId) {
39 | this.instanceId = instanceId;
40 | }
41 |
42 | public String getInstanceId() {
43 | return this.instanceId;
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/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); /* CodeQL [SM05136] Suppressed: SHA1 is not used for cryptographic purposes here. The information being hashed is not sensitive,
33 | and the goal is to generate a deterministic Guid. We cannot update to SHA2-based algorithms without breaking
34 | customers' inflight orchestrations. */
35 | } catch (NoSuchAlgorithmException e) {
36 | throw new RuntimeException(String.format("%s not supported.", algorithm));
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 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/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 |
--------------------------------------------------------------------------------
/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 | --env 'FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI=https://functionscdnstaging.azureedge.net/public' `
33 | $ImageName
34 | }
35 |
36 | if ($sleep -gt 0) {
37 | # The container needs a bit more time before it can start receiving requests
38 | Write-Host "Sleeping for $Sleep seconds to let the container finish initializing..." -ForegroundColor Yellow
39 | Start-Sleep -Seconds $Sleep
40 | }
41 |
42 | # Check to see what containers are running
43 | docker ps
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/src/main/java/com/microsoft/durabletask/NewSubOrchestrationInstanceOptions.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.durabletask;
2 |
3 | /**
4 | * Options for starting a new sub-orchestration instance.
5 | */
6 | public class NewSubOrchestrationInstanceOptions extends TaskOptions {
7 |
8 | private String instanceId;
9 | private String version;
10 |
11 | /**
12 | * Creates default options for the sub-orchestration. Useful for chaining
13 | * when a RetryPolicy or RetryHandler is not needed.
14 | */
15 | public NewSubOrchestrationInstanceOptions() {
16 | super((RetryPolicy) null);
17 | }
18 |
19 | /**
20 | * Creates options with a retry policy for the sub-orchestration.
21 | * @param retryPolicy The retry policy to use for the sub-orchestration.
22 | */
23 | public NewSubOrchestrationInstanceOptions(RetryPolicy retryPolicy) {
24 | super(retryPolicy);
25 | }
26 |
27 | /**
28 | * Creates options with a retry handler for the sub-orchestration.
29 | * @param retryHandler The retry handler to use for the sub-orchestration.
30 | */
31 | public NewSubOrchestrationInstanceOptions(RetryHandler retryHandler) {
32 | super(retryHandler);
33 | }
34 |
35 | /**
36 | * Sets the version for the sub-orchestration instance.
37 | * @param version The version string to use.
38 | * @return This options object for chaining.
39 | */
40 | public NewSubOrchestrationInstanceOptions setVersion(String version) {
41 | this.version = version;
42 | return this;
43 | }
44 |
45 | /**
46 | * Gets the version for the sub-orchestration instance.
47 | * @return The version string, or null if not set.
48 | */
49 | public String getVersion() {
50 | return version;
51 | }
52 |
53 | /**
54 | * Sets a custom instance ID for the sub-orchestration.
55 | * @param instanceId The custom instance ID to use.
56 | * @return This options object for chaining.
57 | */
58 | public NewSubOrchestrationInstanceOptions setInstanceId(String instanceId) {
59 | this.instanceId = instanceId;
60 | return this;
61 | }
62 |
63 | /**
64 | * Gets the custom instance ID for the sub-orchestration.
65 | * @return The instance ID, or null if not set.
66 | */
67 | public String getInstanceId() {
68 | return instanceId;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | String defaultVersion;
16 |
17 | /**
18 | * Sets the {@link DataConverter} to use for converting serializable data payloads.
19 | *
20 | * @param dataConverter the {@link DataConverter} to use for converting serializable data payloads
21 | * @return this builder object
22 | */
23 | public DurableTaskGrpcClientBuilder dataConverter(DataConverter dataConverter) {
24 | this.dataConverter = dataConverter;
25 | return this;
26 | }
27 |
28 | /**
29 | * Sets the gRPC channel to use for communicating with the sidecar process.
30 | *
31 | * This builder method allows you to provide your own gRPC channel for communicating with the Durable Task sidecar
32 | * endpoint. Channels provided using this method won't be closed when the client is closed.
33 | * Rather, the caller remains responsible for shutting down the channel after disposing the client.
34 | *
35 | * If not specified, a gRPC channel will be created automatically for each constructed
36 | * {@link DurableTaskClient}.
37 | *
38 | * @param channel the gRPC channel to use
39 | * @return this builder object
40 | */
41 | public DurableTaskGrpcClientBuilder grpcChannel(Channel channel) {
42 | this.channel = channel;
43 | return this;
44 | }
45 |
46 | /**
47 | * Sets the gRPC endpoint port to connect to. If not specified, the default Durable Task port number will be used.
48 | *
49 | * @param port the gRPC endpoint port to connect to
50 | * @return this builder object
51 | */
52 | public DurableTaskGrpcClientBuilder port(int port) {
53 | this.port = port;
54 | return this;
55 | }
56 |
57 | /**
58 | * Sets the default version that orchestrations will be created with.
59 | *
60 | * @param defaultVersion the default version to create orchestrations with
61 | * @return this builder object
62 | */
63 | public DurableTaskGrpcClientBuilder defaultVersion(String defaultVersion) {
64 | this.defaultVersion = defaultVersion;
65 | return this;
66 | }
67 |
68 | /**
69 | * Initializes a new {@link DurableTaskClient} object with the settings specified in the current builder object.
70 | * @return a new {@link DurableTaskClient} object
71 | */
72 | public DurableTaskClient build() {
73 | return new DurableTaskGrpcClient(this);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorkerVersioningOptions.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 | package com.microsoft.durabletask;
4 |
5 | /**
6 | * Options for configuring versioning behavior in the DurableTaskGrpcWorker.
7 | */
8 | public final class DurableTaskGrpcWorkerVersioningOptions {
9 |
10 | /**
11 | * Strategy for matching versions.
12 | * NONE: No version matching is performed.
13 | * STRICT: The version must match exactly.
14 | * CURRENTOROLDER: The version must be the current version or older.
15 | */
16 | public enum VersionMatchStrategy {
17 | NONE,
18 | STRICT,
19 | CURRENTOROLDER;
20 | }
21 |
22 | /**
23 | * Strategy for handling version mismatches.
24 | * REJECT: Reject the orchestration if the version does not match. The orchestration will be retried later.
25 | * FAIL: Fail the orchestration if the version does not match.
26 | */
27 | public enum VersionFailureStrategy {
28 | REJECT,
29 | FAIL;
30 | }
31 |
32 | private final String version;
33 | private final String defaultVersion;
34 | private final VersionMatchStrategy matchStrategy;
35 | private final VersionFailureStrategy failureStrategy;
36 |
37 | /**
38 | * Constructor for DurableTaskGrpcWorkerVersioningOptions.
39 | * @param version the version that is matched against orchestrations
40 | * @param defaultVersion the default version used when starting sub orchestrations from this worker
41 | * @param matchStrategy the strategy for matching versions
42 | * @param failureStrategy the strategy for handling version mismatches
43 | */
44 | public DurableTaskGrpcWorkerVersioningOptions(String version, String defaultVersion, VersionMatchStrategy matchStrategy, VersionFailureStrategy failureStrategy) {
45 | this.version = version;
46 | this.defaultVersion = defaultVersion;
47 | this.matchStrategy = matchStrategy;
48 | this.failureStrategy = failureStrategy;
49 | }
50 |
51 | /**
52 | * Gets the version that is matched against orchestrations.
53 | * @return the version that is matched against orchestrations
54 | */
55 | public String getVersion() {
56 | return version;
57 | }
58 |
59 | /**
60 | * Gets the default version used when starting sub orchestrations from this worker.
61 | * @return the default version used when starting sub orchestrations from this worker
62 | */
63 | public String getDefaultVersion() {
64 | return defaultVersion;
65 | }
66 |
67 | /**
68 | * Gets the strategy for matching versions.
69 | * @return the strategy for matching versions
70 | */
71 | public VersionMatchStrategy getMatchStrategy() {
72 | return matchStrategy;
73 | }
74 |
75 | /**
76 | * Gets the strategy for handling version mismatches.
77 | * @return the strategy for handling version mismatches
78 | */
79 | public VersionFailureStrategy getFailureStrategy() {
80 | return failureStrategy;
81 | }
82 | }
--------------------------------------------------------------------------------
/client/src/main/java/com/microsoft/durabletask/util/VersionUtils.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 | package com.microsoft.durabletask.util;
4 |
5 | public class VersionUtils {
6 |
7 | /**
8 | * Compares two version strings. We manually attempt to parse the version strings as a Version object
9 | * was not introduced until Java 9 and we compile as far back as Java 8.
10 | */
11 | public static int compareVersions(String v1, String v2) {
12 | if (v1 == null && v2 == null) {
13 | return 0;
14 | }
15 | if (v1 == null) {
16 | return -1;
17 | }
18 | if (v2 == null) {
19 | return 1;
20 | }
21 |
22 | // Check if both versions are in standard format (e.g., "1.0.0")
23 | boolean isV1Standard = isStandardVersionString(v1);
24 | boolean isV2Standard = isStandardVersionString(v2);
25 |
26 | // If both versions were successfully normalized, compare them as structured versions
27 | if (isV1Standard && isV2Standard) {
28 | return compareStandardVersions(v1, v2);
29 | }
30 |
31 | // If either version couldn't be normalized, fall back to string comparison
32 | return v1.compareTo(v2);
33 | }
34 |
35 | /**
36 | * Checks if the version string is in a standard format (e.g., "1.0.0").
37 | * @param version The version string to check
38 | * @return true if the version is in standard format, false otherwise
39 | */
40 | private static boolean isStandardVersionString(String version) {
41 | if (version == null || version.trim().isEmpty()) {
42 | return false;
43 | }
44 |
45 | String[] parts = version.split("\\.");
46 |
47 | // Check if all parts are numeric
48 | for (String part : parts) {
49 | if (tryParseInt(part) == null) {
50 | return false; // Contains non-numeric part, cannot normalize
51 | }
52 | }
53 | return true;
54 | }
55 |
56 | /**
57 | * Compares two standard version strings part by part.
58 | * @param v1 First standard version string
59 | * @param v2 Second standard version string
60 | * @return Negative if v1 < v2, positive if v1 > v2, zero if equal
61 | */
62 | private static int compareStandardVersions(String v1, String v2) {
63 | String[] parts1 = v1.split("\\.");
64 | String[] parts2 = v2.split("\\.");
65 |
66 | int length = Math.max(parts1.length, parts2.length);
67 | for (int i = 0; i < length; i++) {
68 | int p1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0;
69 | int p2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0;
70 | if (p1 != p2) {
71 | return p1 - p2;
72 | }
73 | }
74 |
75 | return 0;
76 | }
77 |
78 | private static Integer tryParseInt(String part) {
79 | try {
80 | return Integer.parseInt(part);
81 | } catch (NumberFormatException e) {
82 | return null; // indicates non-numeric part
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.6.2'
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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | import java.util.Collections;
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 | /**
11 | * Options for starting a new instance of an orchestration.
12 | */
13 | public final class NewOrchestrationInstanceOptions {
14 | private String version;
15 | private String instanceId;
16 | private Object input;
17 | private Instant startTime;
18 | private Map tags;
19 |
20 | /**
21 | * Default constructor for the {@link NewOrchestrationInstanceOptions} class.
22 | */
23 | public NewOrchestrationInstanceOptions() {
24 | }
25 |
26 | /**
27 | * Sets the version of the orchestration to start.
28 | *
29 | * @param version the user-defined version of the orchestration
30 | * @return this {@link NewOrchestrationInstanceOptions} object
31 | */
32 | public NewOrchestrationInstanceOptions setVersion(String version) {
33 | this.version = version;
34 | return this;
35 | }
36 |
37 | /**
38 | * Sets the instance ID of the orchestration to start.
39 | *
40 | * If no instance ID is configured, the orchestration will be created with a randomly generated instance ID.
41 | *
42 | * @param instanceId the ID of the new orchestration instance
43 | * @return this {@link NewOrchestrationInstanceOptions} object
44 | */
45 | public NewOrchestrationInstanceOptions setInstanceId(String instanceId) {
46 | this.instanceId = instanceId;
47 | return this;
48 | }
49 |
50 | /**
51 | * Sets the input of the orchestration to start.
52 | *
53 | * There are no restrictions on the type of inputs that can be used except that they must be serializable using
54 | * the {@link DataConverter} that was configured for the {@link DurableTaskClient} at creation time.
55 | *
56 | * @param input the input of the new orchestration instance
57 | * @return this {@link NewOrchestrationInstanceOptions} object
58 | */
59 | public NewOrchestrationInstanceOptions setInput(Object input) {
60 | this.input = input;
61 | return this;
62 | }
63 |
64 | /**
65 | * Sets the start time of the new orchestration instance.
66 | *
67 | * By default, new orchestration instances start executing immediately. This method can be used
68 | * to start them at a specific time in the future.
69 | *
70 | * @param startTime the start time of the new orchestration instance
71 | * @return this {@link NewOrchestrationInstanceOptions} object
72 | */
73 | public NewOrchestrationInstanceOptions setStartTime(Instant startTime) {
74 | this.startTime = startTime;
75 | return this;
76 | }
77 |
78 | /**
79 | * Sets the tags associated with the new orchestration instance.
80 | *
81 | * @param tags the tags to associate with the new orchestration instance
82 | * @return this {@link NewOrchestrationInstanceOptions} object
83 | */
84 | public NewOrchestrationInstanceOptions setTags(Map tags) {
85 | if (this.tags == null) {
86 | this.tags = new HashMap<>(tags);
87 | } else {
88 | this.tags.putAll(tags);
89 | }
90 | return this;
91 | }
92 |
93 | /**
94 | * Gets the user-specified version of the new orchestration.
95 | *
96 | * @return the user-specified version of the new orchestration.
97 | */
98 | public String getVersion() {
99 | return this.version;
100 | }
101 |
102 | /**
103 | * Gets the instance ID of the new orchestration.
104 | *
105 | * @return the instance ID of the new orchestration.
106 | */
107 | public String getInstanceId() {
108 | return this.instanceId;
109 | }
110 |
111 | /**
112 | * Gets the input of the new orchestration.
113 | *
114 | * @return the input of the new orchestration.
115 | */
116 | public Object getInput() {
117 | return this.input;
118 | }
119 |
120 | /**
121 | * Gets the configured start time of the new orchestration instance.
122 | *
123 | * @return the configured start time of the new orchestration instance.
124 | */
125 | public Instant getStartTime() {
126 | return this.startTime;
127 | }
128 |
129 | /**
130 | * Gets the tags associated with the new orchestration instance. If no tags were set, an empty map is returned.
131 | *
132 | * @return a map of tags associated with the new orchestration instance.
133 | */
134 | public Map getTags() {
135 | return this.tags == null ? Collections.emptyMap() : new HashMap<>(this.tags);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientOptions;
11 | import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions;
12 | import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerOptions;
13 |
14 | import io.grpc.Channel;
15 | import io.grpc.ManagedChannel;
16 | public class IntegrationTestBase {
17 | protected static final Duration defaultTimeout = Duration.ofSeconds(10);
18 |
19 | // All tests that create a server should save it to this variable for proper shutdown
20 | private DurableTaskGrpcWorker server; // All tests that create a client are responsible for closing their own gRPC channel
21 | private ManagedChannel workerChannel;
22 | private ManagedChannel clientChannel;
23 |
24 | @AfterEach
25 | private void shutdown() {
26 | if (this.server != null) {
27 | this.server.stop();
28 | this.server = null;
29 | }
30 |
31 | if (this.workerChannel != null) {
32 | this.workerChannel.shutdownNow();
33 | this.workerChannel = null;
34 | }
35 |
36 | if (this.clientChannel != null) {
37 | this.clientChannel.shutdownNow();
38 | this.clientChannel = null;
39 | }
40 | }
41 |
42 | protected TestDurableTaskWorkerBuilder createWorkerBuilder() {
43 | DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions()
44 | .setEndpointAddress("http://localhost:4001")
45 | .setTaskHubName("default")
46 | .setCredential(null)
47 | .setAllowInsecureCredentials(true);
48 | Channel grpcChannel = options.createGrpcChannel();
49 | this.workerChannel = (ManagedChannel) grpcChannel;
50 | return new TestDurableTaskWorkerBuilder(
51 | new DurableTaskGrpcWorkerBuilder()
52 | .grpcChannel(grpcChannel));
53 | }
54 |
55 | protected DurableTaskGrpcClientBuilder createClientBuilder() {
56 | DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions()
57 | .setEndpointAddress("http://localhost:4001")
58 | .setTaskHubName("default")
59 | .setCredential(null)
60 | .setAllowInsecureCredentials(true);
61 | Channel grpcChannel = options.createGrpcChannel();
62 | // The channel returned is actually a ManagedChannel, so we can safely cast it
63 | this.clientChannel = (ManagedChannel) grpcChannel;
64 | return new DurableTaskGrpcClientBuilder()
65 | .grpcChannel(grpcChannel);
66 | }
67 |
68 | public class TestDurableTaskWorkerBuilder {
69 | final DurableTaskGrpcWorkerBuilder innerBuilder;
70 |
71 | private TestDurableTaskWorkerBuilder() {
72 | this.innerBuilder = new DurableTaskGrpcWorkerBuilder();
73 | }
74 |
75 | private TestDurableTaskWorkerBuilder(DurableTaskGrpcWorkerBuilder innerBuilder) {
76 | this.innerBuilder = innerBuilder;
77 | }
78 |
79 | public DurableTaskGrpcWorker buildAndStart() {
80 | DurableTaskGrpcWorker server = this.innerBuilder.build();
81 | IntegrationTestBase.this.server = server;
82 | server.start();
83 | return server;
84 | }
85 |
86 | public TestDurableTaskWorkerBuilder setMaximumTimerInterval(Duration maximumTimerInterval) {
87 | this.innerBuilder.maximumTimerInterval(maximumTimerInterval);
88 | return this;
89 | }
90 |
91 | public TestDurableTaskWorkerBuilder addOrchestrator(
92 | String name,
93 | TaskOrchestration implementation) {
94 | this.innerBuilder.addOrchestration(new TaskOrchestrationFactory() {
95 | @Override
96 | public String getName() { return name; }
97 |
98 | @Override
99 | public TaskOrchestration create() { return implementation; }
100 | });
101 | return this;
102 | }
103 |
104 | public IntegrationTests.TestDurableTaskWorkerBuilder addActivity(
105 | String name,
106 | TaskActivity implementation)
107 | {
108 | this.innerBuilder.addActivity(new TaskActivityFactory() {
109 | @Override
110 | public String getName() { return name; }
111 |
112 | @Override
113 | public TaskActivity create() { return implementation; }
114 | });
115 | return this;
116 | }
117 |
118 | public TestDurableTaskWorkerBuilder useVersioning(DurableTaskGrpcWorkerVersioningOptions options) {
119 | this.innerBuilder.useVersioning(options);
120 | return this;
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/endtoendtests/src/main/java/com/functions/Versioning.java:
--------------------------------------------------------------------------------
1 | package com.functions;
2 |
3 | import java.util.Optional;
4 |
5 | import com.microsoft.azure.functions.ExecutionContext;
6 | import com.microsoft.azure.functions.HttpMethod;
7 | import com.microsoft.azure.functions.HttpRequestMessage;
8 | import com.microsoft.azure.functions.HttpResponseMessage;
9 | import com.microsoft.azure.functions.annotation.AuthorizationLevel;
10 | import com.microsoft.azure.functions.annotation.FunctionName;
11 | import com.microsoft.azure.functions.annotation.HttpTrigger;
12 | import com.microsoft.durabletask.DurableTaskClient;
13 | import com.microsoft.durabletask.NewOrchestrationInstanceOptions;
14 | import com.microsoft.durabletask.NewSubOrchestrationInstanceOptions;
15 | import com.microsoft.durabletask.TaskOrchestrationContext;
16 | import com.microsoft.durabletask.azurefunctions.DurableActivityTrigger;
17 | import com.microsoft.durabletask.azurefunctions.DurableClientContext;
18 | import com.microsoft.durabletask.azurefunctions.DurableClientInput;
19 | import com.microsoft.durabletask.azurefunctions.DurableOrchestrationTrigger;
20 |
21 | public class Versioning {
22 | /**
23 | * This HTTP-triggered function starts the orchestration.
24 | */
25 | @FunctionName("StartVersionedOrchestration")
26 | public HttpResponseMessage startOrchestration(
27 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request,
28 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext,
29 | final ExecutionContext context) {
30 | context.getLogger().info("Java HTTP trigger processed a request.");
31 |
32 | DurableTaskClient client = durableContext.getClient();
33 | String queryVersion = request.getQueryParameters().getOrDefault("version", "");
34 | context.getLogger().info(String.format("Received version '%s' from the query string", queryVersion));
35 | String instanceId = null;
36 | if (queryVersion.equals("default")) {
37 | instanceId = client.scheduleNewOrchestrationInstance("VersionedOrchestrator");
38 | } else {
39 | instanceId = client.scheduleNewOrchestrationInstance("VersionedOrchestrator", new NewOrchestrationInstanceOptions().setVersion(queryVersion));
40 | }
41 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId);
42 | return durableContext.createCheckStatusResponse(request, instanceId);
43 | }
44 |
45 | /**
46 | * This HTTP-triggered function starts the orchestration.
47 | */
48 | @FunctionName("StartVersionedSubOrchestration")
49 | public HttpResponseMessage startSubOrchestration(
50 | @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request,
51 | @DurableClientInput(name = "durableContext") DurableClientContext durableContext,
52 | final ExecutionContext context) {
53 | context.getLogger().info("Java HTTP trigger processed a request.");
54 |
55 | DurableTaskClient client = durableContext.getClient();
56 | String queryVersion = request.getQueryParameters().getOrDefault("version", "");
57 | String instanceId = null;
58 | if (queryVersion.equals("default")) {
59 | instanceId = client.scheduleNewOrchestrationInstance("VersionedSubOrchestrator");
60 | } else {
61 | instanceId = client.scheduleNewOrchestrationInstance("VersionedSubOrchestrator", queryVersion);
62 | }
63 | context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId);
64 | return durableContext.createCheckStatusResponse(request, instanceId);
65 | }
66 |
67 | @FunctionName("VersionedOrchestrator")
68 | public String versionedOrchestrator(
69 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
70 | return ctx.callActivity("SayVersion", ctx.getVersion(), String.class).await();
71 | }
72 |
73 | @FunctionName("VersionedSubOrchestrator")
74 | public String versionedSubOrchestrator(
75 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
76 | String subVersion = ctx.getInput(String.class);
77 | NewSubOrchestrationInstanceOptions options = new NewSubOrchestrationInstanceOptions();
78 | if (subVersion != null) {
79 | options.setVersion(subVersion);
80 | }
81 | return ctx.callSubOrchestrator("SubOrchestrator", null, null, options, String.class).await();
82 | }
83 |
84 | @FunctionName("SubOrchestrator")
85 | public String subOrchestrator(
86 | @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
87 | return ctx.callActivity("SayVersion", ctx.getVersion(), String.class).await();
88 | }
89 |
90 | @FunctionName("SayVersion")
91 | public String sayVersion(
92 | @DurableActivityTrigger(name = "version") String version,
93 | final ExecutionContext context) {
94 | version = version.replaceAll("\"", "");
95 | context.getLogger().info(String.format("Called with version: '%s'", version));
96 | return String.format("Version: '%s'", version);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Unreleased
2 |
3 | ## v1.6.2
4 | * Fixing gRPC channel shutdown ([#249](https://github.com/microsoft/durabletask-java/pull/249))
5 |
6 | ## v1.6.1
7 | * Add support for default versions in Durable Function sub-orchestrations ([#241](https://github.com/microsoft/durabletask-java/pull/241))
8 |
9 | ## v1.6.0
10 | * Add support for tags when creating new orchestrations ([#231](https://github.com/microsoft/durabletask-java/pull/230))
11 | * Add versioning support for client and worker ([#224](https://github.com/microsoft/durabletask-java/pull/224))
12 | * Add tracing support to send information to the WebJobs extension ([#211](https://github.com/microsoft/durabletask-java/pull/211))
13 |
14 | ## v1.5.2
15 | * Add distributed tracing support for Azure Functions client scenarios ([#211](https://github.com/microsoft/durabletask-java/pull/211))
16 |
17 | ## v1.5.1
18 | * Improve logging for unexpected connection failures in DurableTaskGrpcWorker ([#216](https://github.com/microsoft/durabletask-java/pull/216/files))
19 | * Add User-Agent Header to gRPC Metadata ([#213](https://github.com/microsoft/durabletask-java/pull/213))
20 | * Update protobuf definitions to include new properties in OrchestratorRequest and update source commit hash ([#214](https://github.com/microsoft/durabletask-java/pull/214))
21 | * DTS Support ([#201](https://github.com/microsoft/durabletask-java/pull/201))
22 | * Add automatic proto file download and commit hash tracking during build ([#207](https://github.com/microsoft/durabletask-java/pull/207))
23 | * Fix infinite loop when use continueasnew after wait external event ([#183](https://github.com/microsoft/durabletask-java/pull/183))
24 | * Fix the issue "Deserialize Exception got swallowed when use anyOf with external event." ([#185](https://github.com/microsoft/durabletask-java/pull/185))
25 |
26 | ## v1.5.0
27 | * Fix exception type issue when using `RetriableTask` in fan in/out pattern ([#174](https://github.com/microsoft/durabletask-java/pull/174))
28 | * Add implementation to generate name-based deterministic UUID ([#176](https://github.com/microsoft/durabletask-java/pull/176))
29 | * Update dependencies to resolve CVEs ([#177](https://github.com/microsoft/durabletask-java/pull/177))
30 |
31 |
32 | ## v1.4.0
33 |
34 | ### Updates
35 | * Refactor `createTimer` to be non-blocking ([#161](https://github.com/microsoft/durabletask-java/pull/161))
36 |
37 | ## v1.3.0
38 | * Refactor `RetriableTask` and add new `CompoundTask`, fixing Fan-out/Fan-in stuck when using `RetriableTask` ([#157](https://github.com/microsoft/durabletask-java/pull/157))
39 |
40 | ## v1.2.0
41 |
42 | ### Updates
43 | * Add `thenAccept` and `thenApply` to `Task` interface ([#148](https://github.com/microsoft/durabletask-java/pull/148))
44 | * Support Suspend and Resume Client APIs ([#151](https://github.com/microsoft/durabletask-java/pull/151))
45 | * Support restartInstance and pass restartPostUri in HttpManagementPayload ([#108](https://github.com/microsoft/durabletask-java/issues/108))
46 | * Improve `TaskOrchestrationContext#continueAsNew` method so it doesn't require `return` statement right after it anymore ([#149](https://github.com/microsoft/durabletask-java/pull/149))
47 |
48 | ## v1.1.1
49 |
50 | ### Updates
51 | * Fix exception occurring when invoking the `TaskOrchestrationContext#continueAsNew` method ([#118](https://github.com/microsoft/durabletask-java/issues/118))
52 |
53 | ## v1.1.0
54 |
55 | ### Updates
56 | * Fix the potential NPE issue of `DurableTaskClient#terminate` method ([#104](https://github.com/microsoft/durabletask-java/issues/104))
57 | * Add waitForCompletionOrCreateCheckStatusResponse client API ([#115](https://github.com/microsoft/durabletask-java/pull/115))
58 | * Support long timers by breaking up into smaller timers ([#114](https://github.com/microsoft/durabletask-java/issues/114))
59 |
60 | ## v1.0.0
61 |
62 | ### New
63 |
64 | * Add CHANGELOG.md file to track changes across versions
65 | * Added createHttpManagementPayload API ([#63](https://github.com/microsoft/durabletask-java/issues/63))
66 |
67 | ### Updates
68 |
69 | * update DataConverterException with detail error message ([#78](https://github.com/microsoft/durabletask-java/issues/78))
70 | * update OrchestratorBlockedEvent and TaskFailedException to be unchecked exceptions ([#88](https://github.com/microsoft/durabletask-java/issues/88))
71 | * 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))
72 |
73 | ### Breaking changes
74 |
75 | * Use java worker middleware to avoid wrapper method when create orchestrator function ([#87](https://github.com/microsoft/durabletask-java/pull/87))
76 | * Fixed DurableClientContext.createCheckStatusResponse to return 202 ([#92](https://github.com/microsoft/durabletask-java/pull/92))
77 | * context.allOf() now throws CompositeTaskFailedException(RuntimeException) when one or more tasks fail ([#54](https://github.com/microsoft/durabletask-java/issues/54))
78 | * Updated `DurableTaskClient.purgeInstances(...)` to take a timeout parameter and throw `TimeoutException` ([#37](https://github.com/microsoft/durabletask-java/issues/37))
79 | * The `waitForInstanceStart(...)` and `waitForInstanceCompletion(...)` methods of the `DurableTaskClient` interface now throw a checked `TimeoutException`
80 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Durable Task Client SDK for Java
2 |
3 | [](https://github.com/microsoft/durabletask-java/actions/workflows/build-validation.yml)
4 | [](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 | [](https://mvnrepository.com/artifact/com.microsoft/durabletask-client/1.0.0) |
82 | | Durable Task - 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 |
--------------------------------------------------------------------------------
/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 | .setAllowInsecureCredentials(tokenCredential == null));
96 | }
97 |
98 | // Private helper methods to reduce code duplication
99 | private static DurableTaskGrpcClientBuilder createBuilderFromOptions(DurableTaskSchedulerClientOptions options) {
100 | Channel grpcChannel = options.createGrpcChannel();
101 | return new DurableTaskGrpcClientBuilder().grpcChannel(grpcChannel);
102 | }
103 |
104 | private static void configureBuilder(DurableTaskGrpcClientBuilder builder, DurableTaskSchedulerClientOptions options) {
105 | Channel grpcChannel = options.createGrpcChannel();
106 | builder.grpcChannel(grpcChannel);
107 | }
108 | }
--------------------------------------------------------------------------------