DEV Community

Кирилл Жуков
Кирилл Жуков

Posted on

Asynchronous backend on modern C++ 23.

First of all I need to tell that this article is about our C++ 23 stack currently used in production and produced good performance. Mostly all our libraries based on top of three basic libraries:

  • uvent – asynchronous cross-platform engine which provides coroutines wrappers (like in boost.asio) and cross-platform I/O for all of our libraries.
  • unet – asynchronous web-server based on uvent which allows to use coroutines as handlers for endpoints (a good advantage, to be explained later).
  • ureflect – compile time reflection (without need to use a preprocessor).

In this article we'll also use some other our libraries:

  • upq – asynchronous PostgreSQL client library build on top of libpq, uvent, ureflect. Provides ability to query data from data base with or without reflection (JSON/JSONB also can be parsed into datastructures via reflection).
  • uredis – asynchronous redis library build on top of uvent with own RESP3 implementation.
  • ulog – logger inspired by spdlog build on top of uvent. Instead of using own thread spawns flushing coroutine.
  • ujson – simple json library with reflection.

This article isn't a guide how to get into coroutines however it shows what can be done by using them in correct way.

Configuring databases

PostgreSQL

Before we start using libraries we need to startup PostgreSQL instance. Here is the docker-compose.yaml file:

version: "3.9"

services:
  postgres:
    image: postgres:16
    container_name: local-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: devdb
    ports:
      - "5432:5432"
    volumes:
      - pgdata_local:/var/lib/postgresql/data

volumes:
  pgdata_local:
Enter fullscreen mode Exit fullscreen mode

KeyDB

We need also to add KeyDB as cache. Let's change docker-compose.yaml to:

version: "3.9"

services:
  postgres:
    image: postgres:16
    container_name: local-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: devdb
    ports:
      - "5432:5432"
    volumes:
      - pgdata_local:/var/lib/postgresql/data

  keydb:
    image: eqalpha/keydb:latest
    container_name: local-keydb
    restart: unless-stopped
    command: ["keydb-server", "/etc/keydb/keydb.conf"]
    ports:
      - "6379:6379"
    volumes:
      - keydbdata_local:/data
      - ./keydb.conf:/etc/keydb/keydb.conf:ro

volumes:
  pgdata_local:
  keydbdata_local:
Enter fullscreen mode Exit fullscreen mode

keydb.conf:

bind 0.0.0.0
port 6379

protected-mode yes
requirepass devpass

appendonly yes
dir /data
Enter fullscreen mode Exit fullscreen mode

CMake

After setting up our databases we're able to begin configuring our project. Before we start writing code let's setup CMakeLists.txt file correctly:

cmake_minimum_required(VERSION 3.27)
project(article)

set(CMAKE_CXX_STANDARD 23)
set(UREDIS_BUILD_EXAMPLES OFF)
set(UREDIS_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(UREDIS_BUILD_STATIC ON  CACHE BOOL "" FORCE)
set(UREDIS_LOGS OFF CACHE BOOL "" FORCE)
set(UPQ_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)

find_package(OpenSSL REQUIRED)
find_package(ZLIB REQUIRED)

include(FetchContent)
include_directories(${article_SOURCE_DIR})

FetchContent_Declare(
        uvent
        GIT_REPOSITORY https://github.com/Usub-development/uvent.git
        GIT_TAG main
        OVERRIDE_FIND_PACKAGE
)
FetchContent_Declare(
        unet
        GIT_REPOSITORY https://github.com/Usub-development/unet.git
        GIT_TAG main
        OVERRIDE_FIND_PACKAGE
)
# Loading ujson from upq 
FetchContent_Declare(
        upq
        GIT_REPOSITORY https://github.com/Usub-development/upq.git
        GIT_TAG main
        OVERRIDE_FIND_PACKAGE
)
FetchContent_Declare(
        ulog
        GIT_REPOSITORY https://github.com/Usub-development/ulog.git
        GIT_TAG main
        OVERRIDE_FIND_PACKAGE
)
FetchContent_Declare(
        uredis
        GIT_REPOSITORY https://github.com/Usub-development/uredis.git
        GIT_TAG main
        FIND_PACKAGE_ARGS
)


FetchContent_MakeAvailable(uvent unet ulog upq uredis)

add_executable(${PROJECT_NAME}
        src/main.cpp
)

target_include_directories(${PROJECT_NAME} PRIVATE
        /usr/local/include
        ${CMAKE_CURRENT_LIST_DIR}/include
)

target_link_libraries(${PROJECT_NAME} PRIVATE
        -lpq
        OpenSSL::Crypto
        usub::uvent
        usub::server
        ZLIB::ZLIB
        usub::upq
        usub::ulog
        usub::uredis
)
Enter fullscreen mode Exit fullscreen mode

Our project structure should look like:

.
├── CMakeLists.txt
├── docker-compose.yaml
├── include
├── keydb.conf
└── src
    └── main.cpp
Enter fullscreen mode Exit fullscreen mode

main.cpp

Now we're able to start configuring our main.cpp.

Add necessary includes:

#include <server/server.h>
#include <upq/PgRouting.h>
#include <upq/PgRoutingBuilder.h>
#include <ulog/ulog.h>
#include <uredis/RedisClusterClient.h>
Enter fullscreen mode Exit fullscreen mode

Don't worry about RedisClusterClient, it'll fallback to basic client if KeyDB (same as Redis) not in cluster mode.

Next configure ulog:

int main() {
    usub::ulog::ULogInit cfg{
        .trace_path = nullptr, // nullptr means stdout
        .debug_path = nullptr, // -//-
        .info_path = nullptr, // -//-
        .warn_path = nullptr, // -//-
        .error_path = nullptr, // -//-
        .critical_path = nullptr, // -//-
        .fatal_path = nullptr, // -//-
        .flush_interval_ns = 5'000'000'000ULL, // 5 seconds
        .queue_capacity = 1024,
        .batch_size = 512,
        .enable_color_stdout = true,
        .json_mode = false,
        .track_metrics = true
    };

    usub::ulog::init(cfg);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Let's clarify each parameter:

  • trace_path, debug_path etc. – log paths. nullptr means logs will be printed to stdout
  • flush_interval_ns – how often flushing coroutine will be woken up and flush queues.
  • queue_capacity – capacity of lock-free queue which is used as log storage by default. If capacity exceeded it'll fallback to queue with mutex. You can track fallback metrics like that:
if (auto* lg = usub::ulog::Logger::try_instance())
{
    auto overflows = lg->get_overflow_events();
    ulog::info("logger overflows (mpmc full -> mutex fallback) = {}", overflows);
}
Enter fullscreen mode Exit fullscreen mode
  • batch_size – how many elements will be dequeued from storage (both lock-free and non-lock free queue) at once.
  • enable_color_stdout – responsible for colorful logs.
  • json_mode – ulog is able to flush logs as json, so they'll look like:
{"time":"2025-10-28 12:03:44.861","thread":3,"level":"I","msg":"starting event loop..."}
Enter fullscreen mode Exit fullscreen mode
  • track_metrics – should ulog track fallback metrics or not.

usub::ulog::init(cfg); initializes the global logger. Call it before any logging; otherwise the program may crash (e.g., segfault).

Top comments (0)