A userver-based service usually consists of components. A component is a basic building block that encapsulates dependencies logic with configuration and is able to interact with other components.
Example:
ClientB
, that requires initialization with some SettingsB
ClientA
, that requires initialization with some SettingsA
and a reference to ClientB
.In unit tests you could create the clients and fill the settings manually in code:
When it comes to production services with tens of components and hundreds of settings then the configuration from code becomes cumbersome and not flexible enough. Knowledge about all the dependencies of clients is required; many clients may depend on multiple other clients; some clients may be slow to start and those should be initialized concurrently...
Components to the rescue! A component takes care of constructing and configuring its own client. With components the configuration and dependency management problem is decomposed into small and manageable pieces:
Only components should know about components. Clients and other types constructed by components should not use components::ComponentConfig, components::ComponentContext, or components directly. All the components should inherit from components::LoggableComponentBase base class and may override its methods.
All the components are listed at the Components API Group.
On component construction a components::ComponentContext is passed as a second parameter to the constructor of the component. That context could be used to get references to other components. That reference to the component is guaranteed to outlive the component that is being constructed.
utils::DaemonMain, components::Run or components::RunOnce start all the components from the passed components::ComponentList. Each component is constructed in a separate engine::Task on the default task processor and is initialized concurrently with other components.
This is a useful feature, for example in cases with multiple caches that slowly read from different databases.
To make component A depend on component B just call components::ComponentContext::FindComponent<B>() in the constructor of A. FindComponent() suspends the current task and continues only after the construction of component B is finished. Components are destroyed in reverse order of construction, so the component A is destroyed before the component B. In other words - references from FindComponent() outlive the component that called the FindComponent() function. If any component loading fails, FindComponent() wakes up and throws an components::ComponentsLoadCancelledException.
It is a common practice to have a component that returns a reference R from some function F. In such cases:
Examples:
components::ManagerControllerComponent configures the engine internals from information provided in its static config: preallocates coroutines, creates the engine::TaskProcessor, creates threads for low-level event processing. After that it starts all the components that were added to the components::ComponentList. Each registered component should have a section in service config (also known as static config).
The component configuration is passed as a first parameter of type components::ComponentConfig to the constructor of the component. Note that components::ComponentConfig extends the functionality of yaml_config::YamlConfig with YamlConfig::Mode::kEnvAllowed mode that is able to substitute variables with values, use environment variales and fallbacks. See yaml_config::YamlConfig for more info and examples.
All the components have the following options:
Name | Description | Default value |
---|---|---|
load-enabled | set to false to disable loading of the component | true |
To validate static configs you only need to define member function of your component GetStaticConfigSchema()
All schemas and sub-schemas must have description
field and can have defaultDescription
field if they have a default value.
Scope of static config validatoin can be specified by validate_all_components
section of components_manager
config. To disable it use:
You also can force static config validation of your component by adding components::kHasValidate
Supported types:
boolean
, string
, integer
, double
object
must have options additionalProperties
and properties
array
must have option items
You can configure the configuration mode of the component in the configuration file by specializing the components::kConfigFileMode
template variable in the file with component declaration Supported mode:
kConfigFileMode::kRequired
- The component must be defined in configuration filekConfigFileMode::kNotRequired
- The component may not be defined in the configuration fileUsers of the framework may (and should) write their own components.
Components provide functionality to tie the main part of the program with the configuration and other components. Component should be lightweight and simple.
You need a component if:
Start writing your component from adding a header file with a class inherited from components::LoggableComponentBase.
In source file write the implementation of the component:
Destructor of the component is invoked on service shutdown. Components are destroyed in the reverse order of construction. In other words, references from context.FindComponent<components::DynamicConfig>()
outlive the component.
If you need dynamic configs, you can get them using this approach:
Do not forget to register your component in components::ComponentList before invoking the utils::DaemonMain, components::Run or components::RunOnce.
Done! You've implemented your first component. Full sources:
Starting up the components in unit tests is quite hard. Prefer moving out all the functionality from the component or testing the component with the help of testsuite functional tests.