diff --git a/samples/durable-functions/java/Entities/README.md b/samples/durable-functions/java/Entities/README.md new file mode 100644 index 0000000..8a9fc0f --- /dev/null +++ b/samples/durable-functions/java/Entities/README.md @@ -0,0 +1,65 @@ +# Durable Entities — Durable Functions Java Sample + +Java | Durable Functions + +## Description + +This sample demonstrates Durable Entities with Java using the Durable Task Scheduler backend. It includes: + +1. A `Counter` durable entity with `add`, `subtract`, `get`, and `reset` operations +2. An HTTP endpoint for direct entity signals +3. An HTTP endpoint for direct entity reads +4. An orchestration that signals the entity and reads its final value + +## Prerequisites + +1. [Java 11+](https://adoptium.net/) (JDK) +2. [Maven](https://maven.apache.org/download.cgi) +3. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local) +4. [Docker](https://www.docker.com/products/docker-desktop/) (for the DTS emulator and Azurite) + +## Quick Run + +1. Start the Durable Task Scheduler emulator: + ```bash + docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + ``` + +2. Start Azurite for Azure Functions host storage: + ```bash + docker run --name azurite -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite + ``` + +3. Build the sample: + ```bash + cd samples/durable-functions/java/Entities + mvn clean package + ``` + +4. Run the Functions host: + ```bash + mvn azure-functions:run + ``` + +5. Signal the entity directly: + ```bash + curl -X POST "http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=7" + ``` + +6. Read the entity state: + ```bash + curl "http://localhost:7071/api/GetCounter?key=my-counter" + ``` + +7. Start the orchestration: + ```bash + curl -X POST "http://localhost:7071/api/StartCounterOrchestration?key=my-orch-counter" + ``` + +8. View orchestration activity in the dashboard: http://localhost:8082 + +## Notes + +- `mvn clean package` is configured to stage the Azure Functions app so `mvn azure-functions:run` works as a separate second step. +- `AzureWebJobsStorage=UseDevelopmentStorage=true` requires Azurite to be running locally. +- `SignalCounter` rejects `op=get`; use `GetCounter` to read entity state. \ No newline at end of file diff --git a/samples/durable-functions/java/Entities/demo.http b/samples/durable-functions/java/Entities/demo.http new file mode 100644 index 0000000..bffa36b --- /dev/null +++ b/samples/durable-functions/java/Entities/demo.http @@ -0,0 +1,27 @@ +# For more info on HTTP files go to https://aka.ms/vs/httpfile + +### Signal the counter entity: add 10 +POST http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=10 + +### Signal the counter entity: add 5 +POST http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=5 + +### Signal the counter entity: subtract 3 +POST http://localhost:7071/api/SignalCounter?key=my-counter&op=subtract&value=3 + +### Signal the counter entity: reset +POST http://localhost:7071/api/SignalCounter?key=my-counter&op=reset + +### Get the current counter value +GET http://localhost:7071/api/GetCounter?key=my-counter + +### Start the counter orchestration +POST http://localhost:7071/api/StartCounterOrchestration?key=my-orch-counter + +### Check orchestration status +# Copy the statusQueryGetUri from the StartCounterOrchestration response above +# and paste it below (it includes the required system key): +GET http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}?code={systemKey} + +### Get the orchestration-managed counter value +GET http://localhost:7071/api/GetCounter?key=my-orch-counter diff --git a/samples/durable-functions/java/Entities/host.json b/samples/durable-functions/java/Entities/host.json new file mode 100644 index 0000000..431660d --- /dev/null +++ b/samples/durable-functions/java/Entities/host.json @@ -0,0 +1,21 @@ +{ + "version": "2.0", + "logging": { + "logLevel": { + "DurableTask.Core": "Warning" + } + }, + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/samples/durable-functions/java/Entities/local.settings.json b/samples/durable-functions/java/Entities/local.settings.json new file mode 100644 index 0000000..89a4b63 --- /dev/null +++ b/samples/durable-functions/java/Entities/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "java", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + } +} diff --git a/samples/durable-functions/java/Entities/pom.xml b/samples/durable-functions/java/Entities/pom.xml new file mode 100644 index 0000000..d034fc4 --- /dev/null +++ b/samples/durable-functions/java/Entities/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.example + durable-functions-entities + 1.0-SNAPSHOT + jar + + + UTF-8 + 11 + 1.41.0 + 3.2.4 + 1.9.0 + + + + + com.microsoft.azure.functions + azure-functions-java-library + ${azure.functions.java.library.version} + + + com.microsoft + durabletask-azure-functions + ${durabletask.azure.functions.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + ${java.version} + ${java.version} + + + + com.microsoft.azure + azure-functions-maven-plugin + ${azure.functions.maven.plugin.version} + + + package-functions + + package + + + + + durable-functions-entities + java-functions-group + java-functions-app-service-plan + westus2 + + linux + 11 + + + + + + diff --git a/samples/durable-functions/java/Entities/src/main/java/com/example/CounterEntity.java b/samples/durable-functions/java/Entities/src/main/java/com/example/CounterEntity.java new file mode 100644 index 0000000..0f25701 --- /dev/null +++ b/samples/durable-functions/java/Entities/src/main/java/com/example/CounterEntity.java @@ -0,0 +1,50 @@ +package com.example; + +import com.microsoft.durabletask.AbstractTaskEntity; +import com.microsoft.durabletask.TaskEntityOperation; + +import java.util.logging.Logger; + +/** + * A durable entity that maintains a counter state. + *

+ * Supports operations: add, subtract, get, reset. + * Public methods are automatically dispatched based on operation name. + */ +public class CounterEntity extends AbstractTaskEntity { + private static final Logger logger = Logger.getLogger(CounterEntity.class.getName()); + + @Override + protected Integer initializeState(TaskEntityOperation operation) { + return 0; + } + + @Override + protected Class getStateType() { + return Integer.class; + } + + public void add(int value) { + this.state += value; + logger.info(String.format("Counter '%s': Added %d, new value: %d", + this.context.getId().getKey(), value, this.state)); + } + + public void subtract(int value) { + this.state -= value; + logger.info(String.format("Counter '%s': Subtracted %d, new value: %d", + this.context.getId().getKey(), value, this.state)); + } + + public int get() { + logger.info(String.format("Counter '%s': Current value: %d", + this.context.getId().getKey(), this.state)); + return this.state; + } + + public void reset() { + this.state = 0; + logger.info(String.format("Counter '%s': Reset to 0", + this.context.getId().getKey())); + } +} diff --git a/samples/durable-functions/java/Entities/src/main/java/com/example/Functions.java b/samples/durable-functions/java/Entities/src/main/java/com/example/Functions.java new file mode 100644 index 0000000..5936ddc --- /dev/null +++ b/samples/durable-functions/java/Entities/src/main/java/com/example/Functions.java @@ -0,0 +1,136 @@ +package com.example; + +import com.microsoft.azure.functions.*; +import com.microsoft.azure.functions.annotation.*; +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azurefunctions.*; + +/** + * Azure Functions with Durable Entities. + *

+ * This sample demonstrates: + * 1. A counter entity that supports add, subtract, get, and reset operations + * 2. An orchestration that signals and calls the counter entity + * 3. HTTP triggers to start orchestrations and signal entities directly + */ +public class Functions { + private static final String DEFAULT_ENTITY_KEY = "my-counter"; + + + /** + * Entity function for the counter entity. + * The @DurableEntityTrigger binds to incoming entity operation requests. + */ + @FunctionName("Counter") + public String counterEntity( + @DurableEntityTrigger(name = "req", entityName = "Counter") String req) { + return EntityRunner.loadAndRun(req, CounterEntity::new); + } + + /** + * Orchestration that interacts with the counter entity. + * Signals add/subtract operations and calls get to retrieve the value. + */ + @FunctionName("CounterOrchestration") + public String counterOrchestration( + @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) { + + String entityKey = ctx.getInput(String.class); + if (entityKey == null || entityKey.isBlank()) { + entityKey = DEFAULT_ENTITY_KEY; + } + EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey); + + // Signal entity operations (fire-and-forget) + ctx.signalEntity(entityId, "add", 10); + ctx.signalEntity(entityId, "add", 5); + ctx.signalEntity(entityId, "subtract", 3); + + // Call entity and wait for result + int value = ctx.callEntity(entityId, "get", Integer.class).await(); + + return "Counter '" + entityKey + "' final value: " + value; + } + + /** + * HTTP trigger that starts the counter orchestration. + * POST /api/StartCounterOrchestration?key=my-counter + */ + @FunctionName("StartCounterOrchestration") + public HttpResponseMessage startCounterOrchestration( + @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) + HttpRequestMessage request, + @DurableClientInput(name = "durableContext") DurableClientContext durableContext) { + + DurableTaskClient client = durableContext.getClient(); + String entityKey = request.getQueryParameters().getOrDefault("key", DEFAULT_ENTITY_KEY); + + String instanceId = client.scheduleNewOrchestrationInstance( + "CounterOrchestration", + new NewOrchestrationInstanceOptions().setInput(entityKey)); + + return durableContext.createCheckStatusResponse(request, instanceId); + } + + /** + * HTTP trigger that signals the counter entity directly. + * POST /api/SignalCounter?key=my-counter&op=add&value=10 + */ + @FunctionName("SignalCounter") + public HttpResponseMessage signalCounter( + @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) + HttpRequestMessage request, + @DurableClientInput(name = "durableContext") DurableClientContext durableContext) { + + String entityKey = request.getQueryParameters().getOrDefault("key", DEFAULT_ENTITY_KEY); + String operation = request.getQueryParameters().getOrDefault("op", "add"); + String valueStr = request.getQueryParameters().getOrDefault("value", "1"); + + EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey); + + if ("get".equals(operation)) { + return request.createResponseBuilder(HttpStatus.BAD_REQUEST) + .body("Use GET /api/GetCounter?key= to read entity state.") + .build(); + } + + if ("reset".equals(operation)) { + durableContext.signalEntity(entityId, operation); + } else { + int value = Integer.parseInt(valueStr); + durableContext.signalEntity(entityId, operation, value); + } + + return request.createResponseBuilder(HttpStatus.ACCEPTED) + .body("Signal sent: " + operation + " on entity '" + entityKey + "'") + .build(); + } + + /** + * HTTP trigger that gets the current state of a counter entity. + * GET /api/GetCounter?key=my-counter + */ + @FunctionName("GetCounter") + public HttpResponseMessage getCounter( + @HttpTrigger(name = "req", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS) + HttpRequestMessage request, + @DurableClientInput(name = "durableContext") DurableClientContext durableContext) { + + String entityKey = request.getQueryParameters().getOrDefault("key", DEFAULT_ENTITY_KEY); + EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey); + + EntityMetadata metadata = durableContext.getEntityMetadata(entityId, true); + + if (metadata == null) { + return request.createResponseBuilder(HttpStatus.NOT_FOUND) + .body("Entity '" + entityKey + "' not found") + .build(); + } + + Integer state = metadata.readStateAs(Integer.class); + return request.createResponseBuilder(HttpStatus.OK) + .header("Content-Type", "application/json") + .body("{\"key\": \"" + entityKey + "\", \"value\": " + state + "}") + .build(); + } +} diff --git a/samples/durable-functions/java/HelloCities/README.md b/samples/durable-functions/java/HelloCities/README.md index 35228cb..4bd1a39 100644 --- a/samples/durable-functions/java/HelloCities/README.md +++ b/samples/durable-functions/java/HelloCities/README.md @@ -14,33 +14,48 @@ This quickstart demonstrates Durable Functions with Java using the Durable Task 1. [Java 11+](https://adoptium.net/) (JDK) 2. [Maven](https://maven.apache.org/download.cgi) 3. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local) -4. [Docker](https://www.docker.com/products/docker-desktop/) (for the emulator) +4. [Docker](https://www.docker.com/products/docker-desktop/) (for the DTS emulator and Azurite) ## Quick Run -1. Start the emulator: +1. Start the Durable Task Scheduler emulator: ```bash - docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest ``` -2. Build and run: +2. Start Azurite for Azure Functions host storage: + ```bash + docker run --name azurite -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite + ``` + +3. Build the sample: ```bash cd samples/durable-functions/java/HelloCities mvn clean package - func start ``` -3. Trigger the function chaining orchestration: +4. Run the Functions host: + ```bash + mvn azure-functions:run + ``` + +5. Trigger the function chaining orchestration: ```bash curl -X POST http://localhost:7071/api/StartChaining ``` -4. Trigger the fan-out/fan-in orchestration: +6. Trigger the fan-out/fan-in orchestration: ```bash curl -X POST http://localhost:7071/api/StartFanOutFanIn ``` -5. View in the dashboard: http://localhost:8082 +7. View in the dashboard: http://localhost:8082 + +## Notes + +- `mvn clean package` is configured to stage the Azure Functions app so `mvn azure-functions:run` works as a separate second step. +- `AzureWebJobsStorage=UseDevelopmentStorage=true` requires Azurite to be running locally. +- `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` in `local.settings.json` points to the local DTS emulator on `http://localhost:8080`. ## Expected Output diff --git a/samples/durable-functions/java/HelloCities/host.json b/samples/durable-functions/java/HelloCities/host.json index 4020661..431660d 100644 --- a/samples/durable-functions/java/HelloCities/host.json +++ b/samples/durable-functions/java/HelloCities/host.json @@ -9,7 +9,7 @@ "durableTask": { "hubName": "default", "storageProvider": { - "type": "durabletask-scheduler", + "type": "azureManaged", "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" } } diff --git a/samples/durable-functions/java/HelloCities/pom.xml b/samples/durable-functions/java/HelloCities/pom.xml index c0431f9..2e64d41 100644 --- a/samples/durable-functions/java/HelloCities/pom.xml +++ b/samples/durable-functions/java/HelloCities/pom.xml @@ -45,6 +45,14 @@ com.microsoft.azure azure-functions-maven-plugin ${azure.functions.maven.plugin.version} + + + package-functions + + package + + + durable-functions-hello-cities java-functions-group diff --git a/samples/durable-functions/java/README.md b/samples/durable-functions/java/README.md index 8c52853..e13c9f7 100644 --- a/samples/durable-functions/java/README.md +++ b/samples/durable-functions/java/README.md @@ -3,3 +3,26 @@ [Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview) is an extension of Azure Functions that lets you write stateful functions in a serverless compute environment. These samples demonstrate how to use Durable Functions with Java and the Durable Task Scheduler backend. + +## Available Samples + +- **HelloCities**: Function chaining and fan-out/fan-in patterns +- **Entities**: Durable entities with a counter example (add, subtract, get, reset operations) + +## Local Run Model + +Before running either Java sample locally, start the required services: + +```bash +docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest +docker run --name azurite -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite +``` + +Then run the sample using the same two Maven commands as the Java Durable Functions quickstart: + +```bash +mvn clean package +mvn azure-functions:run +``` + +`mvn clean package` stages the Azure Functions app, and `mvn azure-functions:run` starts the local Functions host from that staged output. diff --git a/samples/durable-task-sdks/java/README.md b/samples/durable-task-sdks/java/README.md index 045db62..17672f1 100644 --- a/samples/durable-task-sdks/java/README.md +++ b/samples/durable-task-sdks/java/README.md @@ -41,6 +41,7 @@ Each sample demonstrates a different orchestration pattern: - **human-interaction**: Integration of human approval steps in orchestrations - **monitoring**: Monitoring and tracking orchestration progress - **sub-orchestrations**: Composing multiple orchestrations hierarchically +- **entities**: Stateful entities that maintain state across operations (counter example) ## Running the Samples @@ -77,6 +78,10 @@ cd monitoring # For sub-orchestrations sample cd sub-orchestrations ./gradlew runSubOrchestrationPattern + +# For entities sample +cd entities +./gradlew runEntitiesPattern ``` ## Testing diff --git a/samples/durable-task-sdks/java/entities/Dockerfile b/samples/durable-task-sdks/java/entities/Dockerfile new file mode 100644 index 0000000..154aba8 --- /dev/null +++ b/samples/durable-task-sdks/java/entities/Dockerfile @@ -0,0 +1,15 @@ +FROM eclipse-temurin:25-jdk + +# Install dos2unix to fix line endings +RUN apt-get update && apt-get install -y dos2unix && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy all files +COPY . . + +# Fix line endings for gradlew +RUN dos2unix gradlew && chmod +x gradlew + +# Run the gradle task +CMD ["./gradlew", "runEntitiesPattern"] diff --git a/samples/durable-task-sdks/java/entities/azure.yaml b/samples/durable-task-sdks/java/entities/azure.yaml new file mode 100644 index 0000000..b71b345 --- /dev/null +++ b/samples/durable-task-sdks/java/entities/azure.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +metadata: + template: entities-java +name: dts-entities +infra: + path: ../../../infra +services: + sampleapp: + project: . + language: java + host: containerapp + apiVersion: 2025-01-01 + docker: + path: ./Dockerfile diff --git a/samples/durable-task-sdks/java/entities/build.gradle b/samples/durable-task-sdks/java/entities/build.gradle new file mode 100644 index 0000000..9e42cc5 --- /dev/null +++ b/samples/durable-task-sdks/java/entities/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java' + id 'application' +} + +group 'io.durabletask' +version = '0.1.0' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} +def grpcVersion = '1.80.0' +base { archivesName = 'durabletask-samples' } + +repositories { + mavenLocal() + mavenCentral() +} + +task runEntitiesPattern(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'io.durabletask.samples.EntitiesPattern' + systemProperty 'logback.configurationFile', 'src/main/resources/logback-spring.xml' +} + +dependencies { + implementation("com.microsoft:durabletask-client:1.9.0") + implementation("com.microsoft:durabletask-azuremanaged:1.9.0") + + // Logging dependencies + implementation 'ch.qos.logback:logback-classic:1.5.32' + implementation 'org.slf4j:slf4j-api:2.0.17' + + // https://github.com/grpc/grpc-java#download + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" + implementation 'com.azure:azure-identity:1.18.2' +} diff --git a/samples/durable-task-sdks/java/entities/gradle/wrapper/gradle-wrapper.properties b/samples/durable-task-sdks/java/entities/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3499ded --- /dev/null +++ b/samples/durable-task-sdks/java/entities/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/durable-task-sdks/java/entities/gradlew b/samples/durable-task-sdks/java/entities/gradlew new file mode 100644 index 0000000..0262dcb --- /dev/null +++ b/samples/durable-task-sdks/java/entities/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/durable-task-sdks/java/entities/gradlew.bat b/samples/durable-task-sdks/java/entities/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/samples/durable-task-sdks/java/entities/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/durable-task-sdks/java/entities/settings.gradle b/samples/durable-task-sdks/java/entities/settings.gradle new file mode 100644 index 0000000..03ebdfe --- /dev/null +++ b/samples/durable-task-sdks/java/entities/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'durabletask-entities-sample' diff --git a/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/CounterEntity.java b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/CounterEntity.java new file mode 100644 index 0000000..203c54b --- /dev/null +++ b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/CounterEntity.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.durabletask.samples; + +import com.microsoft.durabletask.AbstractTaskEntity; +import com.microsoft.durabletask.TaskEntityOperation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A durable entity that maintains a counter state. + *

+ * Supports operations: add, subtract, get, reset. + * Public methods are automatically dispatched based on operation name. + */ +public class CounterEntity extends AbstractTaskEntity { + private static final Logger logger = LoggerFactory.getLogger(CounterEntity.class); + + @Override + protected Integer initializeState(TaskEntityOperation operation) { + return 0; + } + + @Override + protected Class getStateType() { + return Integer.class; + } + + public void add(int value) { + this.state += value; + logger.info("Counter '{}': Added {}, new value: {}", this.context.getId().getKey(), value, this.state); + } + + public void subtract(int value) { + this.state -= value; + logger.info("Counter '{}': Subtracted {}, new value: {}", this.context.getId().getKey(), value, this.state); + } + + public int get() { + logger.info("Counter '{}': Current value: {}", this.context.getId().getKey(), this.state); + return this.state; + } + + public void reset() { + this.state = 0; + logger.info("Counter '{}': Reset to 0", this.context.getId().getKey()); + } +} diff --git a/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/EntitiesPattern.java b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/EntitiesPattern.java new file mode 100644 index 0000000..7c85f10 --- /dev/null +++ b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/EntitiesPattern.java @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.durabletask.samples; + +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions; +import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +import com.azure.core.credential.TokenCredential; +import com.azure.identity.*; + +/** + * Demonstrates the Durable Entities pattern using the Java Durable Task SDK. + *

+ * This sample: + * 1. Registers a counter entity that supports add, subtract, get, and reset operations + * 2. Registers an orchestration that interacts with the counter entity + * 3. Signals the entity directly from the client + * 4. Runs an orchestration that signals and calls the entity + */ +final class EntitiesPattern { + private static final Logger logger = LoggerFactory.getLogger(EntitiesPattern.class); + private static final String DEFAULT_ENTITY_KEY = "my-counter"; + + public static void main(String[] args) throws IOException, InterruptedException, TimeoutException { + // Get environment variables for endpoint and taskhub with defaults + String endpoint = System.getenv("ENDPOINT"); + String taskHubName = System.getenv("TASKHUB"); + String connectionString = System.getenv("DURABLE_TASK_CONNECTION_STRING"); + + if (connectionString == null) { + if (endpoint != null && taskHubName != null) { + String hostAddress = endpoint; + if (endpoint.contains(";")) { + hostAddress = endpoint.split(";")[0]; + } + + boolean isLocalEmulator = endpoint.equals("http://localhost:8080"); + + if (isLocalEmulator) { + connectionString = String.format("Endpoint=%s;TaskHub=%s;Authentication=None", hostAddress, taskHubName); + logger.info("Using local emulator with no authentication"); + } else { + connectionString = String.format("Endpoint=%s;TaskHub=%s;Authentication=DefaultAzure", hostAddress, taskHubName); + logger.info("Using Azure endpoint with DefaultAzure authentication"); + } + + logger.info("Using endpoint: {}", endpoint); + logger.info("Using task hub: {}", taskHubName); + } else { + connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + logger.info("Using default local emulator connection string"); + } + } + + // Check if we're running in Azure with a managed identity + String clientId = System.getenv("AZURE_MANAGED_IDENTITY_CLIENT_ID"); + TokenCredential credential = null; + if (clientId != null && !clientId.isEmpty()) { + logger.info("Using Managed Identity with client ID: {}", clientId); + credential = new ManagedIdentityCredentialBuilder().clientId(clientId).build(); + + AccessToken token = credential.getToken( + new TokenRequestContext().addScopes("https://management.azure.com/.default")) + .block(Duration.ofSeconds(10)); + logger.info("Successfully authenticated with Managed Identity, expires at {}", token.getExpiresAt()); + + } else if (!connectionString.contains("Authentication=None")) { + logger.info("No Managed Identity client ID found, using DefaultAzure authentication"); + } + + // Create worker using Azure-managed extensions + DurableTaskGrpcWorker worker = (credential != null + ? DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(endpoint, taskHubName, credential) + : DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(connectionString)) + // Register the counter entity + .addEntity("counter", CounterEntity::new) + // Register the orchestration that interacts with the entity + .addOrchestration(new TaskOrchestrationFactory() { + @Override + public String getName() { return "CounterWorkflow"; } + + @Override + public TaskOrchestration create() { + return ctx -> { + String entityKey = ctx.getInput(String.class); + if (entityKey == null || entityKey.isBlank()) { + entityKey = DEFAULT_ENTITY_KEY; + } + EntityInstanceId entityId = new EntityInstanceId("counter", Objects.requireNonNull(entityKey)); + + // Signal entity operations (fire-and-forget) + ctx.signalEntity(entityId, "add", 10); + ctx.signalEntity(entityId, "add", 5); + ctx.signalEntity(entityId, "subtract", 3); + + // Call entity and wait for result + int value = ctx.callEntity(entityId, "get", Integer.class).await(); + + ctx.complete("Counter '" + entityKey + "' final value: " + value); + }; + } + }) + .build(); + + // Start the worker and wait for it to connect + worker.start(); + Thread.sleep(5000); + + // Create client using Azure-managed extensions + DurableTaskClient client = (credential != null + ? DurableTaskSchedulerClientExtensions.createClientBuilder(endpoint, taskHubName, credential) + : DurableTaskSchedulerClientExtensions.createClientBuilder(connectionString)).build(); + + String entityKey = DEFAULT_ENTITY_KEY; + if (args.length > 0 && args[0] != null && !args[0].isBlank()) { + entityKey = args[0]; + } + + DurableEntityClient entityClient = client.getEntities(); + + // Demonstrate direct entity signaling from client + logger.info("=== Direct Entity Operations ==="); + EntityInstanceId entityId = new EntityInstanceId("counter", Objects.requireNonNull(entityKey)); + + logger.info("Signaling entity '{}' to add 100", entityKey); + entityClient.signalEntity(entityId, "add", 100); + Thread.sleep(2000); + + logger.info("Signaling entity '{}' to subtract 25", entityKey); + entityClient.signalEntity(entityId, "subtract", 25); + Thread.sleep(2000); + + // Run orchestrations that interact with entities + logger.info("=== Orchestration-based Entity Operations ==="); + int totalOrchestrations = 5; + int completedOrchestrations = 0; + int failedOrTimedOutOrchestrations = 0; + + for (int i = 0; i < totalOrchestrations; i++) { + String instanceEntityKey = entityKey + "-orch-" + (i + 1); + logger.info("Scheduling orchestration #{} for entity '{}'", i + 1, instanceEntityKey); + + String instanceId = client.scheduleNewOrchestrationInstance( + "CounterWorkflow", + new NewOrchestrationInstanceOptions().setInput(instanceEntityKey)); + logger.info("Orchestration #{} scheduled with ID: {}", i + 1, instanceId); + + OrchestrationMetadata result = client.waitForInstanceCompletion( + instanceId, Duration.ofSeconds(120), true); + + if (result != null) { + if (result.isCompleted()) { + completedOrchestrations++; + logger.info("Orchestration {} completed: {}", instanceId, result.readOutputAs(String.class)); + } else { + failedOrTimedOutOrchestrations++; + logger.error("Orchestration {} did not complete successfully: {}", instanceId, result.getRuntimeStatus()); + } + } else { + failedOrTimedOutOrchestrations++; + logger.warn("Orchestration {} did not complete within the timeout period", instanceId); + } + } + + logger.info("=== Entity Demo Complete ==="); + logger.info("Direct entity signals sent to '{}'", entityKey); + logger.info("Orchestrations completed successfully: {}/{}", completedOrchestrations, totalOrchestrations); + logger.info("Orchestrations failed or timed out: {}", failedOrTimedOutOrchestrations); + + // Shutdown the worker and exit + worker.stop(); + System.exit(0); + } +} diff --git a/samples/durable-task-sdks/java/entities/src/main/resources/logback-spring.xml b/samples/durable-task-sdks/java/entities/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..934d9b4 --- /dev/null +++ b/samples/durable-task-sdks/java/entities/src/main/resources/logback-spring.xml @@ -0,0 +1,16 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +