Konfiguration
actor-ts verwendet HOCON zur Konfiguration — eine Obermenge von
JSON, die in JVM-Config-Dateien üblich ist, mit Substitutionen,
Durations und Sizes. (include-Direktiven werden
abgelehnt — setze die Quellen im
Code zusammen.) Drei Schichten, von oben nach unten aufgelöst:
- Konstruktor-Argumente an
ActorSystem.create(name, settings)— explizite Code-Overrides. - Nutzer-Config —
application.confim Projekt-Root, oder ein expliziter Pfad viaconfigFile, oder ein inlineconfig-Objekt in den Settings. - Reference-Defaults — im Framework gebündelt als
REFERENCE_CONF(siehesrc/config/reference.ts).
Alles, was nicht in Schicht 1 oder 2 spezifiziert ist, fällt auf den Reference-Default zurück.
Diese Seite erklärt, was die einzelnen Keys tun. Die vollständige Liste — jede Einstellung, die das Framework mitbringt, wörtlich aus der gebündelten Datei — steht unter Die vollständige reference.conf.
Eine vollständige application.conf
Abschnitt betitelt „Eine vollständige application.conf“actor-ts { system { name = "my-app" } logger { level = "info" }
dispatcher { default = "immediate" # immediate | microtask | throughput throughput = 16 }
cluster { gossip-interval = 1s seed-retry-interval = 3s weakly-up-after = 0s # 0 = opt-in only max-members = 1000 max-tombstones = 10000 tombstone { time-to-live = 24h prune-interval = 5m min-retention = 0s # 0 = derive from down-after } failure-detector { heartbeat-interval = 500ms unreachable-after = 2s down-after = 5s } pub-sub { gossip-interval = 1s max-subscribers-per-topic = 10000 max-topics = 10000 max-remote-nodes-per-topic = 1000 send-to-dead-letters-when-no-subscribers = on } receptionist { gossip-interval = 1s max-subscribers-per-key = 1000 max-subscriptions-total = 10000 } }
distributed-data { gossip-interval = 1s max-pending-quorum-requests = 1000 # 0 = kein Cap max-quorum-timeout = 30s # 0 = keine Obergrenze max-gossip-bytes = 1M # pro Gossip-Frame; 0 = kein Budget }
remote { tcp { host = "0.0.0.0" port = ${?ACTOR_TS_PORT} # Env-Var-Substitution; fällt auf Default zurück } max-frame-bytes = 16M }
persistence { journal { plugin = "actor-ts.persistence.journal.in-memory" } snapshot-store { plugin = "actor-ts.persistence.snapshot-store.in-memory" } }
sharding { number-of-shards = 64 rebalance-interval = 2s hand-off-timeout = 10s remember-entities = false passivation-idle = 5m # 0 deaktiviert die Auto-Passivierung # shard-passivation-idle # ungesetzt: folgt passivation-idle max-entities = 0 # 0 = kein Cap pro Node }}Platziere das im Projekt-Root. Das Framework lädt es bei
ActorSystem.create automatisch.
Die Keys, gruppiert nach Modul
Abschnitt betitelt „Die Keys, gruppiert nach Modul“actor-ts.system
Abschnitt betitelt „actor-ts.system“| Key | Default | Zweck |
|---|---|---|
system.name | "default" | Systemname — verwendet in Actor-Pfaden und zur Cluster-Identifikation. Greift, wenn ActorSystem.create() ohne Namen aufgerufen wird; ein explizites create('billing') gewinnt. |
system.shutdown-drain-timeout | 2s | Wie lange terminate() den Actors unter /user gibt, die eingereihte Arbeit zu beenden, bevor die Stop-Kaskade startet. Kehrt zurück, sobald der Baum ruhig ist — ein untätiges System zahlt einen Tick. Halte den Wert unter coordinated-shutdown.default-phase-timeout; 0 schaltet den Drain ab. |
actor-ts.logger
Abschnitt betitelt „actor-ts.logger“| Key | Default | Zweck |
|---|---|---|
logger.level | "info" | Einer von debug / info / warn / error / off. |
actor-ts.actor
Abschnitt betitelt „actor-ts.actor“| Key | Default | Zweck |
|---|---|---|
actor.throughput | 16 | User-Nachrichten, die ein Actor pro Dispatcher-Turn vor dem Yield abarbeitet. Pro Actor überschreibbar mit ActorOptions.withThroughput(). |
actor-ts.dispatcher
Abschnitt betitelt „actor-ts.dispatcher“| Key | Default | Zweck |
|---|---|---|
dispatcher.default | "immediate" | immediate (Default) / microtask / throughput. |
dispatcher.throughput | 16 | Eingereihte Actor-Turns, die ein ThroughputDispatcher pro Tick abarbeitet, über Actors hinweg — keine Nachrichten und nicht pro Actor. Siehe actor.throughput oben für den Per-Actor-Batch. |
actor-ts.dead-letters
Abschnitt betitelt „actor-ts.dead-letters“| Key | Default | Zweck |
|---|---|---|
dead-letters.store | "off" | Geordnet danach, wie viel aufbewahrt wird: off = nur publishen, nichts aufbewahren / metrics = zählen, keine Payload aufbewahren / memory = ein begrenzter Ring / persistent = zusätzlich ins Journal schreiben, sodass die Queue einen Neustart übersteht. |
dead-letters.max-entries | 1000 | Aufbewahrte Letters, bevor das älteste verdrängt wird. Von off und metrics ignoriert, die keine aufbewahren. |
dead-letters.retention | 1h | Letters nach dieser Zeit altern lassen. 0 schaltet das Altern ab und lässt max-entries als einzige Schranke. |
dead-letters.max-replays | 3 | Wie oft ein Letter erneut zugestellt werden darf, bevor es in Quarantäne kommt. |
dead-letters.persistence-id | "" | Journal-Stream, in den der persistent-Store schreibt. Leer leitet ihn aus dem Systemnamen ab, sodass zwei Systeme mit gemeinsamem Journal getrennte Queues behalten. |
Jeder Key hier ist auch im Code setzbar, feldweise, über
ActorSystemOptions.withDeadLetters(…) — was diesen Block überstimmt, wie
explizite Options es immer tun. Siehe Dead
Letters dafür, was die Queue enthält und wie
man daraus erneut zustellt.
actor-ts.cluster
Abschnitt betitelt „actor-ts.cluster“| Key | Default | Zweck |
|---|---|---|
cluster.gossip-interval | 1s | Wie oft Gossip mit einem zufälligen Peer ausgetauscht wird. |
cluster.seed-retry-interval | 3s | Wie oft Seed-Verbindungen während des Joins wiederholt werden. |
cluster.weakly-up-after | 0s | Befördert ein joining-Mitglied nach dieser Zeit automatisch zu weakly-up. 0 schaltet das ab — siehe Weakly-up. |
cluster.max-members | 1000 | Obergrenze für lebende Mitgliedseinträge, die Gossip anlegen darf. 0 schaltet die Grenze ab. |
cluster.max-tombstones | 10000 | Obergrenze für removed-Tombstones, die Gossip anlegen darf. 0 schaltet die Grenze ab. |
cluster.tombstone.time-to-live | 24h | Wie lange ein removed-Tombstone gehalten wird, bevor er gepruned wird. |
cluster.tombstone.prune-interval | 5m | Wie oft der Prune-Durchlauf läuft. |
cluster.tombstone.min-retention | 0s | Mindestalter, bevor ein Tombstone gepruned werden darf. 0 leitet es aus failure-detector.down-after ab (6×). |
cluster.failure-detector.heartbeat-interval | 500ms | Wie oft Heartbeats gesendet werden. |
cluster.failure-detector.unreachable-after | 2s | Verdachtsschwelle für unreachable-Status. |
cluster.failure-detector.down-after | 5s | Verdachtsschwelle für erzwungenes Down. Wird ab dem letzten Heartbeat gemessen, muss also größer als unreachable-after sein. |
Diese Keys liegen unter dem, was Cluster.join(system, options) übergibt —
ein explizites withGossipIntervalMs(…) gewinnt also weiterhin. Der
Failure Detector wird pro Schwelle gemergt: setzt du im Code nur
down-after, bleiben die beiden anderen aus der Datei erhalten.
HOCON-Baum und ClusterOptions-Felder sind bewusst nicht deckungsgleich:
die drei Tombstone-Stellschrauben stehen unter tombstone, weil man sie so
einstellt, während die Felder flach bleiben (withTombstoneTtlMs(…), …).
remote.tcp.host → host macht dieselbe Übersetzung schon länger.
min-retention = 0s heißt leite die Untergrenze aus dem Failure Detector
ab, nicht keine Untergrenze — also genau das, was ein ungesetztes Feld
bedeutet. Eine Datei, die den Default ausschreibt, verhält sich damit wie
eine, die ihn wegläßt.
Die beiden max-*-Grenzen begrenzen die lokale Mitgliederkarte.
remote.max-frame-bytes begrenzt einen Gossip-Frame; diese begrenzen,
was eine Folge wohlgeformter Frames ansammeln kann — eine andere Größe,
siehe Cluster-Sicherheit.
seeds, roles, der Transport und der Downing Provider haben keine
HOCON-Form — die ersten beiden sind Deployment-Identität, die an die
Join-Stelle gehört, die letzten beiden sind Objekte, die eine
Config-Datei nicht ausdrücken kann.
actor-ts.cluster.pub-sub
Abschnitt betitelt „actor-ts.cluster.pub-sub“| Key | Default | Zweck |
|---|---|---|
cluster.pub-sub.gossip-interval | 1s | Wie oft ein Mediator seine Topic-Menge an einen zufälligen Peer schickt. |
cluster.pub-sub.max-subscribers-per-topic | 10000 | Obergrenze für lokale Subscriber eines Topics. Ein Subscribe darüber wird mit SubscribeRejected beantwortet. |
cluster.pub-sub.max-topics | 10000 | Obergrenze für verschiedene Topics eines Mediators — gilt für gegossipte Topic-Ansprüche genauso wie für lokale Subscribes. |
cluster.pub-sub.max-remote-nodes-per-topic | 1000 | Obergrenze für Peers, die Subscriber für ein Topic beanspruchen dürfen. |
cluster.pub-sub.send-to-dead-letters-when-no-subscribers | on | Ein Publish, das niemanden erreicht hat, geht an system.deadLetters, statt verworfen zu werden. |
Die drei Obergrenzen begrenzen, was ein Mediator überhaupt halten kann,
und der Publish-Fan-out läuft über alle — sie sind also ebenso sehr eine
Latenz- wie eine Speichergrenze. Die Gossip-Hälfte ist die, die man
kennen sollte: ein Peer, der Topics ankündigt, für die er Subscriber
beansprucht, hat früher auf jedem Empfänger einen Eintrag pro Namen
angelegt — ganz ohne lokales Subscribe. Siehe
DistributedPubSub.
actor-ts.cluster.receptionist
Abschnitt betitelt „actor-ts.cluster.receptionist“| Key | Default | Zweck |
|---|---|---|
cluster.receptionist.gossip-interval | 1s | Wie oft der Receptionist seine lokalen Registrierungen gossipt. |
cluster.receptionist.max-subscribers-per-key | 1000 | Obergrenze für Subscriber eines Service Keys. Ein Subscribe darüber wird mit ReceptionistSubscribeRejected beantwortet. |
cluster.receptionist.max-subscriptions-total | 10000 | Obergrenze für Key/Subscriber-Paare über alle Keys dieses Nodes — ein Subscriber mit drei Keys zählt dreifach. |
Beide Blöcke liegen unter dem, was dem start(...) der Extension
übergeben wurde — ein explizites withMaxTopics(…) gewinnt also
weiterhin. Gestoppte Subscriber werden vom Death Watch freigegeben und
zählen gegen keine der Grenzen.
actor-ts.distributed-data
Abschnitt betitelt „actor-ts.distributed-data“| Key | Default | Zweck |
|---|---|---|
distributed-data.gossip-interval | 1s | Wie oft eine Replika ihren Key-Satz an einen zufälligen Peer schickt. |
distributed-data.max-pending-quorum-requests | 1000 | Obergrenze für offene Quorum-Requests — updateAsync und getAsync teilen sich ein Budget. Ein Request darüber wird direkt abgelehnt. 0 deaktiviert das Cap. |
distributed-data.max-quorum-timeout | 30s | Obergrenze für das timeoutMs eines Aufrufs; ein größerer Wert wird darauf heruntergeschnitten. 0 deaktiviert die Obergrenze. |
distributed-data.max-gossip-bytes | 1M | Byte-Budget für die Payload eines Gossip-Frames. Ein größerer Store wird pro Tick in Scheiben geschickt und setzt dort fort, wo der letzte Tick aufgehört hat. 0 entfernt das Budget. |
max-gossip-bytes wird immer auf remote.max-frame-bytes heruntergeschnitten:
Wer das Wire-Cap senkt, senkt damit auch das Gossip-Budget, und keine
Einstellung kann Gossip wieder darüber schieben. Das ist wichtig, weil die
beiden Fehlerbilder nicht vergleichbar sind: Ein Frame über dem Wire-Cap wird
schon an seinem 4-Byte-Längenpräfix abgelehnt, und der empfangende Transport
verwirft die komplette Peer-Verbindung — Heartbeats, Membership-Gossip und
jedes tell über Knotengrenzen inklusive — während ein Budget denselben Zustand
lediglich über mehr Ticks verteilt. Ein einzelner Key, dessen eigene Kodierung
das Budget überschreitet, lässt sich nicht weiter aufteilen: Er wird
übersprungen, mit einer ratenbegrenzten Warnung samt Key-Name und beiden Größen
gemeldet, in distributed_data_gossip_skipped_keys_total gezählt — und er
konvergiert nicht.
Der Block liegt auf oberster Ebene statt unter cluster.* (wo pub-sub
und receptionist wohnen), weil das Modul dort liegt: DistributedData
kommt aus src/crdt/, und der Cluster ist ein positionales Argument von
start(cluster), kein Tunable davon. durableStore hat hier keinen Key —
es ist eine DurableStateStore-Instanz, die eine Config-Datei nicht
ausdrücken kann.
Beide Caps begrenzen die Menge der offenen Requests selbst. Jeder offene
Quorum-Request hält ein Promise, einen Timer und ein Ziel-Set am Leben, bis
seine Deadline abläuft — ein ungedeckelter Replikator unter Last sammelt also
alle drei an, ohne dass irgendetwas ihn bremst. Wer über
max-pending-quorum-requests hinaus ablehnt, macht aus dem, was später als
Timeout-Sturm ankäme, sofortige Fehler, die den Knopf beim Namen nennen.
Siehe Quorum-Reads und -Writes.
max-quorum-timeout begrenzt die andere Achse. Ein offener Request hält
seinen Slot über die gesamte Deadline, ein einzelner Aufrufer mit einem
stundenlangen timeoutMs parkt also das Budget für Stunden und sperrt
spätere Aufrufer aus, die dem Cap selbst nie nahe kamen.
actor-ts.remote
Abschnitt betitelt „actor-ts.remote“| Key | Default | Zweck |
|---|---|---|
remote.tcp.host | "0.0.0.0" | Bind-Adresse. Wird verwendet, wenn ClusterOptions host nicht setzt. |
remote.tcp.port | 2552 | Bind-Port. Wird verwendet, wenn ClusterOptions port nicht setzt. |
remote.max-frame-bytes | 16M | Wire-Cap pro Frame. Ein Frame, dessen Längen-Präfix darüber liegt, wird verworfen, bevor irgendeine Payload gepuffert wird. |
remote.tls.enabled | false | Nicht umgesetzt — true bringt eine Startup-Warnung, keine Verschlüsselung. Siehe unten. |
Weil die Bind-Adresse aus der Config lesbar ist, kann ein Deployment sie komplett aus dem Code herausziehen:
actor-ts.remote.tcp { host = "0.0.0.0" port = ${?ACTOR_TS_PORT} # Env-Var-Substitution}remote.max-frame-bytes gilt für den Transport, den der Cluster sich
selbst baut. Wenn du einen TcpTransport selbst konstruierst und per
withTransport(…) übergibst, setz das Cap dort — das Framework re-capped
keinen Transport, den es nicht erzeugt hat.
actor-ts.http
Abschnitt betitelt „actor-ts.http“| Key | Default | Zweck |
|---|---|---|
http.backend | "fastify" | Einer von fastify / express / hono. |
http.shutdown-grace-period | 0ms | Wie lange unbind() in-flight Requests leerlaufen lässt, bevor Verbindungen erzwungen geschlossen werden. 0 erzwingt sofort. |
http.client | (siehe unten) | Defaults des ausgehenden HttpClient. |
http.websocket | (siehe unten) | Serverseitige WebSocket-Defaults für websocket()-Routen. |
http.backend entscheidet nur darüber, was newServerAt(...).bind()
verwendet, wenn der Builder kein eigenes Backend bekommen hat —
useBackend(new HonoBackend()) gewinnt immer und bleibt der einzige Weg,
ein selbst konfiguriertes oder selbst geschriebenes Backend zu übergeben.
Ein unbekannter Name lässt bind() mit einem ConfigError scheitern,
statt still auf Fastify zurückzufallen. Express und Hono sind optionale
Peer-Dependencies und werden nur importiert, wenn du sie benennst.
Die Grace Period ist per Default 0 — sofort erzwingen —, weil unbind()
genau das in der Praxis immer getan hat. Erhöhe sie, wenn in-flight
Requests fertig werden sollen.
Meist ist sie eine Obergrenze und keine Wartezeit: unbind() kehrt zurück,
sobald der Server tatsächlich geschlossen ist. Wo das close() eines
Backends nicht settlen kann — etwa Express mit einem lebenden WebSocket —
wird das Fenster zu einer Deadline, die immer erreicht wird; ein großer Wert
ist dort also Zeit, die du immer bezahlst. Ein Aufrufer, der einen eigenen
Wert übergibt — binding.unbind(1_000) — überschreibt die Config.
Der gemeinsame HttpClient des Systems — und jedes newClient(...), das ein
Feld offen lässt — liest seine Schranken aus actor-ts.http.client:
| Key | Default | Zweck |
|---|---|---|
http.client.maxResponseBytes | 8M | Größter Response-Body, der gepuffert wird, bevor der Request abgebrochen wird. |
http.client.defaultTimeoutMs | 30s | Deadline für einen Aufruf, der kein eigenes timeoutMs nennt. |
http.client.redirect | "follow" | Was mit einem 3xx samt Location passiert — follow / error / manual. |
http.client.maxRedirects | 5 | Hops, die eine verfolgte Kette gehen darf, bevor der Aufruf abgelehnt wird. 0 lehnt den ersten ab. |
Das ist ein Boden und keine Policy: Eine Integration übergibt ihre eigenen
Schranken an newClient(...), und ein einzelner Aufruf überschreibt sie mit
maxResponseBytes / timeoutMs / redirect / maxRedirects. Was der Block
bringt, ist die Möglichkeit, die flottenweiten Zahlen ohne Codeänderung zu
bewegen — ein Endpunkt, der doch 60 s braucht, oder ein Deployment auf einer
Strecke, auf der 8 MiB die falsche Obergrenze sind, zwang bisher jeden Aufrufer
weg vom gemeinsamen Client.
Die Werte werden beim Lesen auf den gemergten Settings validiert; ein falscher
Wert wirft also OptionsError mit dem Feldnamen, statt später mitten im
Request zu scheitern. maxResponseBytes und defaultTimeoutMs müssen hier
beide positiv sein — anders als das timeoutMs eines Requests, wo 0 der
dokumentierte Weg ist, einen einzelnen Aufruf von jeder Deadline auszunehmen.
Eine 0 in diesem Block würde jeden Aufruf ohne eigene Deadline entschärfen —
genau der Zustand, den die Defaults verhindern sollen.
Serverseitige WebSocket-Routen (websocket(path, actorRef)) lesen
ihre Defaults aus actor-ts.http.websocket:
| Key | Default | Zweck |
|---|---|---|
http.websocket.maxFrameBytes | 1M | Größter akzeptierter eingehender Frame (Bytes). |
http.websocket.onOversizeFrame | "close" | Was passiert, wenn ein Frame das Limit überschreitet — close / drop. |
http.websocket.onInvalidMessage | "close" | Was passiert, wenn eine Nachricht nicht dekodiert werden kann — close / drop / hook. |
http.websocket.maxBufferedBytes | 4M | Outbound-Backpressure-Schwelle (Bytes). |
http.websocket.onBackpressure | "drop" | Was passiert, wenn der Buffer überschritten wird — drop / close. |
http.websocket.maxConnections | (unbegrenzt) | Max. gleichzeitige Verbindungen pro Route; ein Upgrade darüber hinaus wird mit 1013 geschlossen. |
actor-ts.persistence
Abschnitt betitelt „actor-ts.persistence“| Key | Default | Zweck |
|---|---|---|
persistence.journal.plugin | "...in-memory" | Voll qualifizierter Key zur Konfig des Journals. |
persistence.snapshot-store.plugin | "...in-memory" | Voll qualifizierter Key zur Konfig des Snapshot Stores. |
Die Plugin-Keys zeigen auf einen anderen Config-Abschnitt, der
die Settings dieses Plugins enthält. Z. B. enthält
actor-ts.persistence.journal.sqlite die path, pragmas etc.
des SQLite-Journals. Siehe jede Journal-Seite in
Persistence für die per-Plugin-Keys.
actor-ts.cache
Abschnitt betitelt „actor-ts.cache“| Key | Default | Zweck |
|---|---|---|
cache.in-memory.maxEntries | 10000 | Max. Einträge vor LRU-Eviction (In-Memory-Cache). |
cache.in-memory.cleanupMs | 60000 | Intervall (ms) des Hintergrund-Sweeps für abgelaufene Einträge. |
cache.<name>.in-memory.* | (der Block oben) | Dieselben zwei Blätter, für die einzelne Instanz, die cache('<name>') auflöst — überschreibt den globalen Block Blatt für Blatt. |
cache.<name>.plugin | "...in-memory" | Welches Backend cache('<name>') auflöst. |
Redis- und Memcached-Caches lesen ihre Verbindungs-Settings aus
actor-ts.cache.redis / actor-ts.cache.memcached.
Die beiden <name>-Pfade nehmen den Namen, den dein Code an
cache(name) übergibt, und lassen sich deshalb nicht in
reference.conf aufzählen. Einen Consumer allein für seinen eigenen
Key-Raum zu dimensionieren ist genau das, was die Empfehlung ein Cache
pro Consumer braucht.
actor-ts.sharding
Abschnitt betitelt „actor-ts.sharding“| Key | Default | Zweck |
|---|---|---|
sharding.number-of-shards | 64 | In wie viele Shards der Entity-Raum geteilt wird. Jeder Node, der einen Typ startet oder proxyt, muss denselben Wert haben — der Koordinator verweigert eine Region, die abweicht, siehe numShards ist eine clusterweite Vereinbarung. |
sharding.rebalance-interval | 2s | Abstand zwischen koordinator-getriebenen Rebalance-Passes. |
sharding.hand-off-timeout | 10s | Wie lange auf HandOffComplete gewartet wird, bevor zwangsumverteilt wird. |
sharding.remember-entities | false | Persistiere die Menge aktiver Entity-IDs. |
sharding.passivation-idle | 5m | Auto-Passiviere eine Entität nach diesem Idle-Fenster. 0 = deaktiviert. Eine Entität, die Zustand im Speicher hält und ihn nicht in preStart wiederaufbaut, verliert ihn dabei — siehe Passivierung. |
sharding.shard-passivation-idle | (ungesetzt) | Auto-Passiviere einen Shard, nachdem er so lange leer stand. Ungesetzt folgt er passivation-idle; 0 hält leere Shards resident. Bewusst nicht in der reference.conf, denn genau von einem ausgelieferten Wert müsste sich „ungesetzt” unterscheiden lassen. |
sharding.max-entities | 0 | Entity-Obergrenze pro Node; bei Überlauf wird die am längsten ungenutzte Entität passiviert. 0 = kein Cap. |
Diese Keys gelten für jeden auf dem Node gestarteten Sharded Type. Sie
werden einmal pro sharding.start(...) gelesen und unter das gelegt, was
dieser Aufruf explizit übergibt — die übliche Reihenfolge gilt also pro Feld:
// actor-ts.sharding.passivation-idle = 2 minutes
const shardingOptions = StartShardingOptions.create<CartCommand>() .withTypeName('cart') .withEntityActor(CartEntity) .withExtractEntityId((command) => command.entityId) .withNumShards(256);
cluster.sharding.start(shardingOptions);// numShards 256 (explizit), passivationIdleMs 120_000 (Config-Datei)Ein Setting für genau einen Typ gehört deshalb in den Builder; die Config-Datei ist für die node-weite Grundeinstellung, die du ohne Rebuild zwischen Staging und Produktion verschieben willst. Die polymorphen Optionen — der Entity-Actor, die Extraktoren, die Allocation Strategy, ein Lease, die Stores — haben keine HOCON-Form und bleiben im Code.
actor-ts.coordinated-shutdown
Abschnitt betitelt „actor-ts.coordinated-shutdown“| Key | Default | Zweck |
|---|---|---|
coordinated-shutdown.default-phase-timeout | 5s | Default-Timeout pro Phase. Eine Phase, die es überschreitet, wird abgebrochen, damit die nächste noch läuft. |
coordinated-shutdown.terminate-actor-system | true | Ob der eingebaute Terminator-Task in der finalen Phase system.terminate() aufruft. |
coordinated-shutdown.exit-process | false | Ob nach Abschluss der Pipeline process.exit(0) aufgerufen wird. |
terminate-actor-system = false entfernt nur den eingebauten Terminator —
in ActorSystemTerminate registrierte eigene Tasks laufen weiterhin.
Nutze es, wenn ein Host-Prozess die Lebensdauer des Systems besitzt und ein
Signal-Handler es nicht killen darf.
exit-process ist für den umgekehrten Fall: ein hängengebliebenes Handle
(ein offener Socket, eine gepoolte Verbindung, die ein Treiber nie
freigegeben hat) hält den Prozess am Leben, nachdem die Pipeline fertig
ist — von außen nicht von einem Hänger zu unterscheiden. Per Default aus,
weil ein erzwungener Exit genau die Leaks verdeckt, die du eigentlich
finden willst.
actor-ts.worker-cluster (Multi-Runtime-Worker)
Abschnitt betitelt „actor-ts.worker-cluster (Multi-Runtime-Worker)“| Key | Default | Zweck |
|---|---|---|
worker-cluster.workers | "auto" | Anzahl Worker — "auto" verwendet navigator.hardwareConcurrency. |
worker-cluster.restart-policy | "on-failure" | always / on-failure / never. |
Diese Keys füllen WorkerCluster.spawn(options) für Felder auf, die der
Aufruf nicht setzt. spawn ist ein Static ohne ActorSystem im Scope und
lädt die Config-Kette daher selbst — dieselbe, die ActorSystem.create
verwendet, inklusive ACTOR_TS_CONFIG und ./application.conf. Eine
unbekannte restart-policy wird mit einem OptionsError abgelehnt, statt
still „never” zu bedeuten.
Broker-Plugins — actor-ts.io.broker.*
Abschnitt betitelt „Broker-Plugins — actor-ts.io.broker.*“Jeder Broker-Actor liest aus seinem eigenen Subtree. Siehe die per-Protokoll-Seiten für die Keys:
| Subtree | Seite |
|---|---|
io.broker.kafka | Kafka |
io.broker.mqtt | MQTT |
io.broker.amqp | AMQP |
io.broker.nats | NATS |
io.broker.jetstream | NATS JetStream |
io.broker.jetstream-key-value | JetStream KV + Object Store |
io.broker.jetstream-object-store | JetStream KV + Object Store |
io.broker.redis-streams | Redis Streams |
io.broker.grpc.{client,server} | gRPC |
io.broker.websocket | WebSocket Client |
io.broker.sse | SSE |
io.broker.tcp | TCP |
io.broker.udp | UDP |
io.broker.email-bridge | E-Mail-Bridge |
HOCON-spezifische Syntax
Abschnitt betitelt „HOCON-spezifische Syntax“Durations
Abschnitt betitelt „Durations“gossip-interval = 1s # 1000 msunreachable-after = 2.5s # 2500 msdown-after = 5000ms # explizite msgc-cadence = 10m # Minutenttl = 24h # StundenErkannte Einheiten: ns, us, ms, s, m, h, d.
max-frame-bytes = 16M # 16 777 216 Bytesbuffer = 64Kheap = 2GErkannte Einheiten: B, K, M, G, T — standardmäßig
binär (1024).
Environment-Substitution
Abschnitt betitelt „Environment-Substitution“port = ${?ACTOR_TS_PORT} # nutze env, falls gesetzt, sonst fallbacklog-level = ${?LOG_LEVEL} # gleichapi-key = ${API_KEY} # required — Fehler, wenn env nicht gesetztfallback-port = ${?ENV_PORT}fallback-port = ${fallback-port:-2552} # default-if-empty SyntaxVerwende ${?ENV} für optional, ${ENV} für required.
Includes werden nicht unterstützt
Abschnitt betitelt „Includes werden nicht unterstützt“include "shared-cluster.conf" # Fehler: `include` wird nicht unterstütztEine Config-Quelle kann keine weitere Datei und keine URL benennen, die
hinzugeladen wird. Das ist eine Entscheidung, keine Lücke: ein
aufgelöstes include überließe demjenigen, der die Config schreibt, die
Wahl, welche Pfade der Prozess liest — relativ zu einer Wurzel, die der
Parser gar nicht kennen kann. Das Zusammenführen im Code belässt diese
Wahl beim Aufrufer.
import { Config } from 'actor-ts';
const shared = Config.parseFile('shared-cluster.conf');const application = Config.parseFile('application.conf');const config = shared.merge(application);merge legt sein Argument obenauf, die lokale Datei gewinnt also — genau
die Reihenfolge, auf die das include oben hinauswollte. Den geteilten
Inhalt in eine Datei zu kopieren funktioniert ebenfalls.
Der Parser sagt all das, wenn er auf ein include trifft, und zitiert
dabei Zeile und Ziel — eine aus Akka oder Pekko portierte Config
scheitert damit mit einer Antwort statt mit einem Rätsel.
Reservierte Schlüssel
Abschnitt betitelt „Reservierte Schlüssel“Drei Schlüssel werden in einer Config-Quelle überall abgelehnt — als Schlüssel, als jedes Segment eines Pfadausdrucks und innerhalb einer Substitution:
__proto__.anything = 1 # Fehler: abgelehntconstructor.x = 1 # Fehler: abgelehntprototype = 1 # Fehler: abgelehntvalue = ${__proto__} # Fehler: abgelehntEin Schlüsselpfad wird auf ein einfaches Objekt expandiert, und eine
Zuweisung an __proto__ schreibt in den Objekt-Prototypen statt einen
normalen Schlüssel anzulegen — würde man das zulassen, könnte eine
Config-Datei das Verhalten jedes Objekts im Prozess verändern. Der
Parser lehnt sie stattdessen mit der gewohnten Zeilen- und
Spaltenangabe ab.
Die Prüfung vergleicht exakt, ähnlich aussehende Schlüssel sind also
unproblematisch: _proto_, constructorName und prototypes
funktionieren normal.
Lesend sind nur eigene Schlüssel sichtbar
Abschnitt betitelt „Lesend sind nur eigene Schlüssel sichtbar“Drei abgelehnte Namen sichern die Schreibseite. Die Leseseite ist
andersherum abgesichert — positiv — denn Object.prototype hat ein Dutzend
weitere Member, und eine Liste käme dabei nie hinterher:
name = ${toString} # Fehler: unaufgelöste Substitution — keine native Funktionport = ${?valueOf} # optional: fällt auf die Umgebung durch, dann wegJeder Zugriff — eine Substitution, hasPath und jeder typisierte Getter —
liest ausschließlich eigene Schlüssel, im Config-Baum wie in der
Umgebung. Ein Name, der zufällig einem geerbten Member entspricht, geht
damit ins Leere, statt zu dem aufzulösen, was auf dem Prototypen liegt, und
hasPath('toString') ist false, solange die Config keinen eigenen
toString-Schlüssel deklariert. Das bleibt erlaubt und funktioniert
weiterhin: ein eigener Schlüssel überdeckt den geerbten Member,
toString = "mine" liest sich also als "mine" zurück.
Config im Code lesen
Abschnitt betitelt „Config im Code lesen“const actorSystemOptions = ActorSystemOptions.create().withConfig({ 'actor-ts.cluster.gossip-interval': '500ms', });const system = ActorSystem.create( 'my-app', actorSystemOptions,);
// Innerhalb eines Actors / einer Extension:const config = system.config;const interval = config.getDuration('actor-ts.cluster.gossip-interval');// → 500 (in ms)Das Config-Interface hat typisierte Getter: getString, getNumber,
getBoolean, getDuration, getBytes, getStringList, etc.
Fehlende Keys werfen, es sei denn, Du prüfst zuerst mit hasPath.
Ladereihenfolge
Abschnitt betitelt „Ladereihenfolge“1. REFERENCE_CONF ← gebündelte Defaults2. application.conf (CWD) ← Projekt-Root, auto-geladen3. Dateipfad aus `configFile`-Setting ← expliziter Override4. ENV-Var ACTOR_TS_CONFIG ← Pfad oder inline HOCON5. Konstruktor `config: { ... }` ← Code-Level-Override6. Konstruktor-Feld-Overrides ← explizite Felder schlagen Config (logLevel, dispatcher, scheduler, …)Jede Schicht überlagert die vorherige. Schicht 6 (Konstruktor-Felder) gewinnt immer für die Felder, die sie abdeckt; alles andere fließt durch den HOCON-Merge.
Wo der eingebaute Default steht
Abschnitt betitelt „Wo der eingebaute Default steht“Schicht 1 ist reference.conf — aber nicht jede Einstellung hat dort
einen Key, und selbst die, die einen haben, brauchen einen Wert im Code
für den Fall, dass nichts konfiguriert ist. Dieser Wert hat einen festen
Ort, damit immer auffindbar ist, worauf eine Einstellung zurückfällt:
- Options-Defaults stehen neben dem Feld, das sie belegen, in der
passenden
XOptions.ts.DEFAULT_MAX_ENTRIESliegt inInMemoryCacheOptions.ts,DEFAULT_NUM_SHARDSinShardingOptions.ts. - Alles andere — Obergrenzen, Schranken und Timeouts ohne Option
dahinter — steht in einer
Constants.ts:src/<subsystem>/Constants.tsfür ein einzelnes Subsystem,src/util/Constants.tsfür Werte, die mehrere teilen.
Ein reference.conf-Blatt und sein Code-Fallback sind damit immer genau
zwei Dateien, und die zweite ist nach der ersten benannt.
Validierung
Abschnitt betitelt „Validierung“Optionswerte werden einmal validiert, zur Consume-Zeit, auf den
gemergten Settings — nachdem die drei obigen Ebenen aufgelöst sind.
Weil die Prüfung auf dem gemergten Ergebnis läuft, gelten dieselben
Regeln, egal wie ein Wert ankam: über den fluenten Builder, ein einfaches
Settings-Objekt oder einen HOCON-Key. Cross-Field-Regeln (z. B.
„downThreshold muss unreachableThreshold übersteigen”) sehen die
finalen Werte.
Ein ungültiger Wert wirft einen OptionsError (aus dem
Paket-Root exportiert), der das betroffene Feld und die Erwartung nennt:
MqttOptions: protocolVersion must be one of 4, 5 (got 6)- Bei Broker-Aktoren (MQTT, Kafka, WebSocket-Client, …) wird der
Fehler beim Start des Aktors (
preStart) geworfen, nach der Pflichtfeld-Prüfung — ein fehlendes Pflichtfeld erscheint also weiterhin alsBrokerOptionsError, und nur vorhandene, aber ungültige Werte werden zuOptionsError. - Bei Nicht-Broker-Konsumenten (Caches, Leases, Sharding, …) wird der Fehler aus dem Konstruktor geworfen.
OptionsError ist verschieden von ConfigError (fehlerhaftes HOCON —
falscher Typ, fehlender Pfad) und BrokerOptionsError (ein
Broker-Pflichtfeld fehlt in jeder Ebene). Ungesetzte optionale Felder
lösen ihn nie aus; sie fallen auf ihre Defaults zurück.
import { CircuitBreaker, CircuitBreakerOptions, OptionsError } from 'actor-ts';
// Nicht-Broker-Consumer validieren in ihrem Konstruktor, daher wird der// Fehler synchron geworfen und ein einfaches try/catch funktioniert:try { const options = CircuitBreakerOptions.create() .withMaxFailures(0); // ungültig — muss eine positive Ganzzahl sein new CircuitBreaker(options); // validiert im Konstruktor} catch (e) { if (e instanceof OptionsError) { console.error(`bad option ${e.field}: ${e.message}`); }}// Bei einem Broker-Actor wird der OptionsError erst in preStart geworfen// und als ActorInitializationError verpackt — er taucht also über die// Supervision auf (nicht im try/catch des Spawn-Aufrufs).Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Actor System — wie Settings das Framework erreichen.
- Cluster Overview — die Keys, die für Multi-Node-Setups am relevantesten sind.
- Persistence Overview — Journal- + Snapshot-Store-Plugin-Auswahl.
- Versionspolitik — was stabil vs. experimentell ist.
