Skip to content

Commit 6514759

Browse files
committed
Initial commit, C++ client, CLI (CMake, CI, sanitizers)
0 parents  commit 6514759

24 files changed

+952
-0
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* text=auto eol=lf
2+
*.sh text eol=lf

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build-test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
compiler: [gcc, clang]
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Install deps
19+
run: |
20+
sudo apt-get update
21+
sudo apt-get install -y cmake g++ clang libcurl4-openssl-dev
22+
23+
- name: Configure
24+
run: |
25+
if [ "${{ matrix.compiler }}" = "gcc" ]; then
26+
export CXX=g++
27+
else
28+
export CXX=clang++
29+
fi
30+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON -DENABLE_UBSAN=ON
31+
32+
- name: Build
33+
run: cmake --build build -j
34+
35+
- name: Test
36+
run: ctest --test-dir build --output-on-failure

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
build/
2+
.vscode/
3+
.idea/
4+
.DS_Store

CMakeLists.txt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
project(mini_dynamo_cpp LANGUAGES CXX)
3+
4+
set(CMAKE_CXX_STANDARD 17)
5+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
6+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
7+
8+
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
9+
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
10+
11+
include(cmake/Sanitizers.cmake)
12+
13+
add_library(dynamo_client
14+
src/client.cpp
15+
src/config.cpp
16+
src/hash.cpp
17+
src/http.cpp
18+
src/ring.cpp
19+
)
20+
21+
target_include_directories(dynamo_client PUBLIC
22+
${CMAKE_CURRENT_SOURCE_DIR}/include
23+
)
24+
25+
# Warnings (keep reasonable)
26+
if (MSVC)
27+
target_compile_options(dynamo_client PRIVATE /W4)
28+
else()
29+
target_compile_options(dynamo_client PRIVATE -Wall -Wextra -Wpedantic)
30+
endif()
31+
32+
# libcurl for HTTP
33+
find_package(CURL REQUIRED)
34+
target_link_libraries(dynamo_client PUBLIC CURL::libcurl)
35+
36+
enable_sanitizers(dynamo_client)
37+
38+
add_executable(dynamoctl tools/dynamoctl.cpp)
39+
target_link_libraries(dynamoctl PRIVATE dynamo_client)
40+
enable_sanitizers(dynamoctl)
41+
42+
# ---- tests ----
43+
include(CTest)
44+
if (BUILD_TESTING)
45+
add_executable(unit_tests
46+
tests/test_main.cpp
47+
tests/test_hash.cpp
48+
tests/test_ring.cpp
49+
tests/test_config.cpp
50+
)
51+
target_link_libraries(unit_tests PRIVATE dynamo_client)
52+
enable_sanitizers(unit_tests)
53+
add_test(NAME unit_tests COMMAND unit_tests)
54+
endif()

Jenkinsfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
pipeline {
2+
agent any
3+
stages {
4+
stage('Checkout') {
5+
steps { checkout scm }
6+
}
7+
stage('Configure') {
8+
steps {
9+
sh '''
10+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON -DENABLE_UBSAN=ON
11+
'''
12+
}
13+
}
14+
stage('Build') {
15+
steps {
16+
sh 'cmake --build build -j'
17+
}
18+
}
19+
stage('Test') {
20+
steps {
21+
sh 'ctest --test-dir build --output-on-failure'
22+
}
23+
}
24+
}
25+
}

README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# mini-dynamo-cpp
2+
3+
A small C++17 client library + CLI (`dynamoctl`) for the Mini Dynamo Go cluster.
4+
5+
This repo is designed to hit the job-posting requirements:
6+
- C++ project using **CMake**
7+
- Builds on Linux with **clang/gcc**
8+
- Runs tests under **sanitizers** in CI (ASan/UBSan)
9+
- Shows “production discipline”: retries, timeouts, tests, CI
10+
11+
## What it does
12+
- `libdynamo_client`: C++ client that can `PUT` and `GET` keys from a Mini Dynamo cluster
13+
- `dynamoctl`: CLI for quick interaction (put/get/health)
14+
15+
The client implements simple routing using a consistent-hash ring (primary node first), and falls back to other nodes if the primary fails.
16+
17+
## Prereqs
18+
- CMake >= 3.20
19+
- A C++17 compiler (clang or gcc)
20+
- libcurl development headers
21+
22+
On Ubuntu:
23+
```bash
24+
sudo apt-get update
25+
sudo apt-get install -y cmake g++ clang libcurl4-openssl-dev
26+
```
27+
28+
## Build + test (Linux/macOS)
29+
```bash
30+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON -DENABLE_UBSAN=ON
31+
cmake --build build -j
32+
ctest --test-dir build --output-on-failure
33+
```
34+
35+
## Run against your Mini Dynamo cluster
36+
Start your Go cluster (from your Go repo):
37+
```bash
38+
docker compose up -d --build
39+
```
40+
41+
Then from this repo:
42+
```bash
43+
./build/dynamoctl health
44+
./build/dynamoctl put hello world
45+
./build/dynamoctl get hello
46+
```
47+
48+
## Config
49+
By default, `dynamoctl` looks for `nodes.docker.json` in the current directory.
50+
If it doesn't exist, it falls back to `localhost:9001-9003`.
51+
52+
You can pass a config:
53+
```bash
54+
./build/dynamoctl --config nodes.local.json put k v
55+
```
56+
57+
Supported formats:
58+
59+
### Minimal JSON
60+
```json
61+
{ "vnodes": 128, "nodes": [
62+
{ "id": "n1", "host": "localhost", "port": 9001 },
63+
{ "id": "n2", "host": "localhost", "port": 9002 },
64+
{ "id": "n3", "host": "localhost", "port": 9003 }
65+
]}
66+
```
67+
68+
### Minimal text
69+
```txt
70+
vnodes=128
71+
n1 localhost 9001
72+
n2 localhost 9002
73+
n3 localhost 9003
74+
```
75+
76+
> Note: This project intentionally uses a tiny JSON parser (regex-based) for a narrow schema to keep the repo self-contained.
77+
78+
## What to put on your resume
79+
- “Built a C++17 client SDK + CLI for a Dynamo-style KV store; implemented consistent-hash routing, timeouts, and retries.”
80+
- “Configured CMake builds for clang/gcc; added ASan/UBSan builds in CI to catch memory and UB issues early.”
81+
- “Wrote unit tests and automated them in CI (GitHub Actions / Jenkinsfile).”

cmake/Sanitizers.cmake

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
function(enable_sanitizers target_name)
2+
if (MSVC)
3+
# Sanitizers via MSVC/clang-cl are different; keep simple for now.
4+
return()
5+
endif()
6+
7+
if (ENABLE_ASAN)
8+
target_compile_options(${target_name} PRIVATE -fsanitize=address -fno-omit-frame-pointer)
9+
target_link_options(${target_name} PRIVATE -fsanitize=address)
10+
endif()
11+
12+
if (ENABLE_UBSAN)
13+
target_compile_options(${target_name} PRIVATE -fsanitize=undefined -fno-omit-frame-pointer)
14+
target_link_options(${target_name} PRIVATE -fsanitize=undefined)
15+
endif()
16+
endfunction()

include/dynamo/client.hpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
#include <cstdint>
3+
#include <optional>
4+
#include <string>
5+
#include <vector>
6+
7+
#include "dynamo/config.hpp"
8+
#include "dynamo/http.hpp"
9+
#include "dynamo/ring.hpp"
10+
11+
namespace dynamo {
12+
13+
struct RetryPolicy {
14+
int max_retries{2}; // number of retries (not counting first attempt)
15+
uint32_t base_delay_ms{80}; // exponential backoff base
16+
};
17+
18+
class DynamoClient {
19+
public:
20+
DynamoClient(ClusterConfig cfg,
21+
HttpClient http = HttpClient(),
22+
RetryPolicy retry = RetryPolicy());
23+
24+
// PUT /kv/<key> body=<value>
25+
// Returns true if any node returns 2xx.
26+
bool put(const std::string& key, const std::string& value);
27+
28+
// GET /kv/<key>
29+
// Returns value if any node returns 200.
30+
std::optional<std::string> get(const std::string& key);
31+
32+
// GET /health (best-effort)
33+
// Returns per-node status lines.
34+
std::vector<std::string> health_all() const;
35+
36+
private:
37+
HttpResponse request_with_retry(const std::string& url,
38+
const std::string& method,
39+
const std::string& body = "") const;
40+
41+
ClusterConfig cfg_;
42+
HttpClient http_;
43+
RetryPolicy retry_;
44+
ConsistentHashRing ring_;
45+
};
46+
47+
} // namespace dynamo

include/dynamo/config.hpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#pragma once
2+
#include <cstdint>
3+
#include <string>
4+
#include <vector>
5+
6+
namespace dynamo {
7+
8+
struct Node {
9+
std::string id;
10+
std::string host;
11+
uint16_t port{0};
12+
13+
std::string base_url() const; // e.g., http://localhost:9001
14+
};
15+
16+
struct ClusterConfig {
17+
std::vector<Node> nodes;
18+
int vnodes{128}; // virtual nodes per physical node
19+
20+
// Loads a minimal subset of JSON or a simple text config.
21+
// JSON supported shapes:
22+
// { "vnodes": 128, "nodes": [ {"id":"n1","host":"localhost","port":9001}, ... ] }
23+
// { "nodes": [ {"id":"n1","address":"localhost:9001"}, ... ] }
24+
//
25+
// Text supported shape:
26+
// vnodes=128
27+
// n1 localhost 9001
28+
// n2 localhost 9002
29+
// n3 localhost 9003
30+
static ClusterConfig load_from_file(const std::string& path);
31+
32+
// Convenience default for local dev if no config file exists.
33+
static ClusterConfig local_default();
34+
};
35+
36+
} // namespace dynamo

include/dynamo/hash.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#pragma once
2+
#include <cstdint>
3+
#include <string_view>
4+
5+
namespace dynamo {
6+
7+
// Stable 64-bit FNV-1a hash (deterministic across platforms/compilers).
8+
uint64_t fnv1a_64(std::string_view s);
9+
10+
} // namespace dynamo

0 commit comments

Comments
 (0)