For schemas of dynamic configs used by userver itself:
For information on how to write a service that distributes dynamic configs:
Dynamic config is a system of options that can be changed at runtime without restarting the service.
Dynamic config is distributed as a large JSON object where each direct member is called a dynamic config variable, or (somewhat confusingly) a dynamic config. For example:
Dynamic configs are an essential part of a reliable service with high availability. The configs could be used as an emergency switch for new functionality, selector for experiments, limits/timeouts/log-level setup, proxy setup and so forth.
See Production configs and best practices setup example.
Dynamic config values are obtained via the dynamic_config::Source client that is retrieved from components::DynamicConfig:
To read the config, you first need to define a global dynamic_config::Key variable for it. Second, you should get the current value from the config using the Key
:
You can also subscribe to dynamic config updates using dynamic_config::Source::UpdateAndListen functions, see their docs for details.
A dynamic_config::Key requires 3 main elements:
A type-safe map that stores a snapshot of all used configs. dynamic_config::Snapshot is cheaply copyable (and even more cheaply movable), has the semantics of std::shared_ptr<const Impl>
. An obtained dynamic_config::Snapshot instance should be stored while its configs are used or referred to.
A reference to the configs storage, required for obtaining the actual config value and subscription to new values.
The component that stores configs in production and testsuite tests.
For unit tests, dynamic_config::StorageMock is used instead.
components::DynamicConfig&
. Most code should immediately call GetSource()
on it, then use or store the dynamic_config::Source in a field.OnConfigUpdate
and store it into a "comfy" class field. It creates a synchronization problem out of the blue, while it has already been solved in the dynamic config API. Just store dynamic_config::Source in a field and call dynamic_config::Source::GetSnapshot where you need to read the config.OnConfigUpdate
will not be called afterward; then other fields of the class will be destroyed.OnConfigUpdate
will not be called at the point when it is already unable to run safely.The following can be applied to parsing formats::json::Value in any context, but it frequently comes up in the context of defining configs.
If your dynamic config is any more complex than a trivial type, then you need to ensure that JSON parsing is defined for it.
JSON leafs can be parsed out of the box:
OpenAPI | C++ type |
---|---|
boolean | bool |
integer | integer types, e.g. uint32_t , size_t |
numeric | double |
string | std::string |
If the whole config variable is of such type, then the default value for it can be passed to dynamic_config::Key directly (without using dynamic_config::DefaultAsJsonString).
Chrono durations are stored in JSON as integers:
json.As<std::chrono::seconds>()
will parse 10
as 10 seconds;json.As<std::chrono::milliseconds>()
will parse 10
as 10 milliseconds;minutes
and hours
.To allow humans to more easily differentiate between them while looking at the JSON, we recommend ending the config name or the object label with one of the following suffixes: _MS
, _SECONDS
, _MINUTES
, _HOURS
(in the appropriate capitalization).
A string that may only be selected a finite range of values should be mapped to C++ enum class
. Parsers for enums currently have to be defined manually. Example enum parser:
Objects are represented as C++ structs. Parsers for structs currently have to be defined manually. Example struct config:
To represent optional (non-required) properties, use std::optional
or As
with default. For example:
json.As<std::optional<int>>()
will parse a missing value as an empty optional;json.As<int>(42)
will parse a missing value as 42
;json.As<std::vector<int>>({})
will parse a missing value as a default-constructed vector
;field = json.As<T>(field)
.To enable support for std::optional
:
To represent JSON arrays, use containers, typically:
std::vector<T>
std::unordered_set<T>
std::set<T>
To represent JSON objects with unknown keys (OpenAPI's additionalProperties
), use map containers, typically:
std::unordered_map<std::string, T>
std::map<std::string, T>
To enable support for such containers, use the following:
If the nested type is your custom type, make sure to define Parse
for it:
enum class
.Config defaults are specified in their C++ definition. They may be overridden in the static config of dynamic-config
component:
If utils::DaemonMain is used, then the default dynamic configs can be printed by passing --print-dynamic-config-defaults
command line option. It does not account for overrides in dynamic-config
(components::DynamicConfig).
Dynamic config defaults are used in the following places:
dynamic-config.updates-enabled: false
)dynamic-config-client-updater
(components::DynamicConfigClientUpdater) (or a custom updater component) receives response from the configs service with some configs missing, then dynamic-config
(components::DynamicConfig) fills in those configs from defaults.dynamic-config
component loads the config cache file, and some configs are missing, then those are filled in from defaults.dynamic-config
(components::DynamicConfig) itself is required for the functioning of various core userver components, so it is included in both components::MinimalComponentList and components::CommonComponentList.
By default, it is configured to function in the "static dynamic config" mode and may be omitted from the static config entirely.
If you rely on obtaining ground truth configs from a configs service (as it is advised for best experience; described below), then you may skip this section entirely and use defaults only for testing.
On the other hand, if you prefer to run without a configs service, then you may want to override some of the built-in userver configs.
Defaults can be overridden in the config of the dynamic-config
component:
In this case YAML is automatically converted to JSON, then parsed as usual.
Alternatively, you can pass the path to a JSON file with defaults:
It somewhat complicates the deployment process, though.
Suppose that we've managed to persuade you to wind up a configs service and start actively using dynamic configs.
You will need the following components to download configs:
Both are included in components::CommonComponentList.
Here is a reasonable static config for those:
Suppose that the new revision of the current service released before the newly added config was accounted by the config service. In this case it should just return the configs it knows about. The current service will fill in the blanks from the defaults.
If the config service gives out invalid configs (which fail to be parsed), then the periodic config update will fail, and alerts will fire:
dynamic-config.parse-errors
metric (1) will be incremented, and dynamic-config.was-last-parse-successful
metric (2) will be set to 1. You can combine those to safely detect parsing errors in the metrics backend: If the config service is not accessible at this point (down or overloaded), then the periodic config update will also obviously fail.
After a config update failure, if the service has already started, then it will keep using the previous successfully fetched configs. Also, you can monitor config update health using
and related metrics.
If the first config update fails, then the service will read the config cache file specified in dynamic-config.fs-cache-path
, which is hopefully left since the previous service start.
If the first config update fails, and the config cache file is missing, then the service will fail to start, showing a helpful log message. Defaults are not used in this case, because they may be significantly outdated, and to avoid requiring the developer to always keep defaults up-to-date with production requirements. Another reason for such behavior is that the dynamic configs are used to fix up incidents, so such check of the dynamic configs service at first deployment prevents incident escalation due to unnoticed misconfiguration (missing routes to dynamic config service, broken authorization, ...).
If you still wish to boot the service using just dynamic config defaults, you can create a config cache file with contents {}
, or bake a config cache file into the service's Docker image.
Main testing page:
dynamic_config::StorageMock stores configs for unit tests and benchmarks. It must be kept alive while any dynamic_config::Source or dynamic_config::Snapshot is accessing it.
Test code can obtain the default configs using dynamic_config::GetDefaultSnapshot or dynamic_config::GetDefaultSource, or override some configs using dynamic_config::MakeDefaultStorage.
Main testsuite page:
Dynamic config can be overridden specifically for testsuite. It can be done globally in the following ways:
--config-fallback
option to pytest. When using userver_testsuite_add_simple
to setup tests in CMake, it is enough to place the dynamic_config_fallback.json
file next to the static config.The various config patches are applied in the following order, going from the lowest to the highest priority:
dynamic-config.defaults
option from static config , if any--config-fallback
, if anydynamic_config_fallback_patch
, if anyIf the service has config updates disabled, then there is no way to change configs per-test. You can resort to creating multiple separate directories for testsuite tests and overriding the initial dynamic config (as shown above) in each of those directories in different ways.
If the service has config updates enabled, then you can change dynamic config per-test as follows:
Dynamic config can also be modified mid-test using dynamic_config fixture.
Such dynamic config changes are applied (sent to the service) at the first service_client
request in the test, or manually: