userver: Component system
Loading...
Searching...
No Matches
Component system

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:

  • There is a ClientB, that requires initialization with some SettingsB
  • There is a ClientA, that requires initialization with some SettingsA and a reference to ClientB.
struct SettingsB{ /*...*/ };
class ClientB {
public:
ClientB(SettingsB settings);
};
struct SettingsA{ /*...*/ };
class ClientA {
public:
ClientA(ClientB& b, SettingsA settings);
};

In unit tests you could create the clients and fill the settings manually in code:

ClientB b({.path="/opt/", .timeout=15s});
ClientA a(b, {.ttl=3, .skip={"some"}});
a.DoSomething();

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:

public:
ComponentA(const components::ComponentConfig& config,
: client_a_(
context.FindComponent<ComponentB>().GetClientB(),
{.ttl=config["ttl"].As<int>(), .skip=config["skip"].As<std::vector<std::string>>()}
)
{}
ClientA& GetClientA() const { return client_a_; }
private:
ClientA client_a_;
};

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.

Startup context

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.

Components construction and destruction order

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.

References from components and lifetime of clients

It is a common practice to have a component that returns a reference R from some function F. In such cases:

  • a reference R lives as long as the component is alive
  • a reference R is usually a client
  • and it is safe to invoke member functions of reference R concurrently unless otherwise specified.

Examples:

  • components::HttpClient::GetHttpClient()
  • components::StatisticsStorage::GetStorage()

Components static configuration

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

Static configs validation

To validate static configs you only need to define member function of your component GetStaticConfigSchema()

namespace myservice::smth {
yaml_config::Schema Component::GetStaticConfigSchema() {
return yaml_config::MergeSchemas<components::LoggableComponentBase>(R"(
type: object
description: user component smth
additionalProperties: false
properties:
some-url:
type: string
description: url for something
fs-task-processor:
type: string
description: name of the task processor to do some blocking FS syscalls
)");
}
} // namespace myservice::smth

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:

components_manager:
static_config_validation:
validate_all_components: false

You also can force static config validation of your component by adding components::kHasValidate

template <>
inline constexpr bool components::kHasValidate<myservice::smth::Component> =
true;
Note
There are plans to use it to generate documentation.

Supported types:

  • Scalars: boolean, string, integer, double
  • object must have options additionalProperties and properties
  • array must have option items

Setup config file mode

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 file
  • kConfigFileMode::kNotRequired - The component may not be defined in the configuration file
template <>
inline constexpr auto components::kConfigFileMode<myservice::smth::Component> =
ConfigFileMode::kNotRequired;

Writing your own components

Users 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.

Note
Rule of a thumb: if you wish to unit test some code that is located in the component, then in 99% of cases that code should not be located in the component.

Should I write a new component or class would be enough?

You need a component if:

  • you need a static config
  • you need to work with other components
  • you are writing clients (you need a component to be the factory for your clients)
  • you want to subscribe for configs or cache changes

HowTo

Start writing your component from adding a header file with a class inherited from components::LoggableComponentBase.

#pragma once
namespace myservice::smth {
class Component final : public components::LoggableComponentBase {
public:
// name of your component to refer in static config
static constexpr std::string_view kName = "smth";
Component(const components::ComponentConfig& config,
int DoSomething() const;
~Component() final;
static yaml_config::Schema GetStaticConfigSchema();
private:
dynamic_config::Source config_;
};
} // namespace myservice::smth

In source file write the implementation of the component:

#include <userver/dynamic_config/value.hpp>
namespace myservice::smth {
Component::Component(const components::ComponentConfig& config,
: components::LoggableComponentBase(config, context),
config_(
// Searching for some component to initialize members
context.FindComponent<components::DynamicConfig>()
.GetSource() // getting "client" from a component
) {
// Reading config values from static config
[[maybe_unused]] auto url = config["some-url"].As<std::string>();
const auto fs_tp_name = config["fs-task-processor"].As<std::string>();
// Starting a task on a separate task processor from config
auto& fs_task_processor = context.GetTaskProcessor(fs_tp_name);
utils::Async(fs_task_processor, "my-component/fs-work", [] { /*...*/ }).Get();
// ...
}
} // namespace myservice::smth

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:

namespace myservice::smth {
inline const dynamic_config::Key kMyConfig{"SAMPLE_INTEGER_FROM_RUNTIME_CONFIG",
42};
int Component::DoSomething() const {
// Getting a snapshot of dynamic config.
const auto runtime_config = config_.GetSnapshot();
return runtime_config[kMyConfig];
}
} // namespace myservice::smth
Note
See Writing your own configs server for info on how to implement your own config server.

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:

Note
For info on writing HTTP handler components refer to the Writing your first HTTP server.

Testing

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.