Skip to content

Commit 6f94109

Browse files
authored
Add Java Durable Entity samples (#244)
1 parent d39913a commit 6f94109

26 files changed

Lines changed: 1750 additions & 19 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Durable Entities — Durable Functions Java Sample
2+
3+
Java | Durable Functions
4+
5+
## Description
6+
7+
This sample demonstrates Durable Entities with Java using the Durable Task Scheduler backend. It includes:
8+
9+
1. A `Counter` durable entity with `add`, `subtract`, `get`, and `reset` operations
10+
2. An HTTP endpoint for direct entity signals
11+
3. An HTTP endpoint for direct entity reads
12+
4. An orchestration that signals the entity and reads its final value
13+
14+
## Prerequisites
15+
16+
1. [Java 11+](https://adoptium.net/) (JDK)
17+
2. [Maven](https://maven.apache.org/download.cgi)
18+
3. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
19+
4. [Docker](https://www.docker.com/products/docker-desktop/) (for the DTS emulator and Azurite)
20+
21+
## Quick Run
22+
23+
1. Start the Durable Task Scheduler emulator:
24+
```bash
25+
docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
26+
```
27+
28+
2. Start Azurite for Azure Functions host storage:
29+
```bash
30+
docker run --name azurite -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
31+
```
32+
33+
3. Build the sample:
34+
```bash
35+
cd samples/durable-functions/java/Entities
36+
mvn clean package
37+
```
38+
39+
4. Run the Functions host:
40+
```bash
41+
mvn azure-functions:run
42+
```
43+
44+
5. Signal the entity directly:
45+
```bash
46+
curl -X POST "http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=7"
47+
```
48+
49+
6. Read the entity state:
50+
```bash
51+
curl "http://localhost:7071/api/GetCounter?key=my-counter"
52+
```
53+
54+
7. Start the orchestration:
55+
```bash
56+
curl -X POST "http://localhost:7071/api/StartCounterOrchestration?key=my-orch-counter"
57+
```
58+
59+
8. View orchestration activity in the dashboard: http://localhost:8082
60+
61+
## Notes
62+
63+
- `mvn clean package` is configured to stage the Azure Functions app so `mvn azure-functions:run` works as a separate second step.
64+
- `AzureWebJobsStorage=UseDevelopmentStorage=true` requires Azurite to be running locally.
65+
- `SignalCounter` rejects `op=get`; use `GetCounter` to read entity state.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# For more info on HTTP files go to https://aka.ms/vs/httpfile
2+
3+
### ============================================
4+
### Counter Entity Operations (basic entity)
5+
### ============================================
6+
7+
### Signal the counter entity: add 10
8+
POST http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=10
9+
10+
### Signal the counter entity: add 5
11+
POST http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=5
12+
13+
### Signal the counter entity: subtract 3
14+
POST http://localhost:7071/api/SignalCounter?key=my-counter&op=subtract&value=3
15+
16+
### Signal the counter entity: reset
17+
POST http://localhost:7071/api/SignalCounter?key=my-counter&op=reset
18+
19+
### Get the current counter value
20+
GET http://localhost:7071/api/GetCounter?key=my-counter
21+
22+
### Start the counter orchestration
23+
POST http://localhost:7071/api/StartCounterOrchestration?key=my-orch-counter
24+
25+
### Check orchestration status
26+
# Copy the statusQueryGetUri from the StartCounterOrchestration response above
27+
# and paste it below (it includes the required system key):
28+
GET http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}?code={systemKey}
29+
30+
### Get the orchestration-managed counter value
31+
GET http://localhost:7071/api/GetCounter?key=my-orch-counter
32+
33+
### ============================================
34+
### Account Entity Operations (entity signaling + entity starting orchestrations)
35+
### ============================================
36+
37+
### 1. Initialize account-A with 1000
38+
POST http://localhost:7071/api/SignalAccount?key=account-A&op=deposit&value=1000
39+
40+
### 2. Initialize account-B with 200
41+
POST http://localhost:7071/api/SignalAccount?key=account-B&op=deposit&value=200
42+
43+
### 3. Check account-A balance
44+
GET http://localhost:7071/api/GetAccount?key=account-A
45+
46+
### 4. Check account-B balance
47+
GET http://localhost:7071/api/GetAccount?key=account-B
48+
49+
### ============================================
50+
### Locking Entities (TransferFunds with Critical Section)
51+
### ============================================
52+
53+
### 5. Transfer 300 from account-A to account-B (locks both entities atomically)
54+
POST http://localhost:7071/api/TransferFunds?from=account-A&to=account-B&amount=300
55+
56+
### 6. Verify account-A balance after transfer (should be 700)
57+
GET http://localhost:7071/api/GetAccount?key=account-A
58+
59+
### 7. Verify account-B balance after transfer (should be 500)
60+
GET http://localhost:7071/api/GetAccount?key=account-B
61+
62+
### ============================================
63+
### Entity Signaling Other Entities
64+
### ============================================
65+
66+
### 8. Large deposit (>= 500) triggers account entity to signal audit entity
67+
POST http://localhost:7071/api/SignalAccount?key=account-A&op=deposit&value=600
68+
69+
### ============================================
70+
### Entity Starting Orchestrations
71+
### ============================================
72+
73+
### 9. Large withdrawal (>= 500) triggers account entity to start AuditOrchestration
74+
POST http://localhost:7071/api/SignalAccount?key=account-A&op=withdraw&value=500
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"version": "2.0",
3+
"logging": {
4+
"logLevel": {
5+
"DurableTask.Core": "Warning"
6+
}
7+
},
8+
"extensions": {
9+
"durableTask": {
10+
"hubName": "default",
11+
"storageProvider": {
12+
"type": "azureManaged",
13+
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
14+
}
15+
}
16+
},
17+
"extensionBundle": {
18+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
19+
"version": "[4.*, 5.0.0)"
20+
}
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"IsEncrypted": false,
3+
"Values": {
4+
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
5+
"FUNCTIONS_WORKER_RUNTIME": "java",
6+
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
7+
}
8+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>com.example</groupId>
8+
<artifactId>durable-functions-entities</artifactId>
9+
<version>1.0-SNAPSHOT</version>
10+
<packaging>jar</packaging>
11+
12+
<properties>
13+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
14+
<java.version>11</java.version>
15+
<azure.functions.maven.plugin.version>1.41.0</azure.functions.maven.plugin.version>
16+
<azure.functions.java.library.version>3.2.4</azure.functions.java.library.version>
17+
<durabletask.azure.functions.version>1.9.0</durabletask.azure.functions.version>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>com.microsoft.azure.functions</groupId>
23+
<artifactId>azure-functions-java-library</artifactId>
24+
<version>${azure.functions.java.library.version}</version>
25+
</dependency>
26+
<dependency>
27+
<groupId>com.microsoft</groupId>
28+
<artifactId>durabletask-azure-functions</artifactId>
29+
<version>${durabletask.azure.functions.version}</version>
30+
</dependency>
31+
</dependencies>
32+
33+
<build>
34+
<plugins>
35+
<plugin>
36+
<groupId>org.apache.maven.plugins</groupId>
37+
<artifactId>maven-compiler-plugin</artifactId>
38+
<version>3.15.0</version>
39+
<configuration>
40+
<source>${java.version}</source>
41+
<target>${java.version}</target>
42+
</configuration>
43+
</plugin>
44+
<plugin>
45+
<groupId>com.microsoft.azure</groupId>
46+
<artifactId>azure-functions-maven-plugin</artifactId>
47+
<version>${azure.functions.maven.plugin.version}</version>
48+
<executions>
49+
<execution>
50+
<id>package-functions</id>
51+
<goals>
52+
<goal>package</goal>
53+
</goals>
54+
</execution>
55+
</executions>
56+
<configuration>
57+
<appName>durable-functions-entities</appName>
58+
<resourceGroup>java-functions-group</resourceGroup>
59+
<appServicePlanName>java-functions-app-service-plan</appServicePlanName>
60+
<region>westus2</region>
61+
<runtime>
62+
<os>linux</os>
63+
<javaVersion>11</javaVersion>
64+
</runtime>
65+
</configuration>
66+
</plugin>
67+
</plugins>
68+
</build>
69+
</project>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.example;
2+
3+
import com.microsoft.durabletask.AbstractTaskEntity;
4+
import com.microsoft.durabletask.EntityInstanceId;
5+
import com.microsoft.durabletask.TaskEntityOperation;
6+
7+
import java.util.logging.Logger;
8+
9+
/**
10+
* A durable entity that maintains a bank account balance.
11+
* <p>
12+
* Supports operations: deposit, withdraw, getBalance, reset.
13+
* <p>
14+
* Demonstrates:
15+
* - Entities signaling other entities: when a deposit exceeds a threshold, the account
16+
* signals an "Audit" entity to log the transaction.
17+
* - Entities starting orchestrations: when a large withdrawal occurs, the account
18+
* starts an audit orchestration.
19+
*/
20+
public class AccountEntity extends AbstractTaskEntity<Integer> {
21+
private static final Logger logger = Logger.getLogger(AccountEntity.class.getName());
22+
private static final int LARGE_TRANSACTION_THRESHOLD = 500;
23+
24+
@Override
25+
protected Integer initializeState(TaskEntityOperation operation) {
26+
return 0;
27+
}
28+
29+
@Override
30+
protected Class<Integer> getStateType() {
31+
return Integer.class;
32+
}
33+
34+
/**
35+
* Deposits funds into this account.
36+
* If the deposit amount exceeds the threshold, this entity signals an audit entity
37+
* to record the large transaction (entity-to-entity signaling).
38+
*/
39+
public void deposit(int amount) {
40+
this.state += amount;
41+
String key = this.context.getId().getKey();
42+
logger.info(String.format("Account '%s': Deposited %d, new balance: %d", key, amount, this.state));
43+
44+
// Entity signals another entity: notify the audit entity of large deposits
45+
if (amount >= LARGE_TRANSACTION_THRESHOLD) {
46+
EntityInstanceId auditEntityId = new EntityInstanceId("Audit", "ledger");
47+
String message = String.format("Large deposit of %d into account '%s'", amount, key);
48+
this.context.signalEntity(auditEntityId, "record", message);
49+
logger.info(String.format("Account '%s': Signaled audit entity for large deposit of %d", key, amount));
50+
}
51+
}
52+
53+
/**
54+
* Withdraws funds from this account.
55+
* If the withdrawal amount exceeds the threshold, this entity starts an audit
56+
* orchestration to process the large withdrawal (entity starting an orchestration).
57+
*/
58+
public void withdraw(int amount) {
59+
this.state -= amount;
60+
String key = this.context.getId().getKey();
61+
logger.info(String.format("Account '%s': Withdrew %d, new balance: %d", key, amount, this.state));
62+
63+
// Entity starts a new orchestration: trigger an audit workflow for large withdrawals
64+
if (amount >= LARGE_TRANSACTION_THRESHOLD) {
65+
String auditInput = String.format("Large withdrawal of %d from account '%s', remaining balance: %d",
66+
amount, key, this.state);
67+
String instanceId = this.context.startNewOrchestration("AuditOrchestration", auditInput);
68+
logger.info(String.format("Account '%s': Started AuditOrchestration '%s' for large withdrawal of %d",
69+
key, instanceId, amount));
70+
}
71+
}
72+
73+
public int getBalance() {
74+
logger.info(String.format("Account '%s': Current balance: %d",
75+
this.context.getId().getKey(), this.state));
76+
return this.state;
77+
}
78+
79+
public void reset() {
80+
this.state = 0;
81+
logger.info(String.format("Account '%s': Reset to 0",
82+
this.context.getId().getKey()));
83+
}
84+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.example;
2+
3+
import com.microsoft.durabletask.AbstractTaskEntity;
4+
import com.microsoft.durabletask.TaskEntityOperation;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.logging.Logger;
9+
10+
/**
11+
* A durable entity that records audit log entries.
12+
* <p>
13+
* This entity is signaled by other entities (e.g., AccountEntity) to record
14+
* events such as large transactions. It demonstrates entity-to-entity signaling
15+
* as the target of signals from other entities.
16+
*/
17+
public class AuditEntity extends AbstractTaskEntity<List<String>> {
18+
private static final Logger logger = Logger.getLogger(AuditEntity.class.getName());
19+
20+
@Override
21+
@SuppressWarnings("unchecked")
22+
protected List<String> initializeState(TaskEntityOperation operation) {
23+
return new ArrayList<>();
24+
}
25+
26+
@Override
27+
@SuppressWarnings("unchecked")
28+
protected Class<List<String>> getStateType() {
29+
return (Class<List<String>>) (Class<?>) List.class;
30+
}
31+
32+
/**
33+
* Records an audit entry. Called via entity-to-entity signaling from AccountEntity.
34+
*/
35+
public void record(String message) {
36+
this.state.add(message);
37+
logger.info(String.format("Audit '%s': Recorded entry #%d: %s",
38+
this.context.getId().getKey(), this.state.size(), message));
39+
}
40+
41+
/**
42+
* Returns the list of recorded audit entries.
43+
*/
44+
public List<String> getEntries() {
45+
logger.info(String.format("Audit '%s': Returning %d entries",
46+
this.context.getId().getKey(), this.state.size()));
47+
return this.state;
48+
}
49+
50+
/**
51+
* Clears all audit entries.
52+
*/
53+
public void clear() {
54+
this.state.clear();
55+
logger.info(String.format("Audit '%s': Cleared all entries",
56+
this.context.getId().getKey()));
57+
}
58+
}

0 commit comments

Comments
 (0)