fix(config): reject malformed env overrides and expand ${VAR} in TOML - #300
Merged
Conversation
farhan-syah
force-pushed
the
fix/config-startup-gate
branch
from
September 6, 2026 20:12
f28b3e3 to
75af8dd
Compare
…oring them Replace the per-module NODEDB_* env parsing (checkpoint, cluster, host_ports, numeric, timeseries, tls, wal, dispatch, helpers) with a single table-driven gate that validates every override and fails boot on a bad value rather than silently falling back to a default. Move loop intervals and the cluster join-retry policy (clone sweep, constraint reconcile, scope expiry, join attempts/backoff) out of ad hoc std::env::var reads scattered across background loops and into ServerConfig/MaintenanceTuning/ClusterSettings fields, so the same validated value reaches both production and the cluster test harness.
Lets operators reference environment variables directly in a config
file's TOML source, substituted before parsing. Comments and escaped
$${NAME} are left untouched, and an unset or malformed placeholder
fails config load instead of falling through to a TOML parse error.
Values applied later via NODEDB_* env overrides still take precedence
over an expanded placeholder.
Cover the new config surface: sync_host, TLS certificate material,
WAL tuning, cluster settings, maintenance intervals, and
observability options, plus the startup-validation error format and
${VAR} placeholder expansion in TOML files.
farhan-syah
force-pushed
the
fix/config-startup-gate
branch
from
September 6, 2026 20:32
75af8dd to
e681470
Compare
Reset tenant rate counters each second in the pgwire test harness's poller loop, since the server's own background loop that normally does this is never started for tests, so quotas hit once a test exceeded them. Shrink the memtable budget in the timeseries flush tests instead of ingesting millions of rows to cross the shipped 64 MiB default, which timed out on loaded CI runners. Rework the reindex-concurrent stall bound to scale with the rebuild window's own duration and gate on p99 rather than a single slowest sample, throttle its query load below the tenant's default rate limit, and surface the full error chain from failed queries.
farhan-syah
force-pushed
the
fix/config-startup-gate
branch
from
September 7, 2026 02:33
662dc05 to
fc7cc00
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #277. Closes #278.
What this does and why
Both issues are one defect at two stages of config load. An operator supplies a value from the environment, and the server silently uses a different one.
toml::from_str${DATA_DIR}stays literal, or serde throws a type errorThe rule
An operator-supplied environment value is honored, or the process refuses to start. It is never replaced by a config value or a compiled default.
Three kinds of violation:
NODEDB_DATA_PLANE_CORES=abc.64KiB.NODEDB_NODE_IDwith no[cluster]section.One exception. A request the process already satisfies is honored.
NODEDB_TLS_PGWIRE=falsewith no[server.tls]section leaves a plaintext listener, which is what was asked.The gate collects every violation and reports them together, so one typo never hides the next:
Why a table, not 30
ResultsThe override surface had grown three different swallow styles.
warn!-and-continue inenv/*.rs. A silentif let Ok(..) && let Ok(..)inobservability.rs. And.ok().and_then(..).unwrap_or(default)in the bootstrap and cluster paths. Each variable carried its own hand-written read-parse-fallback block, which is how they drifted.Threading
Resultthrough those blocks fixes today's variables and leaves the next one free to repeat the mistake. The gate is one table instead. EachNODEDB_*variable is a row of name, apply function, and redact flag. Adding a variable is adding a row, and there is no per-variable code to write a swallow into. Nine hand-written parser modules are deleted.Also in this change
NODEDB_TLS_CERT_PATHandNODEDB_TLS_KEY_PATHcreate a[server.tls]section when the file has none. One without the other is rejected.true/1/yesandfalse/0/no. The observability toggles took onlytrue/false.NODEDB_OTLP_EXPORT_ENDPOINTrequires anhttp://orhttps://scheme.NODEDB_DATA_PLANE_CORES=0is rejected. It parses as ausizeand asks the Data Plane for a shard count no query can reach.NODEDB_DATA_DIR=is a failed template substitution, not a request..max(10)clamp inscope/expiry.rsis gone. An operator who sets5gets a startup error naming the floor, not a silent10.ServerConfig::validate()re-checks all 14 constrained fields on the loaded config. A TOML file therefore cannot reach a field the gate guards. Both paths read one set of bounds inconfig/server/domain.rs.Expansion details
${VAR}substitution is textual and runs before the parse, so quoting is the operator's choice.data_dir = "${DATA_DIR}"yields a string and bareports.pgwire = ${PORT}yields an integer.$${NAME}escapes to a literal${NAME}.A six-state scanner tracks TOML strings and comments, so a placeholder inside a comment is left alone. A shipped template can carry a commented-out example line without requiring the variable it names.
No shell expressions, no command substitution, no recursive expansion, no
${VAR:-default}. Substituted values are never logged, only the variable names.Commits
${VAR}expansion inServerConfig::from_filedocs/getting-started.mdHow to test
Both stages pass on this branch, with no flaky retries.
The change adds 76 tests. 59 cover the gate across
nodedb/tests/config_env_{listeners,sizing,durability,cluster,observability,maintenance}.rs, one per variable and constraint. 17 more are inline, covering the expansion scanner andServerConfig::from_file.Every gate test asserts that the error names both the variable and the rejected value. An error carrying neither reads like the warn-and-continue it replaces, so the value check alone is not enough.
To see it by hand:
Tradeoffs and alternatives
This is a behavior change, and deployments can break on it. A node that boots today on a mistyped variable stops booting. That is the point of the fix, and the pre-1.0 window is the right time for it.
Well-formed but inapplicable values now fail too.
NODEDB_TLS_*orNODEDB_NODE_IDset with no matching config section stops startup. This costs the pattern of one shared env block across a TLS and a non-TLS deployment. The alternative drops the operator's intent silently. On a TLS toggle that means serving plaintext to someone who asked for TLS.Expansion skips comments, which costs a scanner. A regex over the file text is far shorter. It also fails the first template shipping a commented-out
# data_dir = "${DATA_DIR}". That refuses a boot over a variable the server never reads.Rejected: a strict-config opt-in flag. Enforcement of an invariant that must always hold does not belong behind a flag.
Rejected: threading
Resultthrough the existing per-variable blocks. Smaller diff, same shape, and it leaves the next variable free to swallow again.No performance-sensitive path changes, so there are no benchmark numbers to report.