Skip to content
Open
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.
74 changes: 74 additions & 0 deletions samples/durable-functions/java/Entities/demo.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# For more info on HTTP files go to https://aka.ms/vs/httpfile

### ============================================
### Counter Entity Operations (basic entity)
### ============================================

### 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

### ============================================
### Account Entity Operations (entity signaling + entity starting orchestrations)
### ============================================

### 1. Initialize account-A with 1000
POST http://localhost:7071/api/SignalAccount?key=account-A&op=deposit&value=1000

### 2. Initialize account-B with 200
POST http://localhost:7071/api/SignalAccount?key=account-B&op=deposit&value=200

### 3. Check account-A balance
GET http://localhost:7071/api/GetAccount?key=account-A

### 4. Check account-B balance
GET http://localhost:7071/api/GetAccount?key=account-B

### ============================================
### Locking Entities (TransferFunds with Critical Section)
### ============================================

### 5. Transfer 300 from account-A to account-B (locks both entities atomically)
POST http://localhost:7071/api/TransferFunds?from=account-A&to=account-B&amount=300

### 6. Verify account-A balance after transfer (should be 700)
GET http://localhost:7071/api/GetAccount?key=account-A

### 7. Verify account-B balance after transfer (should be 500)
GET http://localhost:7071/api/GetAccount?key=account-B

### ============================================
### Entity Signaling Other Entities
### ============================================

### 8. Large deposit (>= 500) triggers account entity to signal audit entity
POST http://localhost:7071/api/SignalAccount?key=account-A&op=deposit&value=600

### ============================================
### Entity Starting Orchestrations
### ============================================

### 9. Large withdrawal (>= 500) triggers account entity to start AuditOrchestration
POST http://localhost:7071/api/SignalAccount?key=account-A&op=withdraw&value=500
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,84 @@
package com.example;

import com.microsoft.durabletask.AbstractTaskEntity;
import com.microsoft.durabletask.EntityInstanceId;
import com.microsoft.durabletask.TaskEntityOperation;

import java.util.logging.Logger;

/**
* A durable entity that maintains a bank account balance.
* <p>
* Supports operations: deposit, withdraw, getBalance, reset.
* <p>
* Demonstrates:
* - Entities signaling other entities: when a deposit exceeds a threshold, the account
* signals an "Audit" entity to log the transaction.
* - Entities starting orchestrations: when a large withdrawal occurs, the account
* starts an audit orchestration.
*/
public class AccountEntity extends AbstractTaskEntity<Integer> {
private static final Logger logger = Logger.getLogger(AccountEntity.class.getName());
private static final int LARGE_TRANSACTION_THRESHOLD = 500;

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

@Override
protected Class<Integer> getStateType() {
return Integer.class;
}

/**
* Deposits funds into this account.
* If the deposit amount exceeds the threshold, this entity signals an audit entity
* to record the large transaction (entity-to-entity signaling).
*/
public void deposit(int amount) {
this.state += amount;
String key = this.context.getId().getKey();
logger.info(String.format("Account '%s': Deposited %d, new balance: %d", key, amount, this.state));

// Entity signals another entity: notify the audit entity of large deposits
if (amount >= LARGE_TRANSACTION_THRESHOLD) {
EntityInstanceId auditEntityId = new EntityInstanceId("Audit", "ledger");
String message = String.format("Large deposit of %d into account '%s'", amount, key);
this.context.signalEntity(auditEntityId, "record", message);
logger.info(String.format("Account '%s': Signaled audit entity for large deposit of %d", key, amount));
}
}

/**
* Withdraws funds from this account.
* If the withdrawal amount exceeds the threshold, this entity starts an audit
* orchestration to process the large withdrawal (entity starting an orchestration).
*/
public void withdraw(int amount) {
this.state -= amount;
String key = this.context.getId().getKey();
logger.info(String.format("Account '%s': Withdrew %d, new balance: %d", key, amount, this.state));

// Entity starts a new orchestration: trigger an audit workflow for large withdrawals
if (amount >= LARGE_TRANSACTION_THRESHOLD) {
String auditInput = String.format("Large withdrawal of %d from account '%s', remaining balance: %d",
amount, key, this.state);
String instanceId = this.context.startNewOrchestration("AuditOrchestration", auditInput);
logger.info(String.format("Account '%s': Started AuditOrchestration '%s' for large withdrawal of %d",
key, instanceId, amount));
}
}

public int getBalance() {
logger.info(String.format("Account '%s': Current balance: %d",
this.context.getId().getKey(), this.state));
return this.state;
}

public void reset() {
this.state = 0;
logger.info(String.format("Account '%s': Reset to 0",
this.context.getId().getKey()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.example;

import com.microsoft.durabletask.AbstractTaskEntity;
import com.microsoft.durabletask.TaskEntityOperation;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
* A durable entity that records audit log entries.
* <p>
* This entity is signaled by other entities (e.g., AccountEntity) to record
* events such as large transactions. It demonstrates entity-to-entity signaling
* as the target of signals from other entities.
*/
public class AuditEntity extends AbstractTaskEntity<List<String>> {
private static final Logger logger = Logger.getLogger(AuditEntity.class.getName());

@Override
@SuppressWarnings("unchecked")
protected List<String> initializeState(TaskEntityOperation operation) {
return new ArrayList<>();
}

@Override
@SuppressWarnings("unchecked")
protected Class<List<String>> getStateType() {
return (Class<List<String>>) (Class<?>) List.class;
}

/**
* Records an audit entry. Called via entity-to-entity signaling from AccountEntity.
*/
public void record(String message) {
this.state.add(message);
logger.info(String.format("Audit '%s': Recorded entry #%d: %s",
this.context.getId().getKey(), this.state.size(), message));
}

/**
* Returns the list of recorded audit entries.
*/
public List<String> getEntries() {
logger.info(String.format("Audit '%s': Returning %d entries",
this.context.getId().getKey(), this.state.size()));
return this.state;
}

/**
* Clears all audit entries.
*/
public void clear() {
this.state.clear();
logger.info(String.format("Audit '%s': Cleared all entries",
this.context.getId().getKey()));
}
}
Loading