Before you start
Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.
Step by step guide
An ability to change service behavior at runtime without restarting the service is priceless! We have that ability, it is called dynamic configs and it allows you:
In previous example we made a simple HTTP server with some dynamic configs set in stone. To make the dynamic configs dynamic for real the following steps should be done:
# yaml
dynamic-config-client: # A client that knows how to request configs via HTTP
config-url: http://localhost:8083/ # URL of dynamic config service
http-retries: 5
http-timeout: 20s
service-name: configs-service
fallback-to-no-proxy: false # On error do not attempt to retrieve configs
# by bypassing proxy from USERVER_HTTP_PROXY dynamic config
dynamic-config-client-updater: # A component that periodically uses `dynamic-config-client` to retrieve new values
update-interval: 5s # Request for new configs every 5 seconds
full-update-interval: 1m
config-settings: false
first-update-fail-ok: true
Now let's create a configuration service.
HTTP handler component
Dynamic configs are requested via JSON requests, so we need to create a simple JSON handler that is responding with config values.
There are two ways to write a JSON handler:
We are going to take the second approach:
struct ConfigDataWithTimestamp {
std::chrono::system_clock::time_point updated_at;
std::unordered_map<std::string, formats::json::Value> key_values;
};
public:
static constexpr std::string_view kName = "handler-config";
using KeyValues = std::unordered_map<std::string, formats::json::Value>;
const override;
void SetNewValues(KeyValues&& key_values) {
config_values_.Assign(ConfigDataWithTimestamp{
std::move(key_values),
});
}
private:
};
- Warning
Handle*
functions are invoked concurrently on the same instance of the handler class. Use synchronization primitives or do not modify shared data in Handle*
.
Note the rcu::Variable. There may be (and will be!) multiple concurrent requests and the HandleRequestJsonThrow
would be invoked concurrently on the same instance of ConfigDistributor
. The rcu::Variable allows us to atomically update the config value, concurrently with the HandleRequestJsonThrow
invocations.
Function ConfigDistributor::SetNewValues
is meant for setting config values to send. For example, you can write another component that accepts JSON requests with new config values and puts those values into ConfigDistributor
via SetNewValues(new_values)
.
HandleRequestJsonThrow
All the interesting things happen in the HandleRequestJsonThrow
function, where we grab a rcu::Variable snapshot, fill the update time and the configuration from it:
const {
const auto config_values_ptr = config_values_.Read();
result["configs"] = MakeConfigs(config_values_ptr, json);
const auto updated_at = config_values_ptr->updated_at;
request.GetHttpResponse().
SetContentType(http::content_type::kApplicationJson);
}
The "configs" field is formed in the MakeConfigs
function depending on the request parameters:
const auto updated_since = request[
"updated_since"].
As<std::string>({});
return configs;
}
LOG_DEBUG() <<
"Sending dynamic config for service " << request[
"service"].
As<std::string>(
"<unknown>");
const auto& values = config_values_ptr->key_values;
if (request["ids"].IsMissing()) {
for (const auto& [key, value] : values) {
configs[key] = value;
}
return configs;
}
for (const auto& id : request["ids"]) {
const auto key = id.As<std::string>();
const auto it = values.find(key);
if (it != values.end()) {
configs[key] = it->second;
} else {
LOG_ERROR() <<
"Failed to find config with name '" << key <<
"'";
}
}
return configs;
}
Note that the service name is sent in the "service" field of the JSON request body. Using it you can make service specific dynamic configs.
Static config
Now we have to configure our new HTTP handle. The configuration is quite straightforward:
# yaml
handler-config:
path: /configs/values
method: POST # Only for HTTP POST requests. Other handlers may reuse the same URL but use different method.
task_processor: main-task-processor
int main()
Finally, we add required components to the components::MinimalServerComponentList()
, and start the server with static config kStaticConfig
.
int main(int argc, char* argv[]) {
}
Build and Run
To build the sample, execute the following build steps at the userver root directory:
bash
mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-config_service
The sample could be started by running make start-userver-samples-config_service
. The command would invoke testsuite start target that sets proper paths in the configuration files and starts the service.
To start the service manually run ./samples/config_service/userver-samples-config_service -c </path/to/static_config.yaml>
.
Now you can send a request to your server from another terminal:
bash
$ curl -X POST -d '{}' 127.0.0.1:8083/configs/values | jq
{
"configs": {
"USERVER_DUMPS": {},
"USERVER_LRU_CACHES": {},
"USERVER_CACHES": {},
"USERVER_LOG_REQUEST": true,
"USERVER_TASK_PROCESSOR_QOS": {
"default-service": {
"default-task-processor": {
"wait_queue_overload": {
"action": "ignore",
"length_limit": 5000,
"time_limit_us": 3000
}
}
}
},
"USERVER_TASK_PROCESSOR_PROFILER_DEBUG": {},
"USERVER_LOG_REQUEST_HEADERS": true,
"USERVER_CANCEL_HANDLE_REQUEST_BY_DEADLINE": false,
"USERVER_HTTP_PROXY": ""
},
"updated_at": "2021-06-29T14:15:31.173239295+0000"
}
$ curl -X POST -d '{"ids":["USERVER_TASK_PROCESSOR_QOS"]}' 127.0.0.1:8083/configs/values | jq
{
"configs": {
"USERVER_TASK_PROCESSOR_QOS": {
"default-service": {
"default-task-processor": {
"wait_queue_overload": {
"action": "ignore",
"length_limit": 5000,
"time_limit_us": 3000
}
}
}
}
},
"updated_at": "2021-06-29T14:15:31.173239295+0000"
}
Functional testing
Functional tests for the service could be implemented using the service_client fixture in the following way:
async def test_config(service_client):
response = await service_client.post('/configs/values', json={})
assert response.status == 200
reply = response.json()
assert reply['configs']['USERVER_LOG_REQUEST_HEADERS'] is True
assert 'application/json' in response.headers['Content-Type']
async def test_config_specific_ids(service_client):
response = await service_client.post(
'/configs/values',
json={
'updated_since': '2021-06-29T14:15:31.173239295+0000',
'ids': ['USERVER_TASK_PROCESSOR_QOS'],
},
)
assert response.status == 200
reply = response.json()
assert len(reply['configs']) == 1
assert reply['configs']['USERVER_TASK_PROCESSOR_QOS']
assert 'application/json' in response.headers['Content-Type']
Do not forget to add the plugin in conftest.py:
pytest_plugins = ['pytest_userver.plugins.core']
Ready to use uservice-dynconf
Note that there is a ready to use opensource uservice-dynconf dynamic configs service. Use it for your projects or just disable dynamic config updates and keep developing without a supplementary service.
Full sources
See the full example: