Zum Inhalt springen
Deutsch

Actor-Pfade

Jeder Actor im System hat einen Pfad — eine hierarchische Kennung, gebaut aus seiner Position im Supervisionsbaum:

actor-ts://my-app/user/api/sessions/user-42

Von links nach rechts gelesen:

SegmentWas es ist
actor-ts://Das Schema — für jeden actor-ts-Pfad gleich.
my-appDer Systemname aus ActorSystem.create('my-app').
userDer User-Guardian — jeder Actor, den du spawnst, lebt unter /user.
apiEin Urgroßelter (z.B. ein Api-Manager).
sessionsDer direkte Parent (z.B. ein SessionManager).
user-42Der eigene Name des Leaf-Actors.

Der Pfad identifiziert einen Actor eindeutig innerhalb seines Systems, wie ein Dateisystempfad eine Datei identifiziert: zu jedem Zeitpunkt löst der Pfad entweder zu einem lebenden Actor auf oder nicht.

Du siehst Pfade an drei Stellen:

  • Log-Zeilen — das log jedes Actors ist an seinen Pfad gebunden, Log-Nachrichten tragen also die vollständige Hierarchie.
  • Terminated.actor.path — wenn ein Actor stoppt, können Watcher den Pfad inspizieren, um auf den jeweiligen Actor zu dispatchen.
  • ActorSelection — Actors per Pfad-String nachschlagen, statt eine Ref zu halten.

context.path und ref.path geben beide einen ActorPath zurück:

import { Actor } from 'actor-ts';
class Logger extends Actor<Message> {
override preStart(): void {
this.log.info(`starting; my path is ${this.context.path}`);
// → "starting; my path is actor-ts://my-app/user/logger"
this.log.info(`depth = ${this.context.path.depth()}`);
// → "depth = 2" (Root ist 0, /user ist 1, /user/logger ist 2)
this.log.info(`elements = ${this.context.path.elements().join('')}`);
// → "elements = → user → logger" (führende Lücke = die leer benannte Wurzel)
}
}

Die vollständige Schnittstelle:

MemberWas es dir gibt
path.nameDas Leaf-Segment (oben 'logger').
path.parentDer ActorPath des Parents oder null für die Wurzel.
path.systemNameDer Systemname (oben 'my-app').
path.elements()Array der Segmentnamen von der Wurzel zum Leaf.
path.depth()Abstand zur Wurzel (Wurzel = 0).
path.isAncestorOf(other)Schließt dieser Pfad other ein?
path.equals(other)Lexikalische Gleichheit (vergleicht toString()-Output).
path.toString()Kanonische URI-Form: actor-ts://<sys>/<segments>.

Pfade sind unveränderliche Wertepath.child('thing') gibt einen neuen Pfad zurück; es mutiert das Original nicht.

Du kannst beim Spawnen einen name mitgeben:

const ref = system.spawn(Foo, 'my-foo');
// ref.path.toString() === 'actor-ts://my-app/user/my-foo'
const child = this.context.spawn(Bar, 'bar');
// child.path.toString() === 'actor-ts://my-app/user/my-foo/bar'

Wenn du den Namen weglässt, synthetisiert das Framework einen — $anonymous-, einen Zähler pro Elternteil und zwölf zufällige Hex-Zeichen, zum Beispiel '$anonymous-1-3f9c1a0d7b42'. Der Zähler hält die Spawn-Reihenfolge lesbar; der Zufallsteil verhindert, dass der Pfad von außen erratbar ist — und ein Pfad ist eine Adresse. Drei Regeln gelten beim Namen:

  1. Namen müssen unter Geschwistern eindeutig sein. Zweimal context.spawn(Bar, 'bar') auf demselben Parent wirft einen Fehler — der Pfad ist bereits in Verwendung.

  2. Der Name muss ein einzelnes Pfadsegment sein. Das Framework erzwingt das und wirft bei einem Namen, der den Pfad beschädigen würde: ein Trennzeichen / oder \, die Traversal-Segmente . und .., ein leerer Name oder ein beliebiges Steuerzeichen. Ein Pfad wird durch Verbinden der Segmente mit / gerendert und durch Splitten daran wieder zerlegt — spawn(Bar, 'a/b') würde also einen Pfad erzeugen, der von einem Kind b eines Aktors a nicht zu unterscheiden ist, und damit mit einem anderen Aktor kollidieren oder ihn imitieren, auch über die Cluster-Verbindung, wo die Gegenseite den String erneut splittet. Steuerzeichen werden abgelehnt, weil Pfade in Logs und Trace-Spans geschrieben werden.

  3. Der Name darf nicht mit $ beginnen. Dieses Präfix ist für die Namen reserviert, die das Framework selbst erzeugt — $anonymous-<n>-<random> aus spawnAnonymous. Ohne die Regel könnte ein selbst gewähltes '$anonymous-1-…' mit einem Namen kollidieren, den das Framework vergeben darf, oder ihn imitieren — und wer zuerst da war, hing an der Spawn-Reihenfolge. Ein $ an anderer Stelle im Namen ist unproblematisch ('order$42').

    Die Regel betrifft Namen, die du wählst, und wird deshalb am spawn-Aufruf geprüft, nicht auf ActorPath — Pfade werden auch aus Strings rekonstruiert, die über die Cluster-Verbindung ankommen, und ein $-Verbot dort ließe jede Remote-Referenz auf einen anonymen Aktor beim Empfang scheitern.

    Alles andere ist erlaubt, inklusive Leerzeichen, Punkte innerhalb des Namens und Nicht-ASCII-Zeichen — 'Order.Placed', 'entity#3' und '日本語' sind alle gültig.

Das uid-Feld auf ActorPath unterscheidet aufeinanderfolgende Inkarnationen desselben Pfads — wenn my-foo stoppt und dann am selben Pfad neu gespawnt wird, hat die zweite eine andere uid. Die meiste Code schaut nie auf uid; das Framework verwendet es intern, um sicherzustellen, dass Nachrichten an die alte Inkarnation nicht versehentlich in der neuen landen.

import { ActorSystem } from 'actor-ts';
const system = ActorSystem.create('my-app');
const selection = system.actorSelection('/user/api/sessions/user-42');
const ref = await selection.resolveOne(5_000);
ref.tell({ kind: 'whatever' });

actorSelection(pathString) baut eine Beschreibung davon, wo zu suchen ist. resolveOne(timeoutMs) durchläuft den Actor-Baum, um einen Match zu finden; es retried alle 10 ms bis zum Deadline, nützlich, wenn der Aufrufer mit dem Spawn des Actors race-t.

Zwei Wege, einer Selection etwas zu senden:

  • selection.tell(message) — einmal auflösen, sofort liefern, wenn gefunden, in Dead Letters legen, wenn nicht. Kein Retry. Nützlich, wenn du eine klare Erwartung hast, dass der Actor existiert; ihn zu verfehlen ist ein Fehler.
  • (await selection.resolveOne(timeout)).tell(message) — warte, bis der Actor existiert (oder bis zum Timeout). Nützlich für Spawn-Race-Szenarien.

actorSelection parst drei Input-Formen:

system.actorSelection('actor-ts://my-app/user/api'); // absolute URI
system.actorSelection('/user/api'); // absoluter Pfad
system.actorSelection('user/api'); // absoluter Pfad, ohne führenden Slash

Die URI-Form ist das, was ActorPath.toString() produziert, Round-Tripping eines Pfads durch die String-Form funktioniert also. Der Systemname in URI-Form wird geprüft — actor-ts://other-app/user/api aus einem System namens my-app zu selektieren gibt null zurück (ein komplett anderes System).

Refs sind der Default-Weg, Actors zu adressieren. Halte die ActorRef, die du beim Spawn bekommen hast, gib sie weiter, speichere sie in Feldern. Der Compiler kennt den Nachrichtentyp des Actors; Selection gibt unknown-typisierte Refs zurück, mit denen du Typisierung verlierst.

Greife in drei Situationen zu einer ActorSelection:

  1. Cross-Tree-Lookup per Konvention — der gewünschte Actor lebt an einem bekannten Pfad (/user/api/sessions, /system/cluster/pubsub/mediator), und es gibt keinen sauberen Weg, die Ref durchzureichen.
  2. Spawn-Race-Auflösung — der Aufrufer weiß nicht genau, wann das Target gespawnt wird; resolveOne(timeout) wartet.
  3. Lookups von Actors, die als Strings empfangen wurden — z.B. ein HTTP-Request-Body enthält "/user/sessions/user-42", und du willst an diesen Actor weiterleiten. Parse + Select; vertraue dem String nie, ohne zu re-checken, dass der Actor existiert.

Für die alltägliche In-Actor-Verdrahtung bevorzuge Refs durch Konstruktoren / Nachrichten / Eltern-Kind-Beziehungen weiterzureichen:

// ✗ umständlich
class Worker extends Actor<...> {
override onReceive(message) {
const cache = await this.context.actorSelection('/user/cache').resolveOne();
cache.tell({ kind: 'put', ... });
}
}
// ✓ direkt
class Worker extends Actor<...> {
constructor(private readonly cache: ActorRef<CacheMessage>) { super(); }
override onReceive(message) {
this.cache.tell({ kind: 'put', ... });
}
}
const cache = system.spawn(Cache, 'cache');
const worker = system.spawnAnonymous(() => new Worker(cache));

Die Konstruktor-Injection-Variante ist typsicher, hängt nicht von Pfad-Naming-Konventionen ab und überlebt Renames trivial.

Vier Top-Level-Pfade existieren in jedem System:

PfadWas dort lebt
/userDie Top-Level-Actors deiner Anwendung — alles, was system.spawn(...) erzeugt.
/systemFramework-Internas, nach Subsystem gruppiert.
/tempDie Einweg-Antwort-Refs, die ask erzeugt — einer pro offenem Aufruf.
/deadLettersDer synthetische Empfänger für Nachrichten ohne lebendes Ziel.

/temp fällt aus der Reihe: nichts darunter ist ein Actor. ask synthetisiert einen Antwort-Ref, gibt ihm einen Pfad /temp/askResp-<id> und wirft ihn weg, sobald der Aufruf abgeschlossen ist — er taucht also in keinem Supervisor-Baum auf und actorSelection findet ihn nie. Einen Pfad hat er überhaupt nur, weil eine Antwort, die über die Cluster-Leitung geht, per Pfad adressiert wird: der Knoten des Empfängers braucht ein Ziel zum Zurücksenden. In Logs siehst du diese Refs als sender einer per ask verschickten Nachricht.

Alles, was das Framework für sich selbst erzeugt, liegt hier — eine Gruppe pro Subsystem, sodass der Baum verrät, zu welchem Teil des Frameworks ein Actor gehört:

PfadActor
/system/cluster/receptionistDer Receptionist.
/system/cluster/pubsub/mediatorDer Distributed-PubSub-Mediator.
/system/cluster/crdt/dataDie DistributedData-Replika.
/system/cluster/sharding/region-<typeName>Eine Shard-Region.
/system/cluster/sharding/coordinator-<typeName>Ein Shard-Coordinator.
/system/cluster/singleton/manager-<typeName>Ein Cluster-Singleton-Manager (der Singleton selbst ist dessen Kind).
/system/persistence/projection/<name>Eine Projection.
/system/delivery/consumer-<n>-<random>, producer-<n>-<random>Reliable-Delivery-Controller, wenn du sie nicht selbst benennst.
/system/devtools/hubDer DevTools-WebSocket-Hub, mit seinen Probes daneben.

Zwei Dinge folgen daraus, die beim Lesen eines Baums helfen:

  • Die Gruppen-Ebenen sind Actors. /system/cluster/sharding ist ein leerer Supervisor, dessen ganze Aufgabe darin besteht, die Gruppierung und die Supervisions-Strategie für alles darunter zu halten. Er hat Kinder und verarbeitet nie eine Nachricht.
  • Gruppen entstehen bei der ersten Nutzung. Ein System, das nie Clustering oder DevTools startet, hat ein leeres /system — das ist der Normalzustand, kein fehlender Ast.

Du adressierst /system-Pfade selten direkt: die Extensions (Cluster, PubSub, Sharding) bieten typisierte APIs, die dir Refs geben. Aber die Kenntnis des Layouts zahlt sich beim Lesen von Log-Output aus — der Logger jedes Actors ist an seinen Pfad gebunden — oder bei der Frage “wo ist diese Nachricht hingegangen?”.

Wenn die Cluster-Extension aktiv ist, bekommen Pfade ein Host-Port-Fragment:

actor-ts://my-app@10.0.0.5:2552/user/api/sessions/user-42
SegmentWas es ist
actor-ts://Das Schema — gleich wie bei lokalen Pfaden.
my-appDer Systemname des Clusters.
@10.0.0.5:2552Host:Port des Nodes, zur Cluster.join-Zeit zugewiesen.
/user/api/sessions/user-42Der Actor-Pfad innerhalb dieses Nodes, in der Form identisch zu einem Single-Node-Pfad.

Dieses Fragment sagt der Runtime, auf welchem Node dieser Actor lebt. Ein nackter Pfad wie actor-ts://my-app/user/foo (ohne Host) ist lokal zum auflösenden System. Der Cluster-Transport handhabt die host-geroutete Auslieferung transparent — dein tell sieht in beiden Fällen gleich aus.

Siehe Refs zwischen Nodes dafür, wie der host-aware Pfad wire-encoded wird.

  • Actors erzeugen — das Konfigurations-Bundle, das den optionalen name für den Pfad des Actors enthält.
  • Actor-System — die Guardian-Hierarchie (/user, /system, /deadLetters).
  • Refs zwischen Nodes — wie Pfade und Refs über Cluster-Nodes hinweg funktionieren.
  • Discovery — für Actors, deren Position nicht per Pfad, sondern per Service-Registry-Semantik bekannt ist.

Die ActorPath- und ActorSelection-API-Referenzen decken den vollständigen Method-Set ab.