Skip to content

Commit

Permalink
Simple implementation of Actors Runtime (#1)
Browse files Browse the repository at this point in the history
* 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
mcandeia authored Sep 23, 2024
1 parent 9048492 commit e4df133
Show file tree
Hide file tree
Showing 12 changed files with 701 additions and 35 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,7 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*


.vscode
kv
87 changes: 52 additions & 35 deletions README.md
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}`);

```
20 changes: 20 additions & 0 deletions deno.json
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"
}
47 changes: 47 additions & 0 deletions src/actors/factory.ts
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();
};
},
});
},
};
},
};
19 changes: 19 additions & 0 deletions src/actors/hono/middleware.ts
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;
};
};
7 changes: 7 additions & 0 deletions src/actors/mod.ts
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";
57 changes: 57 additions & 0 deletions src/actors/runtime.test.ts
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);
});
Loading

0 comments on commit e4df133

Please sign in to comment.