Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.
Consider the case when you need to write an HTTP handler that greets a user. However, the localized strings are stored at some remote HTTP server, and those localizations rarely change. To reduce the network traffic and improve response times it is profitable to bulk retrieve the translations from remote and cache them in a local cache.
In this sample we show how to write and test a cache along with creating HTTP requests. If you wish to cache data from database prefer using cache specializations for DB.
For the purpose of this there is some HTTP server that has HTTP handler http://localhost:8090/v1/translations
.
Handler returns all the available translations as JSON on GET:
curl http://localhost:8090/v1/translations -s | jq
{
"content": {
"hello": {
"ru": "Привет",
"en": "Hello"
},
"wellcome": {
"ru": "Добро пожаловать",
"en": "Wellcome"
}
},
"update_time": "2021-11-01T12:00:00Z"
}
In case of query parameter "last_update" the handler returns only the changed translations since requested time:
$ curl http://localhost:8090/v1/translations?last_update=2021-11-01T12:00:00Z -s | jq
{
"content": {
"hello": {
"ru": "Приветище"
}
},
"update_time": "2021-12-01T12:00:00Z"
}
We are planning to cache those translations in a std::unordered_map:
Our cache component should have the following fields:
To create a non LRU cache cache you have to derive from components::CachingComponentBase, call CacheUpdateTrait::StartPeriodicUpdates() at the component constructor and CacheUpdateTrait::StopPeriodicUpdates() at the destructor:
The constructor initializes component fields with data from static configuration and with references to clients.
Depending on cache configuration settings the overloaded Update function is periodically called with different options:
In this sample full and incremental data retrieval is implemented in GetAllData() and GetUpdatedData() respectively.
Both functions return data in the same format and we parse both responses using MergeAndSetData
:
At the end of the MergeAndSetData function the components::CachingComponentBase::Set() invocation stores data as a new cache.
Update time from remote stored into last_update_remote_
. Clocks on different hosts are usually out of sync, so it is better to store the remote time if possible, rather than using a local times from last_update
and now
input parameters.
To make an HTTP request call clients::http::Client::CreateRequest() to get an instance of clients::http::Request builder. Work with builder is quite straightforward:
HTTP requests for incremental update differ only in URL and query parameter last_update
:
To configure the new cache component provide its own options, options from components::CachingComponentBase:
components_manager:
components: # Configuring components that were registered via component_list
cache-http-translations:
translations-url: 'mockserver/v1/translations' # Some other microservice listens on this URL
update-types: full-and-incremental
full-update-interval: 1h
update-interval: 15m
Options for dependent components components::HttpClient, components::TestsuiteSupport and support handler server::handlers::TestsControl should be provided:
Now the cache could be used just as any other component. For example, a handler could get a reference to the cache and use it in HandleRequestThrow
:
Note that the cache is concurrency safe as all the components.
Finally, we add our component to the components::MinimalServerComponentList()
, and start the server with static configuration kStaticConfig
.
To build the sample, execute the following build steps at the userver root directory:
Note that you need a running translations service with bulk handlers to start the service. You could use the mongo service for that purpose.
The sample could be started by running make start-userver-samples-http_caching
. 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/http_caching/userver-samples-http_caching -c </path/to/static_config.yaml>
(do not forget to prepare the configuration files!).
Now you can send a request to your server from another terminal:
$ curl -X POST http://localhost:8089/samples/greet?username=Dear+Developer -i HTTP/1.1 200 OK Date: Thu, 09 Dec 2021 17:01:44 UTC Content-Type: text/html; charset=utf-8 X-YaRequestId: 94193f99ebf94eb58252139f2e9dace4 X-YaSpanId: 4e17e02dfa7b8322 X-YaTraceId: 306d2d54fd0543c09376a5c4bb120a88 Server: userver/2.0 (20211209085954; rv:d05d059a3) Connection: keep-alive Content-Length: 61 Привет, Dear Developer! Добро пожаловать
The server::handlers::TestsControl and components::TestsuiteSupport components among other things provide necessary control over the caches. You can stop/start periodic updates or even force updates of caches by HTTP requests to the server::handlers::TestsControl handler.
For example the following JSON forces incremental update of the cache-http-translations
cache:
Fortunately, the testsuite API provides all the required functionality via simpler to use Python functions.
Functional tests for the service could be implemented using the testsuite. To do that you have to:
See the full example: