From 540b8dc1039dabfc8f27b13ba281b880688ac000 Mon Sep 17 00:00:00 2001 From: David Chen <36295559+David0922@users.noreply.github.com> Date: Sat, 20 Dec 2025 03:39:49 +0800 Subject: [PATCH] - --- .clang-format | 1 + .gitignore | 3 + CMakeLists.txt | 16 ++++ CMakePresets.json | 13 +++ README.md | 95 ++++++++++++++++++ src/main.cc | 202 +++++++++++++++++++++++++++++++++++++++ vcpkg-configuration.json | 14 +++ vcpkg.json | 5 + 8 files changed, 349 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 src/main.cc create mode 100644 vcpkg-configuration.json create mode 100644 vcpkg.json diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f6cb8ad --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Google diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..335b6fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +CMakeUserPresets.json +vcpkg_installed diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..287ebda --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.16) + +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") + +project(redis_playground LANGUAGES CXX) + +find_package(hiredis CONFIG REQUIRED) + +add_library(proj_warnings INTERFACE) +target_compile_options(proj_warnings INTERFACE -Wall -Werror -Wextra -Wpedantic) + +add_executable(main ./src/main.cc) +target_link_libraries(main PRIVATE proj_warnings hiredis::hiredis) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..10f178b --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "vcpkg", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + ] +} diff --git a/README.md b/README.md index e69de29..bd76157 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,95 @@ +## setup dev env + +### install cmake & ninja + +``` +# linux +sudo apt install ninja-build + +CMAKE_VER=v4.2.1 +sudo apt install libssl-dev openssl +git clone --branch $CMAKE_VER --depth 1 https://github.com/Kitware/CMake.git +cd CMake +./bootstrap && make && sudo make install + +# mac +brew install cmake ninja +``` + +### install dependencies + +```bash +vcpkg --disable-metrics install --recurse +``` + +### update vcpkg baseline + +```bash +vcpkg --disable-metrics x-update-baseline +``` + +### show package versions + +```bash +vcpkg --disable-metrics list +``` + +### create `CMakeUserPresets.json` + +```json +{ + "version": 2, + "configurePresets": [ + { + "name": "default", + "inherits": "vcpkg", + "environment": { + "VCPKG_ROOT": "path/to/vcpkg" + } + } + ] +} +``` + +## format + +```bash +find . \( -path ./build -o -path ./vcpkg_installed \) -prune -o \ + -type f \( -name '*.cc' -o -name '*.h' \) -exec clang-format -i {} + +``` + +## build + +``` +cmake --preset=default +cmake --build build +``` + +## run + +spin up a local redis server + +``` +docker run --name redis -p 6379:6379 -it redis:8.4.0 +``` + +run the binary + +``` +./build/main +``` + +push messages with `redis-cli` + +``` +docker exec -it redis redis-cli -h localhost -p 6379 + +XADD key_01 * f1 v1 f2 v2 +``` + +## clean + +``` +rm -rf ./build +rm -rf $HOME/.cache/vcpkg +``` diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 0000000..4b24486 --- /dev/null +++ b/src/main.cc @@ -0,0 +1,202 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct FieldVal { + std::string field, val; +}; + +struct KeyID { + std::string key; + std::string id; +}; + +struct XaddReq { + std::string key; + std::vector field_val_list; +}; + +struct XreadReq { + int count; + int timeout_ms = 5000; + std::vector key_id_list; +}; + +struct StreamEntry { + std::string id; + std::vector field_val_list; +}; + +using Key = std::string; + +using XreadRes = std::unordered_map>; + +class RedisClient { + public: + RedisClient(const std::string& host = "127.0.0.1", int port = 6379) { + ctx_ = redisConnect(host.c_str(), port); + + if (!ctx_) { + throw std::runtime_error("failed to allocate redis context"); + } else if (ctx_->err) { + throw std::runtime_error(ctx_->errstr); + } + } + + RedisClient(const RedisClient&) = delete; + RedisClient& operator=(const RedisClient&) = delete; + + RedisClient(RedisClient&& other) noexcept : ctx_(other.ctx_) { + other.ctx_ = nullptr; + } + + RedisClient& operator=(RedisClient&& other) noexcept { + if (this != &other) { + if (ctx_) redisFree(ctx_); + ctx_ = other.ctx_; + other.ctx_ = nullptr; + } + return *this; + } + + ~RedisClient() { + if (ctx_) redisFree(ctx_); + } + + std::expected xadd(const XaddReq& req) noexcept { + // todo: safe guard against invalid requests + + std::stringstream ss; + ss << "XADD " << req.key << " * "; + for (const auto& [field, val] : req.field_val_list) + ss << field << ' ' << val << ' '; + + auto reply = submit_cmd(ss.str()); + + if (!reply) { + return std::unexpected(ctx_->errstr); + } else if (reply->type == REDIS_REPLY_ERROR) { + return std::unexpected(reply->str ? reply->str : "unknown error"); + } + + return {}; + } + + std::expected xread(const XreadReq& req) noexcept { + // todo: safe guard against invalid requests + + std::stringstream ss; + ss << "XREAD COUNT " << req.count << ' '; + ss << "BLOCK " << req.timeout_ms << ' '; + ss << "STREAMS "; + for (const auto& [key, _] : req.key_id_list) ss << key << ' '; + for (const auto& [_, id] : req.key_id_list) ss << id << ' '; + + auto reply = submit_cmd(ss.str()); + + if (!reply) { + return std::unexpected(ctx_->errstr); + } else if (reply->type == REDIS_REPLY_ERROR) { + return std::unexpected(reply->str ? reply->str : "unknown reply error"); + } else if (reply->type == REDIS_REPLY_NIL) { + return {}; + } else if (reply->type != REDIS_REPLY_ARRAY) { + return std::unexpected( + std::format("unexpected reply type {}", reply->type)); + } + + XreadRes res; + + // todo: verify res has the right format + for (int i = 0, n = reply->elements; i < n; ++i) { + auto key = reply->element[i]->element[0]->str; + + auto entry_list = reply->element[i]->element[1]->element; + int entry_list_len = reply->element[i]->element[1]->elements; + + for (int entry_i = 0; entry_i < entry_list_len; ++entry_i) { + auto id = entry_list[entry_i]->element[0]->str; + + auto raw_field_val = entry_list[entry_i]->element[1]->element; + int raw_field_val_len = entry_list[entry_i]->element[1]->elements; + + StreamEntry streamEntry{.id = id, .field_val_list{}}; + streamEntry.field_val_list.reserve(raw_field_val_len >> 1); + + for (int fv_i = 0; fv_i < raw_field_val_len; fv_i += 2) { + streamEntry.field_val_list.emplace_back( + FieldVal{.field = raw_field_val[fv_i]->str, + .val = raw_field_val[fv_i + 1]->str}); + } + + res[key].emplace_back(streamEntry); + } + } + + return res; + } + + private: + using Reply = std::unique_ptr; + + redisContext* ctx_{nullptr}; + + Reply submit_cmd(const std::string& cmd) noexcept { + redisReply* reply = + static_cast(redisCommand(ctx_, cmd.c_str())); + return Reply(reply, freeReplyObject); + } +}; + +std::ostream& operator<<(std::ostream& os, const XreadRes& res) { + for (const auto& [key, stream_entry_list] : res) { + os << key << '\n'; + for (const auto& [id, field_val_list] : stream_entry_list) { + os << " " << id << '\n'; + for (const auto& [field, val] : field_val_list) { + os << " " << field << ' ' << val << '\n'; + } + } + } + return os; +} + +int main() { + std::jthread redis_thread([](std::stop_token stop_token) { + RedisClient redis("127.0.0.1", 6379); + + std::string key{"key_01"}; + std::string id{"0"}; + + while (!stop_token.stop_requested()) { + auto res = redis.xread(XreadReq{.count = 2, .key_id_list = {{key, id}}}); + + if (!res.has_value()) { + std::cout << "err: " << res.error() << std::endl; + break; + } else if (res.value().empty()) { + break; + } + + std::cout << res.value() << std::endl << "---" << std::endl; + + if (!res.value().contains(key)) break; + + id = res.value()[key].back().id; + } + }); + + redis_thread.join(); + + return 0; +} diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 0000000..2cede91 --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,14 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "d2452281016c6d77b6ef199d599d9929eafe4807", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ] +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..a6dd89a --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,5 @@ +{ + "dependencies": [ + "hiredis" + ] +}