Zum Inhalt springen
Deutsch

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:

  1. Konstruktor-Argumente an ActorSystem.create(name, settings) — explizite Code-Overrides.
  2. Nutzer-Config — application.conf im Projekt-Root, oder ein expliziter Pfad via configFile, oder ein inline config-Objekt in den Settings.
  3. Reference-Defaults — im Framework gebündelt als REFERENCE_CONF (siehe src/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.

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.

KeyDefaultZweck
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-timeout2sWie 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.
KeyDefaultZweck
logger.level"info"Einer von debug / info / warn / error / off.
KeyDefaultZweck
actor.throughput16User-Nachrichten, die ein Actor pro Dispatcher-Turn vor dem Yield abarbeitet. Pro Actor überschreibbar mit ActorOptions.withThroughput().
KeyDefaultZweck
dispatcher.default"immediate"immediate (Default) / microtask / throughput.
dispatcher.throughput16Eingereihte 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.
KeyDefaultZweck
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-entries1000Aufbewahrte Letters, bevor das älteste verdrängt wird. Von off und metrics ignoriert, die keine aufbewahren.
dead-letters.retention1hLetters nach dieser Zeit altern lassen. 0 schaltet das Altern ab und lässt max-entries als einzige Schranke.
dead-letters.max-replays3Wie 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.

KeyDefaultZweck
cluster.gossip-interval1sWie oft Gossip mit einem zufälligen Peer ausgetauscht wird.
cluster.seed-retry-interval3sWie oft Seed-Verbindungen während des Joins wiederholt werden.
cluster.weakly-up-after0sBefördert ein joining-Mitglied nach dieser Zeit automatisch zu weakly-up. 0 schaltet das ab — siehe Weakly-up.
cluster.max-members1000Obergrenze für lebende Mitgliedseinträge, die Gossip anlegen darf. 0 schaltet die Grenze ab.
cluster.max-tombstones10000Obergrenze für removed-Tombstones, die Gossip anlegen darf. 0 schaltet die Grenze ab.
cluster.tombstone.time-to-live24hWie lange ein removed-Tombstone gehalten wird, bevor er gepruned wird.
cluster.tombstone.prune-interval5mWie oft der Prune-Durchlauf läuft.
cluster.tombstone.min-retention0sMindestalter, bevor ein Tombstone gepruned werden darf. 0 leitet es aus failure-detector.down-after ab (6×).
cluster.failure-detector.heartbeat-interval500msWie oft Heartbeats gesendet werden.
cluster.failure-detector.unreachable-after2sVerdachtsschwelle für unreachable-Status.
cluster.failure-detector.down-after5sVerdachtsschwelle 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.

KeyDefaultZweck
cluster.pub-sub.gossip-interval1sWie oft ein Mediator seine Topic-Menge an einen zufälligen Peer schickt.
cluster.pub-sub.max-subscribers-per-topic10000Obergrenze für lokale Subscriber eines Topics. Ein Subscribe darüber wird mit SubscribeRejected beantwortet.
cluster.pub-sub.max-topics10000Obergrenze 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-topic1000Obergrenze für Peers, die Subscriber für ein Topic beanspruchen dürfen.
cluster.pub-sub.send-to-dead-letters-when-no-subscribersonEin 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.

KeyDefaultZweck
cluster.receptionist.gossip-interval1sWie oft der Receptionist seine lokalen Registrierungen gossipt.
cluster.receptionist.max-subscribers-per-key1000Obergrenze für Subscriber eines Service Keys. Ein Subscribe darüber wird mit ReceptionistSubscribeRejected beantwortet.
cluster.receptionist.max-subscriptions-total10000Obergrenze 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.

KeyDefaultZweck
distributed-data.gossip-interval1sWie oft eine Replika ihren Key-Satz an einen zufälligen Peer schickt.
distributed-data.max-pending-quorum-requests1000Obergrenze 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-timeout30sObergrenze für das timeoutMs eines Aufrufs; ein größerer Wert wird darauf heruntergeschnitten. 0 deaktiviert die Obergrenze.
distributed-data.max-gossip-bytes1MByte-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.

KeyDefaultZweck
remote.tcp.host"0.0.0.0"Bind-Adresse. Wird verwendet, wenn ClusterOptions host nicht setzt.
remote.tcp.port2552Bind-Port. Wird verwendet, wenn ClusterOptions port nicht setzt.
remote.max-frame-bytes16MWire-Cap pro Frame. Ein Frame, dessen Längen-Präfix darüber liegt, wird verworfen, bevor irgendeine Payload gepuffert wird.
remote.tls.enabledfalseNicht 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.

KeyDefaultZweck
http.backend"fastify"Einer von fastify / express / hono.
http.shutdown-grace-period0msWie 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:

KeyDefaultZweck
http.client.maxResponseBytes8MGrößter Response-Body, der gepuffert wird, bevor der Request abgebrochen wird.
http.client.defaultTimeoutMs30sDeadline 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.maxRedirects5Hops, 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:

KeyDefaultZweck
http.websocket.maxFrameBytes1MGröß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.maxBufferedBytes4MOutbound-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.
KeyDefaultZweck
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.

KeyDefaultZweck
cache.in-memory.maxEntries10000Max. Einträge vor LRU-Eviction (In-Memory-Cache).
cache.in-memory.cleanupMs60000Intervall (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.

KeyDefaultZweck
sharding.number-of-shards64In 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-interval2sAbstand zwischen koordinator-getriebenen Rebalance-Passes.
sharding.hand-off-timeout10sWie lange auf HandOffComplete gewartet wird, bevor zwangsumverteilt wird.
sharding.remember-entitiesfalsePersistiere die Menge aktiver Entity-IDs.
sharding.passivation-idle5mAuto-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-entities0Entity-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.

KeyDefaultZweck
coordinated-shutdown.default-phase-timeout5sDefault-Timeout pro Phase. Eine Phase, die es überschreitet, wird abgebrochen, damit die nächste noch läuft.
coordinated-shutdown.terminate-actor-systemtrueOb der eingebaute Terminator-Task in der finalen Phase system.terminate() aufruft.
coordinated-shutdown.exit-processfalseOb 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.

KeyDefaultZweck
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.

Jeder Broker-Actor liest aus seinem eigenen Subtree. Siehe die per-Protokoll-Seiten für die Keys:

SubtreeSeite
io.broker.kafkaKafka
io.broker.mqttMQTT
io.broker.amqpAMQP
io.broker.natsNATS
io.broker.jetstreamNATS JetStream
io.broker.jetstream-key-valueJetStream KV + Object Store
io.broker.jetstream-object-storeJetStream KV + Object Store
io.broker.redis-streamsRedis Streams
io.broker.grpc.{client,server}gRPC
io.broker.websocketWebSocket Client
io.broker.sseSSE
io.broker.tcpTCP
io.broker.udpUDP
io.broker.email-bridgeE-Mail-Bridge
gossip-interval = 1s # 1000 ms
unreachable-after = 2.5s # 2500 ms
down-after = 5000ms # explizite ms
gc-cadence = 10m # Minuten
ttl = 24h # Stunden

Erkannte Einheiten: ns, us, ms, s, m, h, d.

max-frame-bytes = 16M # 16 777 216 Bytes
buffer = 64K
heap = 2G

Erkannte Einheiten: B, K, M, G, T — standardmäßig binär (1024).

port = ${?ACTOR_TS_PORT} # nutze env, falls gesetzt, sonst fallback
log-level = ${?LOG_LEVEL} # gleich
api-key = ${API_KEY} # required — Fehler, wenn env nicht gesetzt
fallback-port = ${?ENV_PORT}
fallback-port = ${fallback-port:-2552} # default-if-empty Syntax

Verwende ${?ENV} für optional, ${ENV} für required.

include "shared-cluster.conf" # Fehler: `include` wird nicht unterstützt

Eine 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.

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: abgelehnt
constructor.x = 1 # Fehler: abgelehnt
prototype = 1 # Fehler: abgelehnt
value = ${__proto__} # Fehler: abgelehnt

Ein 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.

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 Funktion
port = ${?valueOf} # optional: fällt auf die Umgebung durch, dann weg

Jeder 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.

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.

1. REFERENCE_CONF ← gebündelte Defaults
2. application.conf (CWD) ← Projekt-Root, auto-geladen
3. Dateipfad aus `configFile`-Setting ← expliziter Override
4. ENV-Var ACTOR_TS_CONFIG ← Pfad oder inline HOCON
5. Konstruktor `config: { ... }` ← Code-Level-Override
6. 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.

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_ENTRIES liegt in InMemoryCacheOptions.ts, DEFAULT_NUM_SHARDS in ShardingOptions.ts.
  • Alles andere — Obergrenzen, Schranken und Timeouts ohne Option dahinter — steht in einer Constants.ts: src/<subsystem>/Constants.ts für ein einzelnes Subsystem, src/util/Constants.ts fü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.

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 als BrokerOptionsError, und nur vorhandene, aber ungültige Werte werden zu OptionsError.
  • 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).