Von Microsoft Orleans
Microsoft Orleans ist ein grainbasiertes Actor-Framework für .NET. Grains sind virtuell - über ID adressiert, beim ersten Eingang automatisch gespawnt, im Leerlauf automatisch deaktiviert. Sharded Entities in actor-ts sind das nächste Analogon.
Das Grain ↔ Sharded-Entity-Mapping
Abschnitt betitelt „Das Grain ↔ Sharded-Entity-Mapping“Orleans: actor-ts: IGreeter Grain-Interface → TypeScript-Nachrichten-Typen (Command-Union) GreeterGrain : Grain → Actor, der PersistentActor erweitert (oder normaler Actor) IGrainKey → string Entity-ID client.GetGrain<IGreeter>(id)→ Referenz + tell über Sharding-RegionBefore/After
Abschnitt betitelt „Before/After“Grain-Definition
Abschnitt betitelt „Grain-Definition“// Orleans (C#):public interface IUserGrain : IGrainWithStringKey { Task<UserProfile> GetProfile(); Task UpdateName(string name);}
public class UserGrain : Grain, IUserGrain { private UserProfile _profile = new();
public Task<UserProfile> GetProfile() => Task.FromResult(_profile); public Task UpdateName(string name) { _profile = _profile with { Name = name }; return Task.CompletedTask; }}// actor-ts:import { match } from 'ts-pattern';import { Actor, type ActorRef } from 'actor-ts';
type Command = | { entityId: string; kind: 'get-profile'; replyTo: ActorRef<UserProfile> } | { entityId: string; kind: 'update-name'; name: string };
class UserActor extends Actor<Command> { private profile: UserProfile = { name: '', email: '' };
override onReceive(command: Command): void { match(command) .with({ kind: 'get-profile' }, (c) => this.onGetProfile(c)) .with({ kind: 'update-name' }, (c) => this.onUpdateName(c)) .exhaustive(); }
private onGetProfile(command: GetProfileCommand): void { command.replyTo.tell(this.profile); }
private onUpdateName(command: UpdateNameCommand): void { this.profile = { ...this.profile, name: command.name }; }}Unterschiede:
- Methodenaufrufe werden zu Nachrichten-Arten + Handlern.
Task<T>-Rückgaben werden zu explizitenreplyTo-Refs.IGrainWithStringKeyist implizit -extractEntityId(command)zieht die ID auf der Sharding-Schicht.
Grain-Client
Abschnitt betitelt „Grain-Client“// Orleans:var grain = grainFactory.GetGrain<IUserGrain>("user-42");var profile = await grain.GetProfile();await grain.UpdateName("Alice");// actor-ts:import { ClusterSharding } from 'actor-ts';
const region = cluster.sharding.start<Command>({ typeName: 'user', entityActor: UserActor, extractEntityId: (command) => command.entityId,});
// Profil via ask holen:const profile = await region.ask({ entityId: 'user-42', kind: 'get-profile', replyTo: undefined as any,}, 5_000);
// Update via tell:region.tell({ entityId: 'user-42', kind: 'update-name', name: 'Alice' });Die Region ist eine einzelne ActorRef - Nachrichten werden
per extractEntityId geroutet. Jede einzigartige ID hat einen
Actor, der beim ersten Eingang spawnt. Anders als ein
Orleans-Grain deaktiviert er sich nicht von selbst: es gibt kein
Standard-Idle-Fenster, eine Entity bleibt also resident, bis sie
jemand passiviert (siehe unten).
Aktivierung / Deaktivierung
Abschnitt betitelt „Aktivierung / Deaktivierung“// Orleans (Lifecycle-Methoden):public override Task OnActivateAsync(CancellationToken token) { // State aus Persistenz laden return base.OnActivateAsync(token);}
public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token) { // Cleanup return base.OnDeactivateAsync(reason, token);}// actor-ts:class UserActor extends Actor<Command> { override preStart(): void { // Äquivalent zu OnActivateAsync } override postStop(): void { // Äquivalent zu OnDeactivateAsync }}Die preStart- + postStop-Methoden des Frameworks mappen
direkt.
Für automatisches Passivieren bei Leerlauf:
sharding.start({ // ... passivationIdleMs: 30_000, // nach 30 s Leerlauf passivieren - wie Orleans' Default});Persistenz
Abschnitt betitelt „Persistenz“// Orleans Event Sourcing:[LogConsistencyProvider(ProviderName = "EventStore")]public class AccountGrain : JournaledGrain<AccountState, AccountEvent>, IAccount { public Task Deposit(decimal amount) { RaiseEvent(new Deposited(amount)); return ConfirmEvents(); }}// actor-ts:class Account extends PersistentActor<Command, Event, State> { readonly persistenceId = `account-${this.entityId}`;
initialState(): State { return { balance: 0 }; }
onEvent(state: State, event: Event): State { return match(event) .with({ kind: 'deposited' }, (e) => ({ balance: state.balance + e.amount })) .exhaustive(); }
onCommand(state: State, command: Command): void { match(command) .with({ kind: 'deposit' }, (c) => this.onDeposit(c)) .exhaustive(); }
private onDeposit(command: DepositCommand): void { this.persist({ kind: 'deposited', amount: command.amount }, () => {}); }}Sehr ähnliches Pattern. Orleans nutzt RaiseEvent + ConfirmEvents; actor-ts nutzt persist mit Callback.
Konzeptionelle Unterschiede
Abschnitt betitelt „Konzeptionelle Unterschiede“| Orleans | actor-ts |
|---|---|
| Virtuelle Actors - immer existierend, auf Anforderung gespawnt. | Sharded Entities sind ähnlich; müssen als sharded Typ gestartet werden. |
| Verzeichnisbasierte Platzierung - Silos schlagen Grain-Standorte nach. | Hash-Mod-Region-Platzierung per Default; Koordinator verwaltet. |
| Streams - Push-basierter Datenfluss. | DistributedPubSub für ähnliche Fan-out-Semantik. |
| Reminders - dauerhafte Timer. | context.timers + Persistenz - manuell kombinieren. |
| Transaktionen über Grains hinweg - eingebaut. | NICHT eingebaut - nutze Sagas oder Two-Phase-Commit-Patterns. |
Reminders
Abschnitt betitelt „Reminders“// Orleans:await this.RegisterOrUpdateReminder("daily-check", TimeSpan.FromDays(1), TimeSpan.FromDays(1));
public Task ReceiveReminder(string reminderName, TickStatus status) { // handle}// actor-ts - kein direktes Äquivalent, baue mit Persistenz + Timern:class MyActor extends PersistentActor<Command, Event, State> { override preStart(): void { super.preStart(); this.context.timers.startTimerWithFixedDelay( 'daily-check', { kind: 'daily-check' }, 24 * 60 * 60_000, ); }}Timer in actor-ts überleben Restart nicht - für echte
“dauerhafte Reminders” würdest du den Zeitplan persistieren und
in onRecoveryComplete neu armieren.
Streams
Abschnitt betitelt „Streams“// Orleans Streams:var stream = streamProvider.GetStream<Order>(streamId, "orders");await stream.OnNextAsync(order);await stream.SubscribeAsync((order, _) => ...);// actor-ts-Äquivalent - DistributedPubSub:const ps = system.extension(DistributedPubSubId);ps.start(cluster);
ps.mediator.tell(new Publish('orders', order));ps.mediator.tell(new Subscribe('orders', subscriberRef));Cluster-weites PubSub. Keine Grain-Beteiligung; topic-basierter Fan-out.
Cross-Grain-Transaktionen
Abschnitt betitelt „Cross-Grain-Transaktionen“// Orleans Transactions (.NET 8+):[Transaction(TransactionOption.Required)]public async Task Transfer(...) { await fromAccount.Withdraw(amount); await toAccount.Deposit(amount);}actor-ts hat keine eingebauten Transaktionen über Actors hinweg. Baue sie mit Sagas:
class TransferSaga extends PersistentFSM<...> { // Schritt 1: vom Quellkonto abheben. // Schritt 2: auf Zielkonto einzahlen. // Bei Fehlschlag: kompensieren (Quellkonto rückerstatten).}Siehe PersistentFSM für das Saga-Pattern.
Cluster-Setup
Abschnitt betitelt „Cluster-Setup“// Orleans (mit K8s):var host = Host.CreateDefaultBuilder() .UseOrleans(siloBuilder => siloBuilder .UseKubernetesHosting() .ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000) ) .Build();// actor-ts:import { KubernetesApiSeedProvider, KubernetesApiSeedProviderOptions } from 'actor-ts';
const kubernetesApiSeedProviderOptions = KubernetesApiSeedProviderOptions.create() .withNamespace(process.env.K8S_NAMESPACE!) .withServiceName('my-app') .withSystemName('my-system') .withPort(2552);const nodes = await new KubernetesApiSeedProvider( kubernetesApiSeedProviderOptions,).lookup();const seeds = nodes.map(String); // 'system@host:port'
const clusterOptions = ClusterOptions.create() .withHost(process.env.POD_IP!) .withPort(2552) .withSeeds(seeds);const cluster = await Cluster.join(system, clusterOptions);Andere API-Form, dieselbe Idee. K8s-Pod-Discovery → Cluster-Join.
Was du aufgibst
Abschnitt betitelt „Was du aufgibst“- Method-Call-Semantik - Orleans-Grain-Aufrufe sind typisierte Methodenaufrufe; actor-ts hat Nachrichten-Typen + Handler. Verboser; expliziter.
- Verteilte Transaktionen über Grains hinweg - nicht eingebaut.
- Reminders als First-Class-Primitive - kombiniere Timer + Persistenz.
- .NET-Ökosystem - Wechsel zur TS- / JS-Runtime.
Was du gewinnst
Abschnitt betitelt „Was du gewinnst“- TypeScript für End-to-End-Typisierung.
- Buns schneller Start vs. .NETs Warm-up.
- Open-Source-Ökosystem für Runtime + Persistenz-Backends.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Quickstart - actor-ts Hello-World.
- Sharding-Übersicht - das nächste Analogon zu Orleans-Grains.
- PersistentActor - für Event-Sourced Grains.
- PersistentFSM - Saga-Pattern für Cross-Grain-Workflows.
