-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simple implementation of Actors Runtime (#1)
* First actors version Signed-off-by: Marcos Candeia <[email protected]> * Use wait group and promise instead of mutex Signed-off-by: Marcos Candeia <[email protected]> * Use denoKv for actors runtime Signed-off-by: Marcos Candeia <[email protected]> * Adds hono middleware Signed-off-by: Marcos Candeia <[email protected]> * Adds hono middleware Signed-off-by: Marcos Candeia <[email protected]> * improve readme Signed-off-by: Marcos Candeia <[email protected]> * Add jsr badge Signed-off-by: Marcos Candeia <[email protected]> --------- Signed-off-by: Marcos Candeia <[email protected]>
- Loading branch information
Showing
12 changed files
with
701 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -128,3 +128,7 @@ dist | |
.yarn/build-state.yml | ||
.yarn/install-state.gz | ||
.pnp.* | ||
|
||
|
||
.vscode | ||
kv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,55 +1,72 @@ | ||
<a href="https://jsr.io/@deco/actors" target="_blank"><img alt="jsr" src="https://jsr.io/badges/@deco/actors" /></a> | ||
|
||
# Actors | ||
|
||
High-scale interactive services often demand a combination of high throughput, low latency, and high availability. These are challenging goals to meet with traditional stateless architectures. Inspired by the Orleans virtual-actor pattern, the **Actors** library offers a stateful solution, enabling developers to manage distributed state in a seamless and scalable way. | ||
High-scale interactive services often demand a combination of high throughput, | ||
low latency, and high availability. These are challenging goals to meet with | ||
traditional stateless architectures. Inspired by the Orleans virtual-actor | ||
pattern, the **Actors** library offers a stateful solution, enabling developers | ||
to manage distributed state in a seamless and scalable way. | ||
|
||
The **Actors** model simplifies the development of stateful applications by | ||
abstracting away the complexity of distributed system concerns, such as | ||
reliability and resource management. This allows developers to focus on building | ||
logic while the framework handles the intricacies of state distribution and | ||
fault tolerance. | ||
|
||
The **Actors** model simplifies the development of stateful applications by abstracting away the complexity of distributed system concerns, such as reliability and resource management. This allows developers to focus on building logic while the framework handles the intricacies of state distribution and fault tolerance. | ||
With **Actors**, developers create "actors" – isolated, stateful objects that | ||
can be invoked directly. Each actor is uniquely addressable, enabling efficient | ||
and straightforward interaction across distributed environments. | ||
|
||
With **Actors**, developers create "actors" – isolated, stateful objects that can be invoked directly. Each actor is uniquely addressable, enabling efficient and straightforward interaction across distributed environments. | ||
## Key Features | ||
|
||
## Key Features: | ||
- **Simplified State Management:** Build stateful services using a straightforward programming model, without worrying about distributed systems complexities like locks or consistency. | ||
- **No Distributed Locks:** Actors handle state independently, eliminating the need for distributed locks. Each actor is responsible for its own state, making it simple to work with highly concurrent scenarios without race conditions. | ||
- **Virtual Actors:** Actors are automatically instantiated, managed, and scaled by the framework, freeing you from managing lifecycles manually. | ||
- **Powered by Deno Cluster Isolates:** Achieve high-performance applications that scale effortlessly by leveraging Deno cluster's unique isolate addressing. | ||
- **Simplified State Management:** Build stateful services using a | ||
straightforward programming model, without worrying about distributed systems | ||
complexities like locks or consistency. | ||
- **No Distributed Locks:** Actors handle state independently, eliminating the | ||
need for distributed locks. Each actor is responsible for its own state, | ||
making it simple to work with highly concurrent scenarios without race | ||
conditions. | ||
- **Virtual Actors:** Actors are automatically instantiated, managed, and scaled | ||
by the framework, freeing you from managing lifecycles manually. | ||
- **Powered by Deno Cluster Isolates:** Achieve high-performance applications | ||
that scale effortlessly by leveraging Deno cluster's unique isolate | ||
addressing. | ||
|
||
## Example: Simple Atomic Counter without Distributed Locks | ||
|
||
```typescript | ||
import { Actor, ActorState, actors } from "@deco/actors"; | ||
import { actors, ActorState } from "@deco/actors"; | ||
|
||
interface ICounter { | ||
increment(): Promise<number>; | ||
getCount(): Promise<number>; | ||
} | ||
class Counter { | ||
private count: number; | ||
|
||
export default class Counter extends Actor implements ICounter { | ||
private count: number; | ||
|
||
constructor(state: ActorState) { | ||
super(state); | ||
this.count = 0; | ||
state.blockConcurrencyWhile(async () => { | ||
this.count = await this.getCount(); | ||
}); | ||
} | ||
|
||
async increment(): Promise<number> { | ||
let val = await this.state.storage.get("counter"); | ||
await this.state.storage.put("counter", ++val); | ||
return val; | ||
} | ||
|
||
async getCount(): Promise<number> { | ||
return await this.state.storage.get("counter"); | ||
} | ||
constructor(protected state: ActorState) { | ||
this.count = 0; | ||
state.blockConcurrencyWhile(async () => { | ||
this.count = await this.state.storage.get<number>("counter") ?? 0; | ||
}); | ||
} | ||
|
||
async increment(): Promise<number> { | ||
await this.state.storage.put("counter", ++this.count); | ||
return this.count; | ||
} | ||
|
||
getCount(): number { | ||
return this.count; | ||
} | ||
} | ||
|
||
// Invoking the counter actor | ||
const counter = actors.proxy<ICounter>({ id: "counter-1" }); | ||
const counterProxy = actors.proxy({ | ||
actor: Counter, | ||
server: "http://localhost:8000", | ||
}); | ||
const counter = counterProxy.id("counter-id"); | ||
// Increment counter | ||
await counter.increment(); | ||
// Get current count | ||
const currentCount = await counter.getCount(); | ||
console.log(`Current count: ${currentCount}`); | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"name": "@deco/actors", | ||
"exports": { | ||
".": "./src/actors/mod.ts", | ||
"./hono": "./src/actors/hono/middleware.ts" | ||
}, | ||
"imports": { | ||
"@core/asyncutil": "jsr:@core/asyncutil@^1.1.1", | ||
"@hono/hono": "jsr:@hono/hono@^4.6.2", | ||
"@std/assert": "jsr:@std/assert@^1.0.5", | ||
"@std/async": "jsr:@std/async@^1.0.5", | ||
"@std/path": "jsr:@std/path@^1.0.6" | ||
}, | ||
"tasks": { | ||
"check": "deno fmt && deno lint --fix && deno check ./src/actors/mod.ts ./src/actors/hono/middleware.ts", | ||
"test": "rm kv;deno test -A --unstable-kv ." | ||
}, | ||
"lock": false, | ||
"version": "0.0.0" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { | ||
type Actor, | ||
ACTOR_ID_HEADER_NAME, | ||
type ActorConstructor, | ||
} from "./runtime.ts"; | ||
|
||
export interface ProxyOptions<TInstance extends Actor> { | ||
actor: ActorConstructor<TInstance> | string; | ||
server: string; | ||
} | ||
|
||
type Promisify<Actor> = { | ||
[key in keyof Actor]: Actor[key] extends (...args: infer Args) => infer Return | ||
? Return extends Promise<unknown> ? Actor[key] | ||
: (...args: Args) => Promise<Return> | ||
: Actor[key]; | ||
}; | ||
export const actors = { | ||
proxy: <TInstance extends Actor>(c: ProxyOptions<TInstance>) => { | ||
return { | ||
id: (id: string): Promisify<TInstance> => { | ||
return new Proxy<Promisify<TInstance>>({} as Promisify<TInstance>, { | ||
get: (_, prop) => { | ||
return async (...args: unknown[]) => { | ||
const resp = await fetch( | ||
`${c.server}/actors/${ | ||
typeof c.actor === "string" ? c.actor : c.actor.name | ||
}/invoke/${String(prop)}`, | ||
{ | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
[ACTOR_ID_HEADER_NAME]: id, | ||
}, | ||
body: JSON.stringify({ | ||
args, | ||
}), | ||
}, | ||
); | ||
return resp.json(); | ||
}; | ||
}, | ||
}); | ||
}, | ||
}; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { MiddlewareHandler } from "@hono/hono"; | ||
import type { ActorRuntime } from "../mod.ts"; | ||
|
||
/** | ||
* Adds middleware to the Hono server that routes requests to actors. | ||
* the default base path is `/actors`. | ||
*/ | ||
export const useActors = ( | ||
rt: ActorRuntime, | ||
basePath = "/actors", | ||
): MiddlewareHandler => { | ||
return async (ctx, next) => { | ||
if (!ctx.req.path.startsWith(basePath)) { | ||
return next(); | ||
} | ||
const response = await rt.fetch(ctx.req.raw); | ||
ctx.res = response; | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// deno-lint-ignore no-empty-interface | ||
export interface Actor { | ||
} | ||
|
||
export { ActorRuntime } from "./runtime.ts"; | ||
export { ActorState } from "./state.ts"; | ||
export { type ActorStorage } from "./storage.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { assertEquals } from "@std/assert"; | ||
import { actors } from "./factory.ts"; | ||
import { ActorRuntime } from "./runtime.ts"; | ||
import type { ActorState } from "./state.ts"; | ||
|
||
class Counter { | ||
private count: number; | ||
|
||
constructor(protected state: ActorState) { | ||
this.count = 0; | ||
state.blockConcurrencyWhile(async () => { | ||
this.count = await this.state.storage.get<number>("counter") ?? 0; | ||
}); | ||
} | ||
|
||
async increment(): Promise<number> { | ||
await this.state.storage.put("counter", ++this.count); | ||
return this.count; | ||
} | ||
|
||
getCount(): number { | ||
return this.count; | ||
} | ||
} | ||
|
||
const runServer = (rt: ActorRuntime): AsyncDisposable => { | ||
const server = Deno.serve(rt.fetch.bind(rt)); | ||
return { | ||
async [Symbol.asyncDispose]() { | ||
await server.shutdown(); | ||
}, | ||
}; | ||
}; | ||
|
||
Deno.test("counter increment and getCount", async () => { | ||
const rt = new ActorRuntime([Counter]); | ||
await using _server = runServer(rt); | ||
const actorId = "1234"; | ||
const counterProxy = actors.proxy({ | ||
actor: Counter, | ||
server: "http://localhost:8000", | ||
}); | ||
|
||
const actor = counterProxy.id(actorId); | ||
// Test increment | ||
const number = await actor.increment(); | ||
assertEquals(number, 1); | ||
|
||
// Test getCount | ||
assertEquals(await actor.getCount(), 1); | ||
|
||
// Test increment again | ||
assertEquals(await actor.increment(), 2); | ||
|
||
// Test getCount again | ||
assertEquals(await actor.getCount(), 2); | ||
}); |
Oops, something went wrong.