Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions samples/durable-functions/java/Entities/README.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions samples/durable-functions/java/Entities/demo.http
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions samples/durable-functions/java/Entities/host.json
Original file line number Diff line number Diff line change
@@ -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)"
}
}
8 changes: 8 additions & 0 deletions samples/durable-functions/java/Entities/local.settings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
69 changes: 69 additions & 0 deletions samples/durable-functions/java/Entities/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>durable-functions-entities</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<azure.functions.maven.plugin.version>1.41.0</azure.functions.maven.plugin.version>
<azure.functions.java.library.version>3.2.4</azure.functions.java.library.version>
<durabletask.azure.functions.version>1.9.0</durabletask.azure.functions.version>
</properties>

<dependencies>
<dependency>
<groupId>com.microsoft.azure.functions</groupId>
<artifactId>azure-functions-java-library</artifactId>
<version>${azure.functions.java.library.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft</groupId>
<artifactId>durabletask-azure-functions</artifactId>
<version>${durabletask.azure.functions.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<version>${azure.functions.maven.plugin.version}</version>
<executions>
<execution>
<id>package-functions</id>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
<configuration>
<appName>durable-functions-entities</appName>
<resourceGroup>java-functions-group</resourceGroup>
<appServicePlanName>java-functions-app-service-plan</appServicePlanName>
<region>westus2</region>
<runtime>
<os>linux</os>
<javaVersion>11</javaVersion>
</runtime>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Supports operations: add, subtract, get, reset.
* Public methods are automatically dispatched based on operation name.
*/
public class CounterEntity extends AbstractTaskEntity<Integer> {
private static final Logger logger = Logger.getLogger(CounterEntity.class.getName());

@Override
protected Integer initializeState(TaskEntityOperation operation) {
return 0;
}

@Override
protected Class<Integer> 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()));
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<Void> 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<Void> 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=<entity-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<Void> 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();
}
}
Loading