Die vollständige reference.conf
Das ist die eingebaute Konfiguration des Frameworks, exakt so, wie sie in
src/config/reference.ts ausgeliefert wird. Es ist die vollständige
Menge der First-Party-Settings: was hier nicht steht, hat keine HOCON-Form,
und was hier steht, wird von irgendetwas gelesen (sonst schlägt ein Test
fehl — siehe Keine toten Keys unten).
Jeder gezeigte Wert ist der bereits geltende Default, du musst also nie das
Ganze kopieren. Schreib nur das in deine application.conf, was du ändern
willst:
# application.conf — alles andere behält die Defaults untenactor-ts { system.name = "billing" sharding.passivation-idle = 2 minutes}Was die einzelnen Keys tun, steht in Konfiguration — diese Seite ist die vollständige Liste, jene die Erklärung.
Die Datei
Abschnitt betitelt „Die Datei“actor-ts { system { name = "default"
# How long terminate() lets the actors under /user finish the work they # already have before the stop cascade starts. The wait ends as soon as # the tree goes quiet, so an idle system pays a tick rather than the # budget; what is still queued when the budget runs out is dead-lettered # exactly as it was before. Keep it under # coordinated-shutdown.default-phase-timeout — the last phase awaits # terminate(), so a drain as long as the phase leaves no room to stop # anything. 0 disables draining and restores the pre-#663 behaviour. # # A throttled or suspended mailbox is never waited on: neither drains at # a rate a shutdown can wait for. shutdown-drain-timeout = 2s }
logger { # System log level. Gates BEFORE the per-sink min-levels below, so a # sink asking for "debug" while this says "info" receives nothing — # lower this first, then narrow per sink. level = "info" # debug | info | warn | error | off
# Grace period terminate() gives the active logger to flush and close # its sinks before whenTerminated() resolves. Bounds the whole logger, # so it applies to a custom one too — any logger with a close(). close-timeout = 3s
# Multi-sink pipeline. Every sink ships disabled; enabling at least one # replaces the default single ConsoleLogger with a MultiSinkLogger over # the enabled set. An explicit logger (or logSinks) passed to # ActorSystem.create replaces this whole block rather than merging with # it -- a sink belongs to one construction world or the other. # # The Sentry sink has no block here: it needs your own @sentry/node # import, which a config file cannot hold, so it is wired in code. # See docs -> Observe -> Logging -> Platform integrations. sinks { console { enabled = false min-level = "info" # debug | info | warn | error | off format = "text" # text = human-readable, json = one NDJSON object per record stream = "auto" # auto | stdout | stderr; auto = console.* for text, stdout for json }
file { enabled = false min-level = "info" format = "text" directory = "logs" # created if missing prefix = "log" # file name is <prefix>-<yyyy-MM-dd>-<HH-mm-ss>.<extension> extension = "txt" # Rolling over always opens a NEW stamped file — the active file is # never renamed, which Windows forbids while it is open anyway. max-file-bytes = 64M # roll when the file would pass this; 0 = never rotate-interval = "daily" # off | hourly | daily — roll on the clock boundary # Retention only ever deletes files matching this sink's own prefix, # extension and timestamp shape — never anything else in the directory. max-files = 14 # keep this many rotated files; 0 = keep all max-age = 14d # delete rotated files older than this; 0 = keep all compress-rotated = false # gzip each rotated file to <name>.gz
delivery { max-batch-size = 500 flush-interval = 1s queue-capacity = 10000 overflow = "drop-new" # drop-new | drop-head } }
# Graylog. Native rather than via OTLP because Graylog's # OpenTelemetry input speaks gRPC only, which the otlp sink does not. gelf { enabled = false min-level = "info" protocol = "udp" # udp | tcp | http host = "127.0.0.1" # udp/tcp port = 12201 # udp/tcp url = "" # http only, e.g. "http://graylog:12201/gelf" # The GELF "host" field. Empty = the OS hostname, else the system name. host-name = "" compression = "gzip" # udp only: none | gzip (server auto-detects) max-chunk-bytes = 1420 # udp only: datagram size before chunking request-timeout = 10s # http only # TLS for tcp is code-only: those fields carry the key material # itself, not a path to it. delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# OpenTelemetry logs over HTTP with a JSON body. One endpoint format # reaches Loki 3+, Parseable, SigNoz, Datadog, Axiom, Honeycomb, New # Relic and every OTel Collector — start here before a native sink. otlp { enabled = false min-level = "info" url = "http://localhost:4318/v1/logs" # service.name on the OTLP resource; defaults to the system name. service-name = "" scope-name = "actor-ts" gzip = false request-timeout = 10s # Request headers (API keys, tenant ids) are code-only: a config # file is the wrong home for a credential. delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Grafana Loki's native push API. Loki 3+ also accepts OTLP at # /otlp/v1/logs, so the otlp sink reaches it too; this one exists for # direct push and explicit control over the label set. loki { enabled = false min-level = "info" url = "" # base URL, e.g. "http://loki:3100" tenant-id = "" # X-Scope-OrgID, for multi-tenant Loki format = "text" # text | json — how each log line is rendered # Labels are Loki's INDEX. Keep them static and few: every distinct # combination is a separate stream, and a per-record value here # (an actor path, a request id) multiplies streams without bound. # Everything variable rides as structured metadata instead. labels { service = "" # empty = the actor system's name } structured-metadata = true request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Parseable's REST ingestion. Its OTLP endpoint works too — this # sink sends a flatter record and skips OTLP semantics. parseable { enabled = false min-level = "info" url = "" # base URL, e.g. "https://parseable.internal" stream = "" # target dataset; created on first use # Basic auth OR an API key, never both. Prefer a substitution # (${?PARSEABLE_API_KEY}) over writing a secret in here. username = "" password = "" api-key = "" request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Seq, over CLEF — newline-delimited JSON with @-prefixed reserved # keys. Close enough to the framework's own NDJSON that there is # almost nothing to translate. seq { enabled = false min-level = "info" url = "" # base URL, e.g. "http://seq:5341" api-key = "" # X-Seq-ApiKey; prefer ${?SEQ_API_KEY} request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Splunk's HTTP Event Collector. splunk { enabled = false min-level = "info" url = "" # HEC base URL, e.g. "https://splunk:8088" token = "" # HEC token; prefer ${?SPLUNK_HEC_TOKEN} index = "" # empty = the token's default index source = "actor-ts" sourcetype = "_json" host-name = "" # empty = the actor system's name request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# RFC 5424 syslog — the integration that needs no vendor: rsyslog, # syslog-ng, journald's forwarder, Papertrail and a long tail of # appliances all speak it. syslog { enabled = false min-level = "info" transport = "udp" # udp | tcp | tls host = "127.0.0.1" port = 514 facility = 16 # 0-23; 16 = local0, the range for applications app-name = "" # empty = the actor system's name host-name = "" # empty = the OS hostname # tcp/tls framing. octet-counting (RFC 6587) is the only one that # survives a message containing a newline — a stack trace always does. framing = "octet-counting" # octet-counting | lf # TLS material is code-only: those fields carry the key itself. delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } } } }
actor { # User messages ONE actor handles per dispatcher turn before it yields. # Distinct from dispatcher.throughput below, which bounds how many queued # units — each belonging to a different actor — a ThroughputDispatcher # drains per tick. This is the knob that amortises the scheduling round # trip, because a cell may only ever have one unit queued at a time. # 1 restores the pre-#409 message-at-a-time interleaving; raising it trades # fairness for throughput, since nothing else on the event loop runs until # the actor yields. Override per actor with ActorOptions.withThroughput(). throughput = 16 }
dispatcher { # hybrid | immediate | microtask | throughput # # hybrid wakes actors on the microtask queue and spends every 64th unit on # a macrotask so timers and I/O still run; immediate is the previous # default, one setImmediate per turn — fair, and ~2.4 us a hop that a # request/response actor cannot amortise. microtask is the unbounded form # and starves the event loop under a sustained volley; it is kept for # measurement, not for production. default = "hybrid" # Queued units a ThroughputDispatcher drains per tick, ACROSS actors — # see actor.throughput above for the per-actor batch. throughput = 16 }
# Bounded record of the messages the system could not deliver, inspectable # and replayable through system.deadLetterQueue. Undeliverable messages are # published on the event stream either way -- this decides whether anything # KEEPS them, which by default nothing does. dead-letters { # One axis, ordered by how much of the letter is kept: # off = publish only, keep nothing (the behaviour before #433) # metrics = count them and keep no payload, for the alert without the # evidence locker (retaining a message is a data-protection # decision; observing a rate is not) # memory = keep in a bounded ring, lost with the process # persistent = additionally write to the configured journal, so the queue # is still there after a restart store = "off"
# Letters held before the oldest is evicted. The queue is a diagnostic # ring: an unbounded one turns a delivery outage into an out-of-memory. # Ignored by the off and metrics stores, which hold no letters. max-entries = 1000
# Age letters out after this long. 0 disables ageing and leaves # max-entries as the only bound. retention = 1h
# How often one letter may be replayed before it is quarantined. A # replayed message that dead-letters again comes back as the SAME entry # with a higher count, so a poison message cannot be retried forever. max-replays = 3
# Journal stream the persistent store writes to. Empty = derive it from # the system name, so two systems sharing a journal keep separate queues. persistence-id = "" }
cluster { gossip-interval = 1s seed-retry-interval = 3s weakly-up-after = 0s # 0 disables auto weakly-up promotion
# Caps on the local member map. max-frame-bytes bounds ONE gossip frame; # these bound what a sequence of well-formed frames can accumulate, since # gossip is what introduces addresses in the first place. 0 disables # either. max-tombstones is the load-bearing one: a tombstone carries no # liveness, so nothing but the TTL below ever reclaims it. max-members = 1000 max-tombstones = 10000
tombstone { time-to-live = 24h prune-interval = 5m min-retention = 0s # 0 = derive from failure-detector down-after }
failure-detector { heartbeat-interval = 500ms unreachable-after = 2s down-after = 5s # measured from the last heartbeat, so > unreachable-after }
# Stable-observation bootstrap: poll discovery until the contact-point set # has been unchanged for stable-margin, then let the lowest-addressed node # -- and only it -- form a cluster if no peer promoted it within # self-election-grace. Opt-in: bootstrapCluster reads this block only when # its stableObservation option is set. required-contact-points is the one # knob worth changing: 1 keeps single-node development working, but only a # value matching the expected replica count catches discovery that is # stably wrong rather than merely slow. bootstrap { stable-margin = 5s poll-interval = 1s max-wait = 60s required-contact-points = 1 self-election-grace = 10s
# Fewest up members -- self included -- before bootstrapCluster's # awaitReady (and Cluster.awaitReady / isReady) counts the cluster as # ready. 1 keeps single-node development working; a deployment states # its replica count here, for the same reason as # required-contact-points. minimum-members = 1
# await-ready ships no value on purpose -- a key that is always present # could not express "unset", and unset is what selects the computed # default: self-election-grace + 5s behind stable observation, 5s # otherwise (#1086). Set a duration to pin the budget regardless: # # await-ready = 30s }
# Cluster-wide publish/subscribe (DistributedPubSub). The caps bound what # one mediator can be made to hold -- by local subscribers and by a peer's # gossiped topic claims alike. A Subscribe over a cap is answered with # SubscribeRejected, never silently dropped. pub-sub { gossip-interval = 1s max-subscribers-per-topic = 10000 max-topics = 10000 max-remote-nodes-per-topic = 1000 # A publish that reached no subscriber goes to system.deadLetters, so a # mistyped topic is observable instead of silent. off = discard it. send-to-dead-letters-when-no-subscribers = on }
# Cluster-wide service registry (Receptionist). Subscribers are watched, # so a stopped one is dropped; the caps bound the ones that are still alive. # The total counts key/subscriber pairs, not distinct subscribers — one # subscriber on three keys spends three of it. receptionist { gossip-interval = 1s max-subscribers-per-key = 1000 max-subscriptions-total = 10000 } }
# Cluster-wide replicated CRDT store (DistributedData). Top-level rather # than under cluster.* because the module is -- the cluster is a positional # argument to start(), not a tunable. Both caps bound quorum requests # (updateAsync + getAsync); 0 disables either. What they buy is a bound on # the unsettled set itself: every entry holds a promise, a timer and a # target set until its deadline passes, so refusing past the cap turns what # would be a timeout storm into immediate, attributable rejections. # # max-gossip-bytes bounds one outbound gossip frame instead; a larger store # is pushed a slice per tick. It is clamped down to remote.max-frame-bytes, # because a frame past that cap is rejected on its length prefix and costs # the whole peer association -- heartbeats included. 0 removes the budget # (the clamp still applies). distributed-data { gossip-interval = 1s max-pending-quorum-requests = 1000 max-quorum-timeout = 30s max-gossip-bytes = 1M }
remote { # Bind address of this node. Cluster.join reads these when its options # leave host/port unset, so a deployment can move the address into config. # # host is the interface to BIND, and the wildcard below is the right # default for it. It is not an identity: what peers dial is # advertised-host, which falls back to host only when host is not a # wildcard, then to CLUSTER_HOST / POD_IP / HOSTNAME, then to 127.0.0.1. # advertised-host ships no value on purpose -- a key that is always # present could not express "unset", and unset is what makes that # fallback chain reachable. Set it wherever the bound interface and the # dialable address differ, which in Kubernetes is every pod: # # advertised-host = ${?POD_IP} # # Leaving it at the wildcard in a multi-node deployment is the failure it # exists to prevent: every node advertises the identical # system@0.0.0.0:2552, each reads the others' announcements as claims # about itself, and every member map ends up holding one entry. tcp { host = "0.0.0.0" port = 2552 } tls { # Read but NOT honoured: the transport the cluster builds for itself is # always plaintext. Setting this to true only buys a startup WARN that # says so — encrypting the wire is issue #941. enabled = false } max-frame-bytes = 16M # per-frame wire cap; lower it on semi-trusted networks }
http { backend = "fastify" # fastify | express | hono # In-flight drain window for unbind() before connections are forced. # 0 keeps the historical behaviour (force immediately); raise it if you # want in-flight requests to finish on shutdown. shutdown-grace-period = 0ms
# Outbound HttpClient defaults — the system's shared client and any # newClient(...) that leaves a field unset. Leaf names match the # HttpClientOptions fields (camelCase) and are validated on read (a bad # value throws OptionsError). A request may still override each of them # per call; these are the floor the fleet inherits without a code change. client { maxResponseBytes = 8M # buffered response body ceiling defaultTimeoutMs = 30s # deadline for a call that names none redirect = "follow" # follow | error | manual maxRedirects = 5 # hops a followed chain may take }
# Server-side defaults for websocket() routes (per-connection policy). # Leaf names match the WebsocketRouteOptions fields (camelCase); a route # may override any of them, and the resolved values are validated # (OptionsError on a bad value). websocket { maxFrameBytes = 1M # inbound frame size cap onOversizeFrame = "close" # close | drop onInvalidMessage = "close" # close | drop | hook maxBufferedBytes = 4M # outbound buffer cap before backpressure onBackpressure = "drop" # drop | close # maxConnections is unlimited by default; set a positive integer to cap. # Inbound frames held while the connection actor starts; past either cap # the socket is closed with 1013 instead of buffered without bound. maxPreAttachFrames = 256 maxPreAttachBytes = 4M # How long an admitted upgrade waits for its connection actor before the # socket is closed and its maxConnections slot released. Infinity to # disable (code only — HOCON has no Infinity literal). acceptTimeoutMs = 10s } }
cache { # Defaults for the built-in in-memory cache (the "default" cache, and any # cache whose plugin resolves to actor-ts.cache.in-memory). Leaf names # match the InMemoryCacheOptions fields (camelCase) and are validated on # read — a bad value throws OptionsError. in-memory { maxEntries = 10000 # LRU cap on entries (Infinity/unbounded only settable in code) cleanupMs = 60000 # background expired-entry sweep interval, ms (0 disables the sweep) } # Per-instance overrides live under the cache's own name and win over the # block above, so one consumer can be sized for its own key space: # # actor-ts.cache.idempotency.in-memory.maxEntries = 200000 # # The name is the application's, so these paths cannot be listed here. # actor-ts.cache.<name>.plugin selects the backend the same way. # # prefixQuotas splits ONE instance between the consumers writing into it, # so a flood of keys under one prefix evicts only that prefix's entries # (#607). Each quota is a cap and a reservation; they must sum to at most # that instance's maxEntries. Off by default — an undivided map is the # behaviour every release before it had. Quote the prefixes, they contain # a colon: # # actor-ts.cache.shared.in-memory { # maxEntries = 10000 # prefixQuotas { "rsp:" = 7000, "idem:" = 2000, "rl:" = 1000 } # } # # A per-name table replaces the global one rather than merging with it. }
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 # idle window before an entity passivates; 0 disables the sweep # shard-passivation-idle -- how long a shard may stand empty before it # stops as well. Deliberately left unset rather than given a value: # unset, it follows passivation-idle, which is what "the shard goes # when its entities do" needs. Set it (0ms disables) to decouple them. max-entities = 0 # 0 = no per-node cap }
worker-cluster { workers = "auto" # "auto" uses navigator.hardwareConcurrency restart-policy = "on-failure" # always | on-failure | never }
coordinated-shutdown { default-phase-timeout = 5s terminate-actor-system = true exit-process = false # call process.exit(0) once the pipeline completes
# Framework components register their own teardown in the pipeline: the # HTTP server unbinds in service-unbind, broker actors close their # connections in service-stop, a joined cluster leaves in cluster-leave, # DevTools detaches with the rest of the service layer. Set false to keep # the phases and register everything yourself -- for an embedder that owns # the lifecycle of the resources it handed the system. auto-register-tasks = true }}Was hier bewusst fehlt
Abschnitt betitelt „Was hier bewusst fehlt“Zwei Familien von Settings haben oben keinen Eintrag, beide mit Absicht.
Plugin-Subtrees. Journals, Snapshot Stores, Caches und Broker lesen
ihre Verbindungs-Settings aus ihrem eigenen Subtree —
actor-ts.persistence.journal.postgres, actor-ts.io.broker.kafka,
actor-ts.cache.redis und so weiter. Sie sind optionale Peers; Defaults
für sie auszuliefern hieße, eine Konfiguration für ein Paket
auszuliefern, das du vielleicht gar nicht installiert hast. Jedes ist auf
seiner eigenen Seite dokumentiert.
Alles, was kein Wert ist. Der Entity-Actor, Message-Extraktoren,
Allocation Strategies, Leases, Transports, Downing Provider, eigene
Backends — eine Config-Datei kann keine Klasse und keine Closure
ausdrücken, also bleiben die im Code und laufen über den jeweiligen
XOptions-Builder.
Präzedenz
Abschnitt betitelt „Präzedenz“Drei Schichten, höchste zuerst:
- Explizite Optionen im Code —
ClusterOptions.create().withPort(2552),ActorSystem.create('billing'). - Deine
application.conf(oder ein inlineconfig-Objekt). - Diese Datei.
Pro Feld, nicht pro Block: setzt du eine Failure-Detector-Schwelle im
Code, kommen die anderen beiden weiterhin aus deiner Config-Datei. Ein
undefined im Code bedeutet „nicht gesetzt” und fällt durch — es löscht
die darunterliegende Schicht nicht.
Keine toten Keys
Abschnitt betitelt „Keine toten Keys“Eine ganze Weile lang dokumentierte diese Datei rund fünfundzwanzig
Settings, die im Framework nie jemand gelesen hat. Du konntest
actor-ts.sharding.passivation-idle = 2 minutes setzen, bekamst keine
Passivierung und keine Warnung — der Wert wurde nicht abgelehnt, er wurde
nie angeschaut (Issue #653).
Das ist jetzt ein Build-Fehler. Ein Test läuft über jedes Blatt oben und
prüft, dass es sowohl von ConfigKeys aus erreichbar ist — der typisierten
Liste der Pfade, die das Framework kennt — als auch im Quellcode
referenziert wird.
Heute ist kein Key ausgenommen. Die Liste der bewussten Ausnahmen gibt
es weiterhin, und jeder Eintrag darauf muss die Issue-Nummer nennen, die ihn
wieder entfernt — sie ist nur leer. remote.tls.enabled war der letzte
Eintrag, und #591 hat sie
geleert, indem der Key einen Leser bekam: er entscheidet jetzt, ob der Node
beim Start warnt, dass die Cluster-Verbindung Klartext ist. Beachte, wo das
die Latte legt — gelesen, nicht umgesetzt. Die Verbindung zu
verschlüsseln ist weiterhin
#941; was dieser Test
ausschließt, ist ein Key, den niemand auch nur anschaut.
