What rtb-config does not do¶
Every limit below is real and current as of v0.6.3. Some are deliberate design choices, some are consequences of the layering engine underneath, and a couple are sharp enough that you want to know about them before you build on top.
Can I use a TOML or JSON config file?¶
Not as an input, no. ConfigBuilder::user_file parses every file as YAML,
whatever its extension. A .toml file registered as a user file fails with
ConfigError::Parse complaining that a string was found where a map was expected.
JSON files happen to work, because YAML is a superset of JSON — but that is a property of the parser, not a supported format.
This is asymmetric with writing. Under the mutable feature Config::write picks
its format from the extension and will happily emit TOML, and that file cannot then
be read back by this crate. If you write config out with the intention of reading it
again, write YAML or JSON.
There is no way to register a file with an explicit format, and no other input
format — no INI, no .env, no HCL.
Why does my underscored field ignore its environment variable?¶
Because the underscore in the variable name is read as a level of nesting, not as part of a key.
The environment layer is registered with underscore-splitting on, which is what makes
MYTOOL_HTTP_PORT populate a nested http.port. The same rule applies to a flat
field whose name contains an underscore:
MYTOOL_MAX_RETRIES=7 becomes the nested key max.retries, which your struct does
not have. Unknown keys are ignored, so nothing fails — the value simply stays at
whatever the file or the embedded default said, with no error and no warning.
There is no option to turn the splitting off. The workarounds are structural:
- Use nested structs rather than underscored names. A
http: HttpSectionwith aportfield is reachable asMYTOOL_HTTP_PORT; a flathttp_portis not. - Use single-word field names where a value has to be settable from the environment.
- Set the value in a config file instead, where
max_retries: 7works normally.
The related trap is the prefix itself. env_prefixed("MYTOOL") without the trailing
underscore leaves the separator on the front of every key and silently matches
nothing. Register "MYTOOL_".
Which file changes does the watcher actually see?¶
Writes into the existing file, reliably. Changes that replace the file, once at most — and then the watcher stops seeing anything at all.
The watcher registers each user_file path with the OS at the moment watch_files
is called. Rewriting the file in place — the usual write(2) an application does —
fires an event and reloads every time. But a save implemented as write a temporary
file, then rename it over the target replaces the thing being watched. The first
such save may still be reported; every change after it, including ordinary in-place
writes, is not, and the WatchHandle stays alive and reports no error.
Reproduced on Linux with the inotify backend, deterministically: three consecutive rename-replace saves reload the config once, and in-place writes afterwards never land. Treat the other platforms' backends as unverified rather than as known-good.
That matters more than it sounds, because rename-and-replace is how a lot of software saves files:
- Editors that save atomically. Writing to a temporary file and renaming it over
the original is a common way to make a save crash-safe, and an editor doing that
looks like a replacement rather than an edit. vim's
backupcopysetting controls exactly this choice. - Anything writing config atomically, which includes the temp-file-and-rename
pattern
Config::writetells you to implement yourself. - Kubernetes ConfigMap and Secret volumes. The kubelet publishes an update by writing a new directory and swapping a symlink, not by rewriting the file in place. That is the same shape of change, so hot-reload from a mounted ConfigMap should be treated as unproven here until someone has watched it work against a real cluster.
Until this is fixed, a service that must pick up every change is better off calling
Config::reload on a timer, or on a signal, than trusting the watcher to keep up.
Can I find out which layer a value came from?¶
No. Once the layers are merged, a value is just a value — there is no provenance API, nothing that answers "did this port come from the file or from the environment?", and nothing that reports which keys an environment variable overrode.
This is the single biggest gap between rtb-config and the Go toolkit's config
module, and it is the headline feature of the planned v2 store architecture. Today,
if you need to show a user where a setting came from, you have to build the answer
yourself by loading the layers separately.
Will writing config back preserve my file?¶
No. Config::write serialises the current in-memory value and replaces the target
file wholesale. Three consequences:
- Comments and key order are lost. A hand-written config file with explanatory comments comes back as machine-ordered YAML with none of them.
- Every layer is flattened into the file. What gets written is the merged
value, so embedded defaults become explicit keys, and any environment override
active at that moment is baked in as if a human had typed it. A
config setbuilt naively on top of this turns a temporaryMYTOOL_PORT=9999into a permanent file entry. - The write is not atomic. A crash mid-write leaves a truncated file, and the next start fails to parse it.
Structure-preserving writes are the other half of the planned v2 work. Until then, a
tool that edits a user's config file is better off editing that file directly than
round-tripping it through Config::write.
Is there a CLI-flag layer?¶
No. The layering stops at environment variables. There is no provider for
command-line arguments, and no merge entry point that would let you push a parsed
clap struct in as the top layer — the source list is fixed at three kinds and is
populated only through the builder.
Tools that want flags to win over the environment resolve that themselves after
get(), or take the value from clap in preference to the config field at the point
of use.
Are there remote or dynamic sources?¶
No. Files on the local filesystem, strings compiled into the binary, and the process
environment. There is no support for Consul, etcd, S3, a secrets manager or an HTTP
endpoint, and no plugin trait that would let you add one — Sources is a private
struct with three fixed vectors.
A value fetched from a remote system has to be fetched by your code and supplied
another way, for instance by setting an environment variable before build().
Does it validate anything beyond types?¶
Only what serde does. There is no range checking, no cross-field validation, no
required-unless rules, and no schema enforcement on load — Config::schema generates
a JSON Schema for export but nothing in the crate validates against it.
Unknown keys are ignored by default, so a misspelled setting silently does nothing.
#[serde(deny_unknown_fields)] on your struct turns that into an error, with the
caveat noted in the API reference.
Does it do anything about secrets?¶
Almost nothing. Config fields are ordinary types, so a token in your config struct is
a String in memory like any other, and it will appear in anything that serialises or
debug-prints the struct, Config::write included.
The one protection is that Config's own Debug implementation never renders the
stored value — it prints the file paths, environment prefixes and layer count instead.
That stops tracing::debug!("{config:?}") leaking a secret, and nothing more.
Can I reload just one part of the configuration?¶
No. reload() re-reads every source and replaces the whole value. There is no
per-key reload, no way to hold one section fixed, and no diff of what changed — a
subscriber is woken and has to work out for itself whether the part it cares about
moved.
Note also that a Config built with with_value or Default has no sources, so
reloading one does not preserve the value it was given; it re-parses an empty source
set. For a struct whose fields all default, that silently resets everything to
Default.
What replaces these limits?¶
A v2 store architecture — value provenance and structure-preserving writes — is approved for this repository and will address the middle two sections above. It is not implemented, no release carries it, and the figment-backed API documented here is the supported surface until it lands.
Next¶
- Layering and precedence — how the merge actually works
- Error reference — the failures that are reported