├── .clang-format ├── .devcontainer ├── README.md └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── docker.yaml ├── .gitignore ├── .vscode ├── README.md └── c_cpp_properties.json ├── CMakeLists.txt ├── CMakePresets.json ├── Makefile ├── README.md ├── cmake ├── DownloadUserver.cmake └── get_cpm.cmake ├── configs ├── config_vars.docker.yaml ├── config_vars.testing.yaml ├── config_vars.yaml └── static_config.yaml ├── postgresql ├── data │ └── initial_data.sql └── schemas │ └── db_1.sql ├── proto └── handlers │ └── hello.proto ├── requirements.txt ├── run_as_user.sh ├── src ├── hello.cpp ├── hello.hpp ├── hello_benchmark.cpp ├── hello_client.cpp ├── hello_client.hpp ├── hello_test.cpp └── main.cpp └── tests ├── __init__.py ├── conftest.py ├── pytest.ini └── test_hello.py /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: google 2 | DerivePointerAlignment: false 3 | IncludeBlocks: Preserve 4 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | This is the minimal devcontainers configuration e.g. for use in VSCode or CLion. 2 | If you don't use devcontainers, feel free to delete this directory. 3 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ghcr.io/userver-framework/ubuntu-22.04-userver-pg-dev", 3 | "remoteUser": "user", 4 | "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/service_template,type=bind", 5 | "workspaceFolder": "/home/user/service_template", 6 | "runArgs": [ 7 | "--cap-add=SYS_PTRACE", 8 | "--security-opt", 9 | "seccomp=unconfined" 10 | ], 11 | "forwardPorts": [ 12 | 8080 13 | ], 14 | "containerEnv": { 15 | "SHELL": "/bin/bash", 16 | "PREFIX": "${PREFIX:-~/.local}", 17 | "CCACHE_DIR": "/home/user/service_template/.ccache", 18 | "CORES_DIR": "/cores" 19 | }, 20 | "customizations": { 21 | "vscode": { 22 | "extensions": [ 23 | "llvm-vs-code-extensions.vscode-clangd", 24 | "ms-vscode.cmake-tools", 25 | "ms-vscode.makefile-tools", 26 | "vadimcn.vscode-lldb", 27 | "ms-azuretools.vscode-docker" 28 | ], 29 | "settings": { 30 | "cmake.automaticReconfigure": false, 31 | "cmake.configureOnEdit": false, 32 | "cmake.configureOnOpen": false, 33 | "cmake.copyCompileCommands": "${workspaceFolder}/.vscode/compile_commands.json", 34 | "clangd.path": "/usr/bin/clangd", 35 | "clangd.arguments": [ 36 | "--background-index", 37 | "--compile-commands-dir=.vscode", 38 | "--clang-tidy", 39 | "--completion-style=detailed", 40 | "--header-insertion=never" 41 | ] 42 | } 43 | } 44 | }, 45 | "onCreateCommand": "sudo git config --global --add safe.directory /home/user/service_template", 46 | "mounts": [] 47 | } 48 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | 'on': 4 | schedule: 5 | - cron: '30 5 * * 1' # Every Monday at 5:30 6 | pull_request: 7 | push: 8 | branches: 9 | - develop 10 | 11 | env: 12 | UBSAN_OPTIONS: print_stacktrace=1 13 | 14 | jobs: 15 | posix: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - os: ubuntu-22.04 21 | make: test-debug 22 | info: g++-11 + test-debug 23 | 24 | - os: ubuntu-22.04 25 | make: test-release 26 | info: g++-11 + test-release 27 | 28 | name: '${{matrix.os}}: ${{matrix.info}}' 29 | runs-on: ${{matrix.os}} 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | submodules: true 35 | 36 | - name: Reuse ccache directory 37 | uses: actions/cache@v4 38 | with: 39 | path: ~/.cache/ccache 40 | key: '${{matrix.os}} ${{matrix.info}} ccache-dir ${{github.ref}} run-${{github.run_number}}' 41 | restore-keys: | 42 | ${{matrix.os}} ${{matrix.info}} ccache-dir ${{github.ref}} run-' 43 | ${{matrix.os}} ${{matrix.info}} ccache- 44 | 45 | - name: Install packages 46 | run: | 47 | DEPS_FILE="https://raw.githubusercontent.com/userver-framework/userver/refs/heads/develop/scripts/docs/en/deps/${{matrix.os}}.md" 48 | sudo apt update 49 | sudo apt install --allow-downgrades -y postgresql $(wget -q -O - ${DEPS_FILE}) 50 | python3 -m pip install -r requirements.txt 51 | 52 | - name: Reinstall postgres 14 53 | run: | 54 | sudo apt purge libpq5 libpq-dev postgresql-* 55 | sudo apt install -y postgresql-14 postgresql-client-14 postgresql-server-dev-14 56 | 57 | - name: Setup ccache 58 | run: | 59 | ccache -M 2.0GB 60 | ccache -s 61 | 62 | - name: Run ${{matrix.make}} 63 | run: | 64 | make ${{matrix.make}} 65 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Docker build 3 | 4 | 'on': 5 | schedule: 6 | - cron: '30 5 * * 1' # Every Monday at 5:30 7 | pull_request: 8 | push: 9 | branches: 10 | - master 11 | - develop 12 | - feature/** 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | submodules: true 21 | 22 | - name: Reuse ccache directory 23 | uses: actions/cache@v4 24 | with: 25 | path: .ccache 26 | key: 'ccache-dir-${{github.ref}}_run-${{github.run_number}}' 27 | restore-keys: | 28 | ccache-dir-${{github.ref}}_run- 29 | ccache- 30 | 31 | - name: Cmake 32 | run: make docker-cmake-release 33 | 34 | - name: Build 35 | run: make docker-build-release 36 | 37 | - name: Run tests 38 | run: make docker-test-release 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build*/ 3 | compile_commands.json 4 | .cache/ 5 | .ccache/ 6 | .idea/ 7 | .vscode/ 8 | !.vscode/c_cpp_properties.json 9 | !.vscode/cmake-variants.yaml 10 | !.vscode/README.md 11 | .cores/ 12 | .pgdata/ 13 | cmake-build-* 14 | Testing/ 15 | .DS_Store 16 | Makefile.local 17 | CMakeUserPresets.json 18 | -------------------------------------------------------------------------------- /.vscode/README.md: -------------------------------------------------------------------------------- 1 | This is the minimal configuration for VSCode IDE. 2 | If you don't use VSCode, feel free to delete this directory. 3 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "CMake", 5 | "compileCommands": "${config:cmake.buildDirectory}/compile_commands.json", 6 | "configurationProvider": "ms-vscode.cmake-tools" 7 | } 8 | ], 9 | "version": 4 10 | } 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12...3.31) 2 | project(service_template CXX) 3 | 4 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 5 | include(DownloadUserver) 6 | 7 | find_package(userver COMPONENTS core postgresql grpc QUIET) 8 | if(NOT userver_FOUND) 9 | # Tries TRY_DIR first, falls back to downloading userver from GitHub using CPM. 10 | download_userver(TRY_DIR third_party/userver VERSION 2.8) 11 | endif() 12 | 13 | userver_setup_environment() 14 | 15 | 16 | # Common sources 17 | add_library(${PROJECT_NAME}_objs OBJECT 18 | src/hello.hpp 19 | src/hello.cpp 20 | src/hello_client.hpp 21 | src/hello_client.cpp 22 | ) 23 | target_link_libraries(${PROJECT_NAME}_objs PUBLIC userver::postgresql userver::grpc) 24 | 25 | # Create a proto library with userver extensions 26 | userver_add_grpc_library(${PROJECT_NAME}_proto PROTOS handlers/hello.proto) 27 | target_link_libraries(${PROJECT_NAME}_objs PUBLIC ${PROJECT_NAME}_proto) 28 | 29 | 30 | # The Service 31 | add_executable(${PROJECT_NAME} src/main.cpp) 32 | target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_objs) 33 | 34 | 35 | # Unit Tests 36 | add_executable(${PROJECT_NAME}_unittest 37 | src/hello_test.cpp 38 | ) 39 | target_link_libraries(${PROJECT_NAME}_unittest PRIVATE ${PROJECT_NAME}_objs userver::utest) 40 | add_google_tests(${PROJECT_NAME}_unittest) 41 | 42 | 43 | # Benchmarks 44 | add_executable(${PROJECT_NAME}_benchmark 45 | src/hello_benchmark.cpp 46 | ) 47 | target_link_libraries(${PROJECT_NAME}_benchmark PRIVATE ${PROJECT_NAME}_objs userver::ubench) 48 | add_google_benchmark_tests(${PROJECT_NAME}_benchmark) 49 | 50 | 51 | # Functional Tests 52 | userver_testsuite_add_simple() 53 | 54 | 55 | # Install 56 | include(GNUInstallDirs) 57 | 58 | if(DEFINED ENV{PREFIX}) 59 | message(STATUS "Set install prefix: $ENV{PREFIX}") 60 | file(TO_CMAKE_PATH "$ENV{PREFIX}" PREFIX_PATH) 61 | set(CMAKE_INSTALL_PREFIX "${PREFIX_PATH}") 62 | endif() 63 | 64 | file(GLOB CONFIGS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/configs/*.yaml ${CMAKE_CURRENT_SOURCE_DIR}/configs/*.json) 65 | 66 | install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${PROJECT_NAME}) 67 | install(FILES ${CONFIGS_FILES} DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/${PROJECT_NAME} COMPONENT ${PROJECT_NAME}) 68 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 20, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "debug", 11 | "displayName": "Debug", 12 | "description": "Fully featured Debug build", 13 | "inherits": [ 14 | "common-flags" 15 | ], 16 | "binaryDir": "${sourceDir}/build-debug", 17 | "cacheVariables": { 18 | "CMAKE_BUILD_TYPE": "Debug", 19 | "USERVER_SANITIZE": "addr;ub" 20 | } 21 | }, 22 | { 23 | "name": "release", 24 | "displayName": "Release", 25 | "description": "Fully featured Release build", 26 | "inherits": [ 27 | "common-flags" 28 | ], 29 | "binaryDir": "${sourceDir}/build-release", 30 | "cacheVariables": { 31 | "CMAKE_BUILD_TYPE": "Release" 32 | } 33 | }, 34 | { 35 | "name": "common-flags", 36 | "hidden": true, 37 | "generator": "Ninja", 38 | "cacheVariables": { 39 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", 40 | "CMAKE_C_COMPILER": "cc", 41 | "CMAKE_CXX_COMPILER": "c++", 42 | "USERVER_FEATURE_POSTGRESQL": "ON", 43 | "USERVER_FEATURE_GRPC": "ON" 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = service_template 2 | NPROCS ?= $(shell nproc) 3 | CLANG_FORMAT ?= clang-format 4 | DOCKER_IMAGE ?= ghcr.io/userver-framework/ubuntu-24.04-userver:latest 5 | # If we're under TTY, pass "-it" to "docker run" 6 | DOCKER_ARGS = $(shell /bin/test -t 0 && /bin/echo -it || echo) 7 | PRESETS ?= debug release debug-custom release-custom 8 | 9 | .PHONY: all 10 | all: test-debug test-release 11 | 12 | # Run cmake 13 | .PHONY: $(addprefix cmake-, $(PRESETS)) 14 | $(addprefix cmake-, $(PRESETS)): cmake-%: 15 | cmake --preset $* 16 | 17 | $(addsuffix /CMakeCache.txt, $(addprefix build-, $(PRESETS))): build-%/CMakeCache.txt: 18 | $(MAKE) cmake-$* 19 | 20 | # Build using cmake 21 | .PHONY: $(addprefix build-, $(PRESETS)) 22 | $(addprefix build-, $(PRESETS)): build-%: build-%/CMakeCache.txt 23 | cmake --build build-$* -j $(NPROCS) --target $(PROJECT_NAME) 24 | 25 | # Test 26 | .PHONY: $(addprefix test-, $(PRESETS)) 27 | $(addprefix test-, $(PRESETS)): test-%: build-%/CMakeCache.txt 28 | cmake --build build-$* -j $(NPROCS) 29 | cd build-$* && ((test -t 1 && GTEST_COLOR=1 PYTEST_ADDOPTS="--color=yes" ctest -V) || ctest -V) 30 | pycodestyle tests 31 | 32 | # Start the service (via testsuite service runner) 33 | .PHONY: $(addprefix start-, $(PRESETS)) 34 | $(addprefix start-, $(PRESETS)): start-%: 35 | cmake --build build-$* -v --target start-$(PROJECT_NAME) 36 | 37 | .PHONY: service-start-debug service-start-release 38 | service-start-debug service-start-release: service-start-%: start-% 39 | 40 | # Cleanup data 41 | .PHONY: $(addprefix clean-, $(PRESETS)) 42 | $(addprefix clean-, $(PRESETS)): clean-%: 43 | cmake --build build-$* --target clean 44 | 45 | .PHONY: dist-clean 46 | dist-clean: 47 | rm -rf build* 48 | rm -rf tests/__pycache__/ 49 | rm -rf tests/.pytest_cache/ 50 | rm -rf .ccache 51 | rm -rf .vscode/.cache 52 | rm -rf .vscode/compile_commands.json 53 | 54 | # Install 55 | .PHONY: $(addprefix install-, $(PRESETS)) 56 | $(addprefix install-, $(PRESETS)): install-%: build-% 57 | cmake --install build-$* -v --component $(PROJECT_NAME) 58 | 59 | .PHONY: install 60 | install: install-release 61 | 62 | # Format the sources 63 | .PHONY: format 64 | format: 65 | find src -name '*pp' -type f | xargs $(CLANG_FORMAT) -i 66 | find tests -name '*.py' -type f | xargs autopep8 -i 67 | 68 | .PHONY: docker-start-service-debug docker-start-service-release 69 | docker-start-service-debug docker-start-service-release: docker-start-service-%: docker-start-% 70 | 71 | # Start targets makefile in docker wrapper. 72 | # The docker mounts the whole service's source directory, 73 | # so you can do some stuff as you wish, switch back to host (non-docker) system 74 | # and still able to access the results. 75 | .PHONY: $(addprefix docker-cmake-, $(PRESETS)) $(addprefix docker-build-, $(PRESETS)) $(addprefix docker-test-, $(PRESETS)) $(addprefix docker-clean-, $(PRESETS)) 76 | $(addprefix docker-cmake-, $(PRESETS)) $(addprefix docker-build-, $(PRESETS)) $(addprefix docker-test-, $(PRESETS)) $(addprefix docker-clean-, $(PRESETS)): docker-%: 77 | docker run $(DOCKER_ARGS) \ 78 | --network=host \ 79 | -v $$PWD:$$PWD \ 80 | -w $$PWD \ 81 | $(DOCKER_IMAGE) \ 82 | env CCACHE_DIR=$$PWD/.ccache \ 83 | HOME=$$HOME \ 84 | $$PWD/run_as_user.sh $(shell /bin/id -u) $(shell /bin/id -g) make $* 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This repository is for userver v2.8 or older versions. For newer versions of userver please use 2 | [userver-create-service](https://userver.tech/de/dab/md_en_2userver_2build_2build.html#autotoc_md177) script.** 3 | 4 | # pg_grpc_service_template 5 | 6 | Template of a C++ service that uses [userver framework](https://github.com/userver-framework/userver) with PostgreSQL and gRPC. 7 | 8 | 9 | ## Download and Build 10 | 11 | To create your own userver-based service follow the following steps: 12 | 13 | 1. Press the "Use this template button" at the top right of this GitHub page 14 | 2. Clone the service `git clone your-service-repo && cd your-service-repo` 15 | 3. Give a proper name to your service and replace all the occurrences of "service_template" string with that name 16 | (could be done via `find . -not -path "./third_party/*" -not -path ".git/*" -not -path './build-*' -type f | xargs sed -i 's/service_template/YOUR_SERVICE_NAME/g'`). 17 | 4. Feel free to tweak, adjust or fully rewrite the source code of your service. 18 | 19 | 20 | ## Makefile 21 | 22 | `PRESET` is either `debug`, `release`, or if you've added custom presets in `CMakeUserPresets.json`, it 23 | can also be `debug-custom`, `release-custom`. 24 | 25 | * `make cmake-PRESET` - run cmake configure, update cmake options and source file lists 26 | * `make build-PRESET` - build the service 27 | * `make test-PRESET` - build the service and run all tests 28 | * `make start-PRESET` - build the service, start it in testsuite environment and leave it running 29 | * `make install-PRESET` - build the service and install it in directory set in environment `PREFIX` 30 | * `make` or `make all` - build and run all tests in `debug` and `release` modes 31 | * `make format` - reformat all C++ and Python sources 32 | * `make dist-clean` - clean build files and cmake cache 33 | * `make docker-COMMAND` - run `make COMMAND` in docker environment 34 | * `make docker-clean-data` - stop docker containers and clean database data 35 | 36 | 37 | ## License 38 | 39 | The original template is distributed under the [Apache-2.0 License](https://github.com/userver-framework/userver/blob/develop/LICENSE) 40 | and [CLA](https://github.com/userver-framework/userver/blob/develop/CONTRIBUTING.md). Services based on the template may change 41 | the license and CLA. 42 | -------------------------------------------------------------------------------- /cmake/DownloadUserver.cmake: -------------------------------------------------------------------------------- 1 | include_guard(GLOBAL) 2 | 3 | function(download_userver) 4 | set(OPTIONS) 5 | set(ONE_VALUE_ARGS TRY_DIR VERSION GIT_TAG) 6 | set(MULTI_VALUE_ARGS) 7 | cmake_parse_arguments( 8 | ARG "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN} 9 | ) 10 | 11 | if(ARG_TRY_DIR) 12 | get_filename_component(ARG_TRY_DIR "${ARG_TRY_DIR}" REALPATH) 13 | if(EXISTS "${ARG_TRY_DIR}") 14 | message(STATUS "Using userver from ${ARG_TRY_DIR}") 15 | add_subdirectory("${ARG_TRY_DIR}" third_party/userver) 16 | return() 17 | endif() 18 | endif() 19 | 20 | # CMP0077 and CMP0126 are required for correct option forwarding. 21 | cmake_minimum_required(VERSION 3.21) 22 | include(get_cpm) 23 | 24 | if(NOT DEFINED ARG_VERSION AND NOT DEFINED ARG_GIT_TAG) 25 | set(ARG_GIT_TAG develop) 26 | endif() 27 | 28 | if(NOT DEFINED CPM_USE_NAMED_CACHE_DIRECTORIES) 29 | set(CPM_USE_NAMED_CACHE_DIRECTORIES ON) 30 | endif() 31 | 32 | CPMAddPackage( 33 | NAME userver 34 | GITHUB_REPOSITORY userver-framework/userver 35 | VERSION ${ARG_VERSION} 36 | GIT_TAG ${ARG_GIT_TAG} 37 | ${ARG_UNPARSED_ARGUMENTS} 38 | ) 39 | endfunction() 40 | -------------------------------------------------------------------------------- /cmake/get_cpm.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT 2 | # 3 | # SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors 4 | 5 | set(CPM_DOWNLOAD_VERSION 0.40.2) 6 | set(CPM_HASH_SUM "c8cdc32c03816538ce22781ed72964dc864b2a34a310d3b7104812a5ca2d835d") 7 | 8 | if(CPM_SOURCE_CACHE) 9 | set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 10 | elseif(DEFINED ENV{CPM_SOURCE_CACHE}) 11 | set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 12 | else() 13 | set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 14 | endif() 15 | 16 | # Expand relative path. This is important if the provided path contains a tilde (~) 17 | get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) 18 | 19 | file(DOWNLOAD 20 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake 21 | ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} 22 | ) 23 | 24 | include(${CPM_DOWNLOAD_LOCATION}) 25 | -------------------------------------------------------------------------------- /configs/config_vars.docker.yaml: -------------------------------------------------------------------------------- 1 | worker-threads: 4 2 | worker-fs-threads: 2 3 | worker-grpc-threads: 2 4 | logger-level: debug 5 | 6 | is-testing: false 7 | 8 | server-port: 8080 9 | server-grpc-port: 8081 10 | 11 | hello-endpoint: '[::1]:8081' 12 | -------------------------------------------------------------------------------- /configs/config_vars.testing.yaml: -------------------------------------------------------------------------------- 1 | worker-threads: 4 2 | worker-fs-threads: 2 3 | worker-grpc-threads: 2 4 | logger-level: debug 5 | 6 | is-testing: true 7 | 8 | server-port: 8080 9 | server-grpc-port: 8081 10 | 11 | hello-endpoint: '[::1]:8081' 12 | 13 | # service_template_db-1 is the service name + _ + filename of the db_1.sql 14 | dbconnection: 'postgresql://testsuite@localhost:15433/service_template_db_1' 15 | -------------------------------------------------------------------------------- /configs/config_vars.yaml: -------------------------------------------------------------------------------- 1 | worker-threads: 4 2 | worker-fs-threads: 2 3 | worker-grpc-threads: 2 4 | logger-level: info 5 | 6 | is-testing: false 7 | 8 | server-port: 8080 9 | server-grpc-port: 8081 10 | 11 | hello-endpoint: '[::1]:8081' 12 | 13 | dbconnection: 'postgresql://service_template_user:password@localhost:5432/service_template_db_1' 14 | -------------------------------------------------------------------------------- /configs/static_config.yaml: -------------------------------------------------------------------------------- 1 | components_manager: 2 | task_processors: # Task processor is an executor for coroutine tasks 3 | 4 | main-task-processor: # Make a task processor for CPU-bound coroutine tasks. 5 | worker_threads: $worker-threads # Process tasks in 4 threads. 6 | 7 | fs-task-processor: # Make a separate task processor for filesystem bound tasks. 8 | worker_threads: $worker-fs-threads 9 | 10 | grpc-blocking-task-processor: 11 | worker_threads: $worker-grpc-threads 12 | thread_name: grpc-worker 13 | 14 | default_task_processor: main-task-processor 15 | 16 | components: # Configuring components that were registered via component_list 17 | # Settings common to all gRPC client factories 18 | grpc-client-common: 19 | # The TaskProcessor for blocking connection initiation 20 | blocking-task-processor: grpc-blocking-task-processor 21 | 22 | grpc-client-factory: 23 | # Optional channel parameters for gRPC Core 24 | # https://grpc.github.io/grpc/core/group__grpc__arg__keys.html 25 | channel-args: {} 26 | 27 | grpc-client-logging: 28 | grpc-client-deadline-propagation: 29 | 30 | handler-hello: 31 | 32 | hello-client: 33 | endpoint: $hello-endpoint 34 | 35 | grpc-server: 36 | port: 8081 37 | service-defaults: 38 | task-processor: main-task-processor 39 | 40 | grpc-client-middlewares-pipeline: 41 | grpc-server-middlewares-pipeline: 42 | 43 | congestion-control: 44 | grpc-server-logging: 45 | grpc-server-deadline-propagation: 46 | grpc-server-congestion-control: 47 | 48 | server: 49 | listener: # configuring the main listening socket... 50 | port: $server-port # ...to listen on this port and... 51 | task_processor: main-task-processor # ...process incoming requests on this task processor. 52 | logging: 53 | fs-task-processor: fs-task-processor 54 | loggers: 55 | default: 56 | file_path: '@stderr' 57 | level: $logger-level 58 | overflow_behavior: discard # Drop logs if the system is too busy to write them down. 59 | 60 | # Dynamic config options. Cache is disabled, updates are disabled. 61 | dynamic-config: 62 | # For most of userver dynamic configs, defaults are used, some are overridden here. 63 | # See userver "dynamic config" docs for what configs exist. 64 | defaults: 65 | HTTP_CLIENT_CONNECTION_POOL_SIZE: 1000 66 | POSTGRES_DEFAULT_COMMAND_CONTROL: 67 | network_timeout_ms: 750 68 | statement_timeout_ms: 500 69 | 70 | testsuite-support: {} 71 | 72 | http-client: 73 | load-enabled: $is-testing 74 | fs-task-processor: fs-task-processor 75 | 76 | tests-control: 77 | load-enabled: $is-testing 78 | path: /tests/{action} 79 | method: POST 80 | task_processor: main-task-processor 81 | 82 | handler-ping: 83 | path: /ping 84 | method: GET 85 | task_processor: main-task-processor 86 | throttling_enabled: false 87 | url_trailing_slash: strict-match 88 | 89 | postgres-db-1: 90 | dbconnection: $dbconnection 91 | dbconnection#env: DB_CONNECTION 92 | blocking_task_processor: fs-task-processor 93 | dns_resolver: async 94 | sync-start: true 95 | connlimit_mode: manual 96 | 97 | dns-client: 98 | fs-task-processor: fs-task-processor 99 | -------------------------------------------------------------------------------- /postgresql/data/initial_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO hello_schema.users(name, count) 2 | VALUES ('user-from-initial_data.sql', 42) 3 | ON CONFLICT (name) 4 | DO NOTHING; 5 | -------------------------------------------------------------------------------- /postgresql/schemas/db_1.sql: -------------------------------------------------------------------------------- 1 | DROP SCHEMA IF EXISTS hello_schema CASCADE; 2 | 3 | CREATE SCHEMA IF NOT EXISTS hello_schema; 4 | 5 | CREATE TABLE IF NOT EXISTS hello_schema.users ( 6 | name TEXT PRIMARY KEY, 7 | count INTEGER DEFAULT(1) 8 | ); 9 | -------------------------------------------------------------------------------- /proto/handlers/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package handlers.api; 4 | 5 | service HelloService { 6 | rpc SayHello(HelloRequest) returns(HelloResponse) {} 7 | } 8 | 9 | message HelloRequest { 10 | string name = 1; 11 | } 12 | 13 | message HelloResponse { 14 | string text = 1; 15 | } 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycodestyle 2 | -------------------------------------------------------------------------------- /run_as_user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Exit on any error and treat unset variables as errors 4 | set -euo 5 | 6 | OLD_UID=$1 7 | OLD_GID=$2 8 | shift; shift 9 | 10 | groupadd --gid $OLD_GID --non-unique user 11 | useradd --uid $OLD_UID --gid $OLD_GID --non-unique user 12 | 13 | sudo -E -u user "$@" 14 | -------------------------------------------------------------------------------- /src/hello.cpp: -------------------------------------------------------------------------------- 1 | #include "hello.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace service_template { 10 | 11 | Hello::SayHelloResult Hello::SayHello(CallContext& /*context*/, 12 | handlers::api::HelloRequest&& request) { 13 | auto name = request.name(); 14 | 15 | auto user_type = UserType::kFirstTime; 16 | if (!name.empty()) { 17 | auto result = pg_cluster_->Execute( 18 | userver::storages::postgres::ClusterHostType::kMaster, 19 | "INSERT INTO hello_schema.users(name, count) VALUES($1, 1) " 20 | "ON CONFLICT (name) " 21 | "DO UPDATE SET count = users.count + 1 " 22 | "RETURNING users.count", 23 | name); 24 | 25 | if (result.AsSingleRow() > 1) { 26 | user_type = UserType::kKnown; 27 | } 28 | } 29 | if (name.substr(0, 5) == "mock_") { 30 | name = client_.SayHello(name.substr(5)); 31 | } 32 | handlers::api::HelloResponse response; 33 | response.set_text(service_template::SayHelloTo(name, user_type)); 34 | return response; 35 | } 36 | 37 | std::string SayHelloTo(std::string_view name, UserType type) { 38 | if (name.empty()) { 39 | name = "unknown user"; 40 | } 41 | 42 | switch (type) { 43 | case UserType::kFirstTime: 44 | return fmt::format("Hello, {}!\n", name); 45 | case UserType::kKnown: 46 | return fmt::format("Hi again, {}!\n", name); 47 | } 48 | 49 | UINVARIANT(false, "Invalid user type"); 50 | } 51 | 52 | void AppendHello(userver::components::ComponentList& component_list) { 53 | component_list.Append(); 54 | component_list.Append("postgres-db-1"); 55 | component_list.Append(); 56 | } 57 | 58 | } // namespace service_template 59 | -------------------------------------------------------------------------------- /src/hello.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "hello_client.hpp" 11 | 12 | namespace service_template { 13 | 14 | enum class UserType { kFirstTime, kKnown }; 15 | std::string SayHelloTo(std::string_view name, UserType type); 16 | 17 | class Hello final : public handlers::api::HelloServiceBase::Component { 18 | public: 19 | static constexpr std::string_view kName = "handler-hello"; 20 | 21 | Hello(const userver::components::ComponentConfig& config, 22 | const userver::components::ComponentContext& component_context) 23 | : handlers::api::HelloServiceBase::Component(config, component_context), 24 | pg_cluster_( 25 | component_context 26 | .FindComponent("postgres-db-1") 27 | .GetCluster()), 28 | client_(component_context.FindComponent()) {} 29 | 30 | SayHelloResult SayHello(CallContext& context, 31 | handlers::api::HelloRequest&& request); 32 | 33 | private: 34 | userver::storages::postgres::ClusterPtr pg_cluster_; 35 | HelloClient& client_; 36 | }; 37 | 38 | void AppendHello(userver::components::ComponentList& component_list); 39 | 40 | } // namespace service_template 41 | -------------------------------------------------------------------------------- /src/hello_benchmark.cpp: -------------------------------------------------------------------------------- 1 | #include "hello.hpp" 2 | 3 | #include // for std::uint64_t 4 | #include // for std::size 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | void HelloBenchmark(benchmark::State& state) { 11 | userver::engine::RunStandalone([&] { 12 | constexpr std::string_view kNames[] = {"userver", "is", "awesome", "!"}; 13 | std::uint64_t i = 0; 14 | 15 | for (auto _ : state) { 16 | const auto name = kNames[i++ % std::size(kNames)]; 17 | auto result = service_template::SayHelloTo( 18 | name, service_template::UserType::kFirstTime); 19 | benchmark::DoNotOptimize(result); 20 | } 21 | }); 22 | } 23 | 24 | BENCHMARK(HelloBenchmark); 25 | -------------------------------------------------------------------------------- /src/hello_client.cpp: -------------------------------------------------------------------------------- 1 | #include "hello_client.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace service_template { 8 | 9 | std::string HelloClient::SayHello(std::string name) { 10 | handlers::api::HelloRequest request; 11 | request.set_name(std::move(name)); 12 | 13 | // Perform RPC by sending the request and receiving the response. 14 | auto response = client_.SayHello(request); 15 | 16 | return std::move(*response.mutable_text()); 17 | } 18 | 19 | userver::yaml_config::Schema HelloClient::GetStaticConfigSchema() { 20 | return userver::yaml_config::MergeSchemas< 21 | userver::components::LoggableComponentBase>(R"( 22 | type: object 23 | description: > 24 | a user-defined wrapper around api::GreeterServiceClient that provides 25 | a simplified interface. 26 | additionalProperties: false 27 | properties: 28 | endpoint: 29 | type: string 30 | description: > 31 | Some other service endpoint (URI). 32 | )"); 33 | } 34 | 35 | void AppendHelloClient(userver::components::ComponentList& component_list) { 36 | component_list.Append(); 37 | } 38 | 39 | } // namespace service_template 40 | -------------------------------------------------------------------------------- /src/hello_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace service_template { 10 | 11 | class HelloClient final : public userver::components::LoggableComponentBase { 12 | public: 13 | static constexpr std::string_view kName = "hello-client"; 14 | 15 | HelloClient(const userver::components::ComponentConfig& config, 16 | const userver::components::ComponentContext& component_context) 17 | : userver::components::LoggableComponentBase(config, component_context), 18 | client_factory_( 19 | component_context 20 | .FindComponent() 21 | .GetFactory()), 22 | // The client needs a fixed endpoint 23 | client_(client_factory_.MakeClient( 24 | "hello-client", config["endpoint"].As())) {} 25 | 26 | std::string SayHello(std::string name); 27 | 28 | static userver::yaml_config::Schema GetStaticConfigSchema(); 29 | 30 | private: 31 | userver::ugrpc::client::ClientFactory& client_factory_; 32 | handlers::api::HelloServiceClient client_; 33 | }; 34 | 35 | void AppendHelloClient(userver::components::ComponentList& component_list); 36 | 37 | 38 | } // namespace service_template 39 | 40 | template <> 41 | inline constexpr bool userver::components::kHasValidate = true; 42 | -------------------------------------------------------------------------------- /src/hello_test.cpp: -------------------------------------------------------------------------------- 1 | #include "hello.hpp" 2 | 3 | #include 4 | 5 | UTEST(SayHelloTo, Basic) { 6 | using service_template::SayHelloTo; 7 | using service_template::UserType; 8 | 9 | EXPECT_EQ(SayHelloTo("Developer", UserType::kFirstTime), 10 | "Hello, Developer!\n"); 11 | EXPECT_EQ(SayHelloTo({}, UserType::kFirstTime), "Hello, unknown user!\n"); 12 | 13 | EXPECT_EQ(SayHelloTo("Developer", UserType::kKnown), 14 | "Hi again, Developer!\n"); 15 | } 16 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "hello.hpp" 13 | #include "hello_client.hpp" 14 | 15 | int main(int argc, char* argv[]) { 16 | auto component_list = 17 | userver::components::MinimalServerComponentList() 18 | .AppendComponentList(userver::ugrpc::server::MinimalComponentList()) 19 | .AppendComponentList(userver::ugrpc::client::MinimalComponentList()) 20 | .Append() 21 | .Append() 22 | .Append() 23 | .Append() 24 | .Append() 25 | .Append(); 26 | 27 | service_template::AppendHello(component_list); 28 | service_template::AppendHelloClient(component_list); 29 | 30 | return userver::utils::DaemonMain(argc, argv, component_list); 31 | } 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/userver-framework/pg_grpc_service_template/c4aee336c2ad9a3f198a3c5e1626b40c4a293683/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | 4 | import pytest 5 | import grpc 6 | 7 | from testsuite.databases.pgsql import discover 8 | 9 | import handlers.hello_pb2_grpc as hello_services # noqa: E402, E501 10 | 11 | USERVER_CONFIG_HOOKS = ['prepare_service_config'] 12 | pytest_plugins = [ 13 | 'pytest_userver.plugins.postgresql', 14 | 'pytest_userver.plugins.grpc', 15 | ] 16 | 17 | 18 | @pytest.fixture 19 | def grpc_service(pgsql, grpc_channel, service_client): 20 | return hello_services.HelloServiceStub(grpc_channel) 21 | 22 | 23 | @pytest.fixture(scope='session') 24 | def prepare_service_config(grpc_mockserver_endpoint): 25 | def patch_config(config, config_vars): 26 | components = config['components_manager']['components'] 27 | components['hello-client']['endpoint'] = grpc_mockserver_endpoint 28 | 29 | return patch_config 30 | 31 | 32 | def pytest_configure(config): 33 | sys.path.append(str( 34 | pathlib.Path(__file__).parent.parent / 'proto/handlers/')) 35 | 36 | 37 | @pytest.fixture(scope='session') 38 | def service_source_dir(): 39 | """Path to root directory service.""" 40 | return pathlib.Path(__file__).parent.parent 41 | 42 | 43 | @pytest.fixture(scope='session') 44 | def initial_data_path(service_source_dir): 45 | """Path for find files with data""" 46 | return [ 47 | service_source_dir / 'postgresql/data', 48 | ] 49 | 50 | 51 | @pytest.fixture(scope='session') 52 | def pgsql_local(service_source_dir, pgsql_local_create): 53 | """Create schemas databases for tests""" 54 | databases = discover.find_schemas( 55 | 'service_template', 56 | [service_source_dir.joinpath('postgresql/schemas')], 57 | ) 58 | return pgsql_local_create(list(databases.values())) 59 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode = auto 3 | log_level = debug 4 | mockserver-tracing-enabled = true 5 | -------------------------------------------------------------------------------- /tests/test_hello.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import handlers.hello_pb2 as hello_protos 4 | import handlers.hello_pb2_grpc as hello_grpc 5 | 6 | # Start the tests via `make test-debug` or `make test-release` 7 | 8 | 9 | async def test_grpc_client(grpc_mockserver_new, grpc_service): 10 | @grpc_mockserver_new(hello_grpc.HelloServiceServicer.SayHello) 11 | async def mock_say_hello(request, context): 12 | assert request.name 13 | return hello_protos.HelloResponse( 14 | text=f'{request.name}!!', 15 | ) 16 | 17 | request = hello_protos.HelloRequest(name='mock_userver') 18 | response = await grpc_service.SayHello(request) 19 | assert response.text == 'Hello, userver!!!\n' 20 | assert mock_say_hello.times_called == 1 21 | 22 | 23 | async def test_first_time_users(grpc_service): 24 | request = hello_protos.HelloRequest(name='userver') 25 | response = await grpc_service.SayHello(request) 26 | assert response.text == 'Hello, userver!\n' 27 | 28 | 29 | async def test_db_updates(grpc_service): 30 | request = hello_protos.HelloRequest(name='World') 31 | response = await grpc_service.SayHello(request) 32 | assert response.text == 'Hello, World!\n' 33 | 34 | request = hello_protos.HelloRequest(name='World') 35 | response = await grpc_service.SayHello(request) 36 | assert response.text == 'Hi again, World!\n' 37 | 38 | request = hello_protos.HelloRequest(name='World') 39 | response = await grpc_service.SayHello(request) 40 | assert response.text == 'Hi again, World!\n' 41 | 42 | 43 | @pytest.mark.pgsql('db_1', files=['initial_data.sql']) 44 | async def test_db_initial_data(grpc_service): 45 | request = hello_protos.HelloRequest(name='user-from-initial_data.sql') 46 | response = await grpc_service.SayHello(request) 47 | assert response.text == 'Hi again, user-from-initial_data.sql!\n' 48 | --------------------------------------------------------------------------------