Project: userver (App Definition and Development / Streaming & Messaging)
File: http_caching.md

Make sure that you can compile and run core tests and read a basic example @ref scripts/docs/en/userver/tutorial/hello_service.md. 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 @ref scripts/docs/en/userver/caches.md "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: ``` bash 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: ``` bash $ 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: @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - datatypes Our cache @ref scripts/docs/en/userver/component_system.md "component" should have the following fields: Reference to HTTP client, that is required to make HTTP requests URL to the incremental and full update handle String with last update time from remote. This string used only from the components::CachingComponentBase::Update overload and it is guaranteed @ref scripts/docs/en/userver/caches.md "that Update is not called concurrently", so there is no need to protect it from concurrent access. @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - component To create a non @ref scripts/docs/en/userver/lru_cache.md "LRU cache" cache you have to derive from components::CachingComponentBase, call CacheUpdateTrait::StartPeriodicUpdates() at the component constructor and CacheUpdateTrait::StopPeriodicUpdates() at the destructor: @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - constructor 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: @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - update 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`: @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - 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 `lastupdateremote_`. 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

Work with builder is quite straightforward: @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - GetAllData HTTP requests for incremental update differ only in URL and query parameter `last_update`: @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - GetUpdatedData To configure the new cache component provide its own options, options from components::CachingComponentBase: @snippet samples/httpcaching/staticconfig.yaml HTTP caching sample - static config cache Options for dependent components components::HttpClient, components::TestsuiteSupport and support handler server::handlers::TestsControl should be provided: @snippet samples/httpcaching/staticconfig.yaml HTTP caching sample - static config deps 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`: @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - GreetUser Note that the cache is concurrency safe @ref scripts/docs/en/userver/component_system.md "as all the components". Finally, we add our component to the `components::MinimalServerComponentList()`, and start the server with static configuration `kStaticConfig`. @snippet samples/httpcaching/httpcaching.cpp HTTP caching sample - main To build the sample, execute the following build steps at the userver root directory: ``` mkdir build_release cd build_release cmake -DCMAKEBUILDTYPE=Release .. make userver-samples-http_caching ``` Note that you need a running translations service with bulk handlers to start the service. You could use the @ref scripts/docs/en/userver/tutorial/mongo_service.md "mongo service" for that purpose. The sample could be started by running `make start-userver-samples-http_caching`. The command would invoke @ref scripts/docs/en/userver/functional_testing.md "testsuite start target" that sets proper paths in the configuration files and starts the service. To start the service manually run `./samples/httpcaching/userver-samples-httpcaching -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: ``` bash $ 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: ```json {"invalidate_caches": { "update_type": "incremental", "names": ["cache-http-translations"] }} ``` Fortunately, the @ref scripts/docs/en/userver/functional_testing.md "testsuite API" provides all the required functionality via simpler to use Python functions. @ref scripts/docs/en/userver/functional_testing.md "Functional tests" for the service could be implemented using the testsuite. To do that you have to: Mock the translations service data: @snippet samples/http_caching/tests/conftest.py translations Mock the translations service API: @snippet samples/http_caching/tests/conftest.py mockserver Import the pytest_userver.plugins.core plugin and teach testsuite how to patch the service config to use the mocked URL: @snippet samples/http_caching/tests/conftest.py patch configs Write the test: @snippet samples/httpcaching/tests/testhttp_caching.py Functional test See the full example: @ref samples/httpcaching/httpcaching.cpp @ref samples/httpcaching/staticconfig.yaml @ref samples/http_caching/CMakeLists.txt @ref samples/http_caching/tests/conftest.py @ref samples/httpcaching/tests/testhttp_caching.py @htmlonly <div class="bottom-nav"> @endhtmlonly @ref scripts/docs/en/userver/tutorial/tcpfull.md | @ref scripts/docs/en/userver/tutorial/flatbufservice.md @htmlonly </div> @endhtmlonly @example samples/httpcaching/httpcaching.cpp @example samples/httpcaching/staticconfig.yaml @example samples/http_caching/CMakeLists.txt @example samples/http_caching/tests/conftest.py @example samples/httpcaching/tests/testhttp_caching.py