Github   Telegram
Loading...
Searching...
No Matches
Component system

Any userver-based service consists of components. A component is a basic building block that encapsulates dependencies logic with configuration and is able to interact with other components.

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.

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 that is able to substitute variables with values, use 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, define member function of your component GetStaticConfigSchema() and specialize variable components::kHasValidate

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 bloking FS syscalls
)");
}
} // namespace myservice::smth
template <>
inline constexpr bool components::kHasValidate<myservice::smth::Component> =
true;

All schemas and sub-schemas must have description field and can have defaultDescription field if they have a default value.

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;

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 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 which makes them initialize concurrently. This is a useful feature, for examples 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

It is a common practice to have a component that returns 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()

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 {
namespace {
int ParseRuntimeCfg(const dynamic_config::DocsMap& docs_map) {
return docs_map.Get("SAMPLE_INTEGER_FROM_RUNTIME_CONFIG").As<int>();
}
} // namespace
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.