Skip to content

Commit 777a90c

Browse files
authored
Durable Entities Support (#75)
1 parent ce0c399 commit 777a90c

57 files changed

Lines changed: 17618 additions & 7473 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,34 @@ const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext,
9393
As an aside, you'll also notice that the example orchestration above works with custom business objects. Support for custom business objects includes support for custom classes, custom data classes, and named tuples. Serialization and deserialization of these objects is handled automatically by the SDK.
9494

9595
You can find the full sample [here](./examples/human_interaction.ts).
96+
### Durable Entities (Stateful Actors)
9697

98+
Durable entities provide a way to manage small pieces of state with a simple object-oriented programming model:
99+
100+
```typescript
101+
import { TaskEntity, EntityInstanceId } from "durabletask-js";
102+
103+
// Define an entity by extending TaskEntity
104+
class CounterEntity extends TaskEntity<{ value: number }> {
105+
add(amount: number): number {
106+
this.state.value += amount;
107+
return this.state.value;
108+
}
109+
110+
protected initializeState() {
111+
return { value: 0 };
112+
}
113+
}
114+
115+
// From an orchestration, call the entity
116+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
117+
const entityId = new EntityInstanceId("Counter", "myCounter");
118+
const value: number = yield* ctx.entities.callEntity(entityId, "add", 5);
119+
return value;
120+
};
121+
```
122+
123+
You can find full entity samples [here](./examples/hello-world/entity-counter.ts) and [here](./examples/hello-world/entity-orchestration.ts).
97124
## Feature overview
98125

99126
The following features are currently supported:
@@ -122,6 +149,10 @@ Orchestrations can wait for external events using the `wait_for_external_event`
122149

123150
Orchestrations can be continued as new using the `continue_as_new` API. This API allows an orchestration to restart itself from scratch, optionally with a new input.
124151

152+
### Durable Entities
153+
154+
Durable entities are stateful objects that can be accessed from orchestrations or directly from clients. They support operations that can read/modify state, and multiple entities can be locked together for atomic cross-entity transactions. See the detailed section below for more information.
155+
125156
### Suspend, resume, and terminate
126157

127158
Orchestrations can be suspended using the `suspend_orchestration` client API and will remain suspended until resumed using the `resume_orchestration` client API. A suspended orchestration will stop processing new events, but will continue to buffer any that happen to arrive until resumed, ensuring that no data is lost. An orchestration can also be terminated using the `terminate_orchestration` client API. Terminated orchestrations will stop processing new events and will discard any buffered events.
@@ -130,6 +161,120 @@ Orchestrations can be suspended using the `suspend_orchestration` client API and
130161

131162
Orchestrations can specify retry policies for activities and sub-orchestrations. These policies control how many times and how frequently an activity or sub-orchestration will be retried in the event of a transient error.
132163

164+
### Durable Entities
165+
166+
Durable entities are stateful objects that can be accessed and manipulated from orchestrations or directly from clients. Entities provide a way to manage small pieces of state that need to be accessed and updated reliably.
167+
168+
#### Defining an Entity
169+
170+
Entities are defined by extending the `TaskEntity` class:
171+
172+
```typescript
173+
import { TaskEntity } from "durabletask-js";
174+
175+
interface CounterState {
176+
value: number;
177+
}
178+
179+
class CounterEntity extends TaskEntity<CounterState> {
180+
// Operations are just methods on the class
181+
add(amount: number): number {
182+
this.state.value += amount;
183+
return this.state.value;
184+
}
185+
186+
get(): number {
187+
return this.state.value;
188+
}
189+
190+
reset(): void {
191+
this.state.value = 0;
192+
}
193+
194+
// Required: Initialize the entity state
195+
protected initializeState(): CounterState {
196+
return { value: 0 };
197+
}
198+
}
199+
200+
// Register with the worker
201+
worker.addEntity("Counter", () => new CounterEntity());
202+
```
203+
204+
#### Accessing Entities from a Client
205+
206+
Entities can be signaled (fire-and-forget) or queried from a client:
207+
208+
```typescript
209+
import { TaskHubGrpcClient, EntityInstanceId } from "durabletask-js";
210+
211+
const client = new TaskHubGrpcClient("localhost:4001");
212+
const entityId = new EntityInstanceId("Counter", "myCounter");
213+
214+
// Signal an operation (fire-and-forget)
215+
await client.signalEntity(entityId, "add", 5);
216+
217+
// Get the entity state
218+
const response = await client.getEntity<CounterState>(entityId);
219+
console.log(`Current value: ${response.state?.value}`);
220+
```
221+
222+
#### Calling Entities from Orchestrations
223+
224+
Orchestrations can call entities and wait for results:
225+
226+
```typescript
227+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
228+
const entityId = new EntityInstanceId("Counter", "myCounter");
229+
230+
// Call entity and wait for result
231+
const currentValue: number = yield* ctx.entities.callEntity(entityId, "get");
232+
233+
// Signal entity (fire-and-forget)
234+
ctx.entities.signalEntity(entityId, "add", 10);
235+
236+
return currentValue;
237+
};
238+
```
239+
240+
#### Entity Locking (Critical Sections)
241+
242+
Multiple entities can be locked together for atomic operations:
243+
244+
```typescript
245+
const transferOrchestration: TOrchestrator = async function* (
246+
ctx: OrchestrationContext,
247+
input: { from: string; to: string; amount: number }
248+
): any {
249+
const fromEntity = new EntityInstanceId("Account", input.from);
250+
const toEntity = new EntityInstanceId("Account", input.to);
251+
252+
// Lock both entities atomically (sorted to prevent deadlocks)
253+
const lock = yield* ctx.entities.lockEntities(fromEntity, toEntity);
254+
255+
try {
256+
const fromBalance: number = yield* ctx.entities.callEntity(fromEntity, "getBalance");
257+
if (fromBalance >= input.amount) {
258+
yield* ctx.entities.callEntity(fromEntity, "withdraw", input.amount);
259+
yield* ctx.entities.callEntity(toEntity, "deposit", input.amount);
260+
}
261+
} finally {
262+
lock.release();
263+
}
264+
};
265+
```
266+
267+
#### Entity Management
268+
269+
Clean up empty or unused entities:
270+
271+
```typescript
272+
// Clean up entities that have been empty for 30 days
273+
await client.cleanEntityStorage({ removeEmptyEntities: true });
274+
```
275+
276+
You can find full entity examples [here](./examples/hello-world/entity-counter.ts) and [here](./examples/hello-world/entity-orchestration.ts).
277+
133278
## Getting Started
134279

135280
### Prerequisites

examples/README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Durable Task JavaScript SDK - Examples
2+
3+
This directory contains examples demonstrating various features of the Durable Task JavaScript SDK.
4+
5+
## Example Applications
6+
7+
Each example is a standalone application with its own README and can be run independently.
8+
9+
### Basic Orchestration Examples
10+
11+
Located in `hello-world/`:
12+
13+
- **[Activity Sequence](./hello-world/activity-sequence.ts)**: Basic orchestration that calls three activities in sequence.
14+
- **[Fan-out/Fan-in](./hello-world/fanout-fanin.ts)**: Orchestration that schedules multiple activities in parallel and aggregates results.
15+
- **[Human Interaction](./hello-world/human_interaction.ts)**: Demonstrates waiting for external events in orchestrations.
16+
17+
### Durable Entities Examples
18+
19+
Durable Entities are stateful objects with built-in concurrency control:
20+
21+
- **[Entity Counter](./entity-counter/)**: Simple counter entity demonstrating basic entity operations, signaling, and state management.
22+
- **[Entity Orchestration](./entity-orchestration/)**: Bank transfer scenario using entity locking for atomic cross-entity operations.
23+
24+
### Azure Integration Examples
25+
26+
- **[Azure Managed DTS](./azure-managed/)**: Integration with Azure Managed Durable Task Scheduler using Azure authentication.
27+
- **[Azure Managed DTS (Simple)](./azure-managed-dts.ts)**: Simplified version showing Azure DTS connection setup.
28+
29+
## Prerequisites
30+
31+
Examples require a Durable Task-compatible backend. Choose one:
32+
33+
### Option 1: DTS Emulator (Recommended for Testing)
34+
35+
The DTS Emulator is ideal for local development and testing:
36+
37+
```bash
38+
docker run --name dts-emulator -i -p 8080:8080 -d --rm mcr.microsoft.com/dts/dts-emulator:latest
39+
```
40+
41+
Most standalone examples can run against the emulator using:
42+
43+
```bash
44+
cd examples/entity-counter
45+
npm run start:emulator
46+
```
47+
48+
### Option 2: Local Sidecar
49+
50+
Install and run locally (requires Go 1.18+):
51+
52+
```bash
53+
# Install Dapr CLI (includes Durable Task sidecar)
54+
https://docs.dapr.io/getting-started/install-dapr-cli/
55+
56+
# Or build from source
57+
git clone https://github.com/microsoft/durabletask-go
58+
cd durabletask-go
59+
go run . start --backend Emulator
60+
```
61+
62+
The sidecar runs on `localhost:4001` by default.
63+
64+
### Option 3: Unofficial Sidecar Docker Image
65+
66+
For quick local development:
67+
68+
```bash
69+
docker run \
70+
--name durabletask-sidecar -d --rm \
71+
-p 4001:4001 \
72+
--env 'DURABLETASK_SIDECAR_LOGLEVEL=Debug' \
73+
kaibocai/durabletask-sidecar:latest start \
74+
--backend Emulator
75+
```
76+
77+
## Running Examples
78+
79+
### Standalone Applications (Recommended)
80+
81+
Standalone applications include `entity-counter` and `entity-orchestration`. Each has its own `package.json`:
82+
83+
```bash
84+
cd examples/entity-counter
85+
npm run start:emulator # Run against DTS emulator
86+
# OR
87+
npm run start # Run against local sidecar on localhost:4001
88+
```
89+
90+
See individual README files for detailed instructions.
91+
92+
### Single-File Examples
93+
94+
Basic orchestration examples in `hello-world/` can be run directly:
95+
96+
```bash
97+
npm run example ./examples/hello-world/activity-sequence.ts
98+
```
99+
100+
## Testing Against DTS Emulator
101+
102+
All entity examples are designed to work with the DTS emulator:
103+
104+
1. Start the DTS emulator:
105+
```bash
106+
docker run --name dts-emulator -i -p 8080:8080 -d --rm mcr.microsoft.com/dts/dts-emulator:latest
107+
```
108+
109+
2. Run the example:
110+
```bash
111+
cd examples/entity-counter
112+
npm run start:emulator
113+
```
114+
115+
The emulator provides a clean, isolated environment for testing without requiring external dependencies.
116+
117+
## Azure Managed DTS
118+
119+
For production scenarios with Azure, see the [Azure Managed DTS example](./azure-managed/) which demonstrates:
120+
- Connection string configuration
121+
- Azure authentication with DefaultAzureCredential
122+
- Environment-based configuration
123+
124+
## Documentation
125+
126+
For more information about Durable Task concepts:
127+
128+
- **Orchestrations**: Workflow definitions that coordinate activities
129+
- **Activities**: Units of work executed by orchestrations
130+
- **Entities**: Stateful actors with automatic concurrency control
131+
- **Entity Locking**: Critical sections for atomic multi-entity operations
132+
133+
See the main [README](../README.md) for comprehensive documentation.

0 commit comments

Comments
 (0)