Before you start
Make sure that you can compile and run core tests as described at Configure and Build.
Note that there is a ready to use opensource service template to ease the development of your userver based services. The template already has a preconfigured CI, build and install scripts, testsuite and unit-tests setups.
Step by step guide
Typical HTTP server application in userver consists of the following parts:
- HTTP handler component - main logic of your application
- Static config - startup config that does not change for the whole lifetime of an application
- Dynamic config - config that could be changed at runtime
- int main() - startup code
Let's write a simple server that responds with "Hello world!\n" on every request to /hello
URL.
HTTP handler component
HTTP handlers must derive from server::handlers::HttpHandlerBase
and have a name, that is obtainable at compile time via kName
variable and is obtainable at runtime via HandlerName()
.
The primary functionality of the handler should be located in HandleRequestThrow
function. Return value of this function is the HTTP response body. If an exception exc
derived from server::handlers::CustomHandlerException
is thrown from the function then the HTTP response code will be set to exc.GetCode()
and exc.GetExternalErrorBody()
would be used for HTTP response body. Otherwise if an exception exc
derived from std::exception
is thrown from the function then the HTTP response code will be set to 500
.
namespace samples::hello {
public:
static constexpr std::string_view kName = "handler-hello-sample";
using HttpHandlerBase::HttpHandlerBase;
std::string HandleRequestThrow(
return "Hello world!\n";
}
};
}
- 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*
.
Static config
Now we have to configure the service by providing coro_pool
, task_processors
and default_task_processor
options for the components::ManagerControllerComponent and configuring each component in components
section:
# yaml
components_manager:
coro_pool:
initial_size: 500 # Preallocate 500 coroutines at startup.
max_size: 1000 # Do not keep more than 1000 preallocated coroutines.
task_processors: # Task processor is an executor for coroutine tasks
main-task-processor: # Make a task processor for CPU-bound couroutine tasks.
worker_threads: 4 # Process tasks in 4 threads.
thread_name: main-worker # OS will show the threads of this task processor with 'main-worker' prefix.
fs-task-processor: # Make a separate task processor for filesystem bound tasks.
thread_name: fs-worker
worker_threads: 4
default_task_processor: main-task-processor
components: # Configuring components that were registered via component_list
server:
listener: # configuring the main listening socket...
port: 8080 # ...to listen on this port and...
task_processor: main-task-processor # ...process incoming requests on this task processor.
logging:
fs-task-processor: fs-task-processor
loggers:
default:
file_path: '@stderr'
level: debug
overflow_behavior: discard # Drop logs if the system is too busy to write them down.
tracer: # Component that helps to trace execution times and requests in logs.
service-name: hello-service # "You know. You all know exactly who I am. Say my name. " (c)
dynamic-config: # Dynamic config storage options, do nothing
fs-cache-path: ''
dynamic-config-fallbacks: # Load options from file and push them into the dynamic config storage.
fallback-path: /etc/hello_service/dynamic_config_fallback.json
handler-hello-sample: # Finally! Our handler.
path: /hello # Registering handler by URL '/hello'.
method: GET,POST # It will only reply to GET (HEAD) and POST requests.
task_processor: main-task-processor # Run it on CPU bound task processor
Note that all the components and handlers have their static options additionally described in docs.
Dynamic config
We are not planning to get new dynamic config values in this sample. Because of that we just write the defaults to the fallback file of the components::DynamicConfigFallbacks
component.
All the values are described in a separate section Dynamic configs .
{
"BAGGAGE_SETTINGS": {
"allowed_keys": []
},
"HTTP_CLIENT_CONNECTION_POOL_SIZE": 1000,
"HTTP_CLIENT_CONNECT_THROTTLE": {
"max-size": 100,
"token-update-interval-ms": 0
},
"USERVER_BAGGAGE_ENABLED": false,
"USERVER_CACHES": {},
"USERVER_CANCEL_HANDLE_REQUEST_BY_DEADLINE": false,
"USERVER_CHECK_AUTH_IN_HANDLERS": true,
"USERVER_DEADLINE_PROPAGATION_ENABLED": true,
"USERVER_DUMPS": {},
"USERVER_FILES_CONTENT_TYPE_MAP": {
".css": "text/css",
".gif": "image/gif",
".htm": "text/html",
".html": "text/html",
".jpeg": "image/jpeg",
".js": "application/javascript",
".json": "application/json",
".md": "text/markdown",
".png": "image/png",
".svg": "image/svg+xml",
"__default__": "text/plain"
},
"USERVER_HANDLER_STREAM_API_ENABLED": false,
"USERVER_HTTP_PROXY": "",
"USERVER_LOG_DYNAMIC_DEBUG": {
"force-disabled": [],
"force-enabled": []
},
"USERVER_LOG_REQUEST": true,
"USERVER_LOG_REQUEST_HEADERS": false,
"USERVER_LRU_CACHES": {},
"USERVER_NO_LOG_SPANS": {
"names": [],
"prefixes": []
},
"USERVER_RPS_CCONTROL": {
"down-level": 1,
"down-rate-percent": 2,
"min-limit": 10,
"no-limit-seconds": 1000,
"overload-off-seconds": 3,
"overload-on-seconds": 3,
"up-level": 2,
"up-rate-percent": 2
},
"USERVER_RPS_CCONTROL_ACTIVATED_FACTOR_METRIC": 5,
"USERVER_RPS_CCONTROL_CUSTOM_STATUS": {},
"USERVER_RPS_CCONTROL_ENABLED": false,
"USERVER_TASK_PROCESSOR_PROFILER_DEBUG": {
"fs-task-processor": {
"enabled": false,
"execution-slice-threshold-us": 1000000
},
"main-task-processor": {
"enabled": false,
"execution-slice-threshold-us": 2000
}
},
"USERVER_TASK_PROCESSOR_QOS": {
"default-service": {
"default-task-processor": {
"wait_queue_overload": {
"action": "ignore",
"length_limit": 5000,
"time_limit_us": 3000
}
}
}
}
}
A production ready service would dynamically retrieve the above options at runtime from a configuration service. See Writing your own configs server for insights on how to change the above options on the fly, without restarting the service.
int main()
Finally, after writing down the dynamic config values into file at dynamic-config-fallbacks.fallback-path
, we add our component to the components::MinimalServerComponentList()
, and start the server with static configuration file passed from command line.
int main(int argc, char* argv[]) {
const auto component_list =
}
Build and Run
To build the sample, execute the following build steps at the userver root directory:
mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-hello_service
The sample could be started by running make start-userver-samples-hello_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/hello_service/userver-samples-hello_service -c </path/to/static_config.yaml>
(do not forget to prepare the configuration files!).
- Note
- Without file path to
static_config.yaml
userver-samples-hello_service
will look for a file with name config_dev.yaml
-
CMake doesn't copy
static_config.yaml
and dynamic_config_fallback.json
files from samples
directory into build directory.
Now you can send a request to your server from another terminal:
bash
$ curl 127.0.0.1:8080/hello
Hello world!
Functional testing
Functional tests for the service could be implemented using the service_client fixture from pytest_userver.plugins.core in the following way:
async def test_ping(service_client):
response = await service_client.get('/hello')
assert response.status == 200
assert response.content == b'Hello world!\n'
Do not forget to add the plugin in conftest.py:
pytest_plugins = ['pytest_userver.plugins.core']
Full sources
See the full example at: