Skip to content
Closed
9 changes: 3 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.15)
cmake_minimum_required(VERSION 3.20)

project(KV_Database LANGUAGES CXX)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# C++17 Configuration
set(CMAKE_CXX_STANDARD 17)
# C++23 Configuration
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Turn our core logic into a LIBRARY so both the App and the Tests can use it
Expand All @@ -17,10 +17,7 @@ add_library(KV_Core
src/Worker/SnapshotScheduler.cpp
src/Limit/Limiter.cpp
src/UserSession/UserSession.cpp
src/UserSession/UserSession.h
src/UserSession/Connection.h
src/Worker/UserSessionBackgroundWorker.cpp
src/Worker/UserSessionBackgroundWorker.h
src/Networking/EpollEventLoop.cpp
src/Networking/IocpEventLoop.cpp
)
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

_Redis-inspired · event-driven · cross-platform · containerized_

[![C++](https://img.shields.io/badge/C%2B%2B-17-00599C?style=flat-square&logo=cplusplus&logoColor=white)](https://en.cppreference.com/w/cpp/17)
[![CMake](https://img.shields.io/badge/CMake-3.15%2B-064F8C?style=flat-square&logo=cmake&logoColor=white)](https://cmake.org/)
[![C++](https://img.shields.io/badge/C%2B%2B-23-00599C?style=flat-square&logo=cplusplus&logoColor=white)](https://en.cppreference.com/w/cpp/23)
[![CMake](https://img.shields.io/badge/CMake-3.20%2B-064F8C?style=flat-square&logo=cmake&logoColor=white)](https://cmake.org/)
[![Docker](https://img.shields.io/badge/Docker-ready-2496ED?style=flat-square&logo=docker&logoColor=white)](https://www.docker.com/)
[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20Windows-informational?style=flat-square&logo=linux&logoColor=white)](#)
[![Tests](https://img.shields.io/badge/tests-GoogleTest-4285F4?style=flat-square&logo=google&logoColor=white)](https://github.com/google/googletest)
Expand Down Expand Up @@ -148,8 +148,8 @@ ByteForge uses the **Token Bucket** algorithm for rate limiting — the same app

### Prerequisites

- C++17 compiler
- CMake 3.15+
- C++23 compiler
- CMake 3.20+
- Docker (optional)
- [nmap/ncat](https://nmap.org/download.html) for testing

Expand Down Expand Up @@ -183,13 +183,13 @@ The server starts on port `6625` by default. Type `stop` to shut it down cleanly
Build the image:

```bash
docker build -t byteforce:latest .
docker build -t byteforge:latest .
```

Run the container:

```bash
docker run -d -p 6625:6625 --name my-byteforce-db byteforce-db:latest
docker run -d -p 6625:6625 --name my-byteforge-db byteforge:latest
```

Connect using:
Expand Down
45 changes: 23 additions & 22 deletions src/Limit/Limiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@
*/
class RateLimiter
{

public:
/**
* @brief Constructs the rate limiter.
* @param maxTokens Max burst capacity per IP (default: 10).
* @param refillRate Tokens refilled per second per IP (default: 5).
* @param globalLimit Max total requests per second (default: 1000).
*/
RateLimiter(double maxTokens = 10, double refillRate = 5, int globalLimit = 1000);

/**
* @brief Destroys the rate limiter and clears all state.
*/
~RateLimiter();

/**
* @brief Checks if a request from the given IP should be allowed.
* @param ip The raw client IP address
* @return true if allowed, false if rate limited.
*/
bool isAllowed(uint32_t ip);

private:
/**
* @brief Token bucket state for a single IP address.
Expand All @@ -26,7 +48,7 @@ class RateLimiter
};

std::unordered_map<uint32_t, IPRecord> ipRecords; /**< Per-IP bucket state. */
std::mutex mapMutex; /**< Protects ipRecords from concurrent access. */
std::mutex mapMutex; /**< Protects ipRecords from concurrent access. */

std::atomic<int> globalCount; /**< Total requests in current global window. */
std::chrono::steady_clock::time_point globalWindowStart; /**< Start of current global window. */
Expand All @@ -49,27 +71,6 @@ class RateLimiter
* allowed, false if the IP is rate limited.
*/
bool isAllowedPrivate(uint32_t ip);

public:
/**
* @brief Constructs the rate limiter.
* @param maxTokens Max burst capacity per IP (default: 10).
* @param refillRate Tokens refilled per second per IP (default: 5).
* @param globalLimit Max total requests per second (default: 1000).
*/
RateLimiter(double maxTokens = 10, double refillRate = 5, int globalLimit = 1000);

/**
* @brief Destroys the rate limiter and clears all state.
*/
~RateLimiter();

/**
* @brief Checks if a request from the given IP should be allowed.
* @param ip The raw client IP address
* @return true if allowed, false if rate limited.
*/
bool isAllowed(uint32_t ip);
};

#endif
73 changes: 37 additions & 36 deletions src/RAM/HashMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,6 @@
*/
class HashMap
{
private:
/**
* @brief A node in the hash map's linked list.
*
* Stores the key-value pair and a pointer to the next node
* to handle bucket collisions via separate chaining.
*/
struct Node
{
std::string key; /**< The unique string key identifier (e.g., Name). */
std::string value; /**< The string data value associated with the key (e.g., Phone Number). */
Node *next; /**< Pointer to the next node in the chain (nullptr if last). */

/**
* @brief Constructs a new Node object.
* @param k The string key.
* @param v The string value.
*/
Node(const std::string &k, const std::string &v);
};

/** * @brief Dynamic array of Node pointers representing the hash table buckets.
*
* Since it points to an array of pointers, it requires double pointer syntax (Node**).
*/
Node **table;

int capacity; /**< The total size of the table array (number of available buckets). */
mutable std::shared_mutex sm; /** A mutex that protects the hashmap from 2 writer threads writing at the same time */

/**
* @brief Converts a string key into an array index.
* @param key The string key to be hashed.
* @return An integer index between 0 and capacity - 1.
*/
int hashFunction(const std::string& key) const;

public:
/**
Expand Down Expand Up @@ -88,6 +52,43 @@ class HashMap
* @param callback A function to call for each key-value pair.
*/
void forEach(std::function<void(const std::string &, const std::string &)> callback) const;

private:
/**
* @brief A node in the hash map's linked list.
*
* Stores the key-value pair and a pointer to the next node
* to handle bucket collisions via separate chaining.
*/
struct Node
{
std::string key; /**< The unique string key identifier (e.g., Name). */
std::string value; /**< The string data value associated with the key (e.g., Phone Number). */
Node *next; /**< Pointer to the next node in the chain (nullptr if last). */

/**
* @brief Constructs a new Node object.
* @param k The string key.
* @param v The string value.
*/
Node(const std::string &k, const std::string &v);
};

/** * @brief Dynamic array of Node pointers representing the hash table buckets.
*
* Since it points to an array of pointers, it requires double pointer syntax (Node**).
*/
Node **table;

int capacity; /**< The total size of the table array (number of available buckets). */
mutable std::shared_mutex sm; /** A mutex that protects the hashmap from 2 writer threads writing at the same time */

/**
* @brief Converts a string key into an array index.
* @param key The string key to be hashed.
* @return An integer index between 0 and capacity - 1.
*/
int hashFunction(const std::string &key) const;
};

#endif
8 changes: 5 additions & 3 deletions src/Server/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,21 @@ Server::~Server()
std::cout << "[SERVER] Cleanup complete\n";
}

void Server::start()
std::expected<void, std::string> Server::start()
{
std::cout << "[SERVER] Binding socket...\n";

if (bind(serverSocket, (sockaddr *)&serverAddr, sizeof(serverAddr)) != 0)
{
throw std::runtime_error("Server Failed to Start using bind");
return std::unexpected("Server Failed to Start using bind");
}

std::cout << "[SERVER] Bind successful!\n";
std::cout << "[SERVER] Listening for clients...\n";

if (listen(serverSocket, SOMAXCONN) != 0)
{
throw std::runtime_error("Server Failed to Listen using listen");
return std::unexpected("Server Failed to Listen using listen");
}

std::cout << "[SERVER] Server is now accepting connections!\n";
Expand All @@ -82,6 +82,8 @@ void Server::start()

// Start the event loop on its own thread now that the server is live.
eventLoopThread = std::thread(&Server::runEventLoop, this);

return {};
}

void Server::acceptClients()
Expand Down
8 changes: 6 additions & 2 deletions src/Server/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
#include <thread>
#include <vector>
#include <mutex>
#include <unordered_set>
#include <expected>
#include <string>
#include <flat_map>
#include <flat_set>

#include "../Worker/UserSessionBackgroundWorker.h"
#include "../RAM/HashMap.h"
Expand Down Expand Up @@ -71,8 +74,9 @@ class Server
/**
* @brief Starts the server, binds and begins listening.
* @throws std::runtime_error if socket setup fails.
* @return on success returns void, on failure a string
*/
void start();
std::expected<void, std::string> start();

/**
* @brief Accepts incoming client connections in a loop.
Expand Down
11 changes: 6 additions & 5 deletions src/Storage/Persistence.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@
*/
class Persistence
{
private:
std::ofstream aof_stream; /**< Persistent file stream handle for real-time logging. */
const std::string aof_path = "appendonly.log"; /**< File path for the transaction log file. */
const std::string rdb_path = "snapshot.log"; /**< File path for the point-in-time snapshot file. */
std::mutex aof_mutex; /**< Protects aof_stream from concurrent access. */

public:
/**
Expand Down Expand Up @@ -71,6 +66,12 @@ class Persistence
* @param hashmap A constant reference to the in-memory HashMap data source.
*/
void createSnapshot(const HashMap &hashmap);

private:
std::ofstream aof_stream; /**< Persistent file stream handle for real-time logging. */
const std::string aof_path = "appendonly.log"; /**< File path for the transaction log file. */
const std::string rdb_path = "snapshot.log"; /**< File path for the point-in-time snapshot file. */
std::mutex aof_mutex; /**< Protects aof_stream from concurrent access. */
};

#endif
2 changes: 1 addition & 1 deletion src/Tests/test_hashmap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ TEST(HashMapTests, HandledRemoveWhenKeyIsntInHashmap)
EXPECT_EQ(map.get(key), value);
}

TEST(HashMapConcurrencyTests, HandlesConcurrentInsertsSafely)
TEST(HashMapTests, HandlesConcurrentInsertsSafely)
{
HashMap map{5}; // The map is small to make sure that there is many collitions
const int num_threads = 5;
Expand Down
23 changes: 12 additions & 11 deletions src/Worker/SnapshotScheduler.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,6 @@
*/
class SnapshotScheduler
{
private:
std::thread schedulerThread; /**< Background thread that runs the scheduler loop. */
std::atomic<bool> active; /**< Controls whether the scheduler is running. */
std::function<void()> snapshotCallback; /**< The snapshot function to call every interval. */
std::chrono::minutes interval; /**< How often to take a snapshot. */
std::condition_variable cv; /**< Bell that wakes up sleeping workers. */
std::mutex m; /**< Required by condition_variable for wait_for to work. */
/**
* @brief The main loop that sleeps then triggers a snapshot.
*/
void run();

public:
/**
Expand All @@ -37,6 +26,18 @@ class SnapshotScheduler
* @brief Stops the scheduler and joins the background thread.
*/
~SnapshotScheduler();

private:
std::thread schedulerThread; /**< Background thread that runs the scheduler loop. */
std::atomic<bool> active; /**< Controls whether the scheduler is running. */
std::function<void()> snapshotCallback; /**< The snapshot function to call every interval. */
std::chrono::minutes interval; /**< How often to take a snapshot. */
std::condition_variable cv; /**< Bell that wakes up sleeping workers. */
std::mutex m; /**< Required by condition_variable for wait_for to work. */
/**
* @brief The main loop that sleeps then triggers a snapshot.
*/
void run();
};

#endif
13 changes: 7 additions & 6 deletions src/Worker/ThreadPool.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@
*/
class ThreadPool
{
private:
std::queue<std::function<void()>> jobs; /**< Queue of pending jobs to be executed. */
std::vector<std::thread> threads; /**< Pool of worker threads. */
std::mutex m; /**< Mutex protecting the job queue. */
std::condition_variable cv; /**< Bell that wakes up sleeping workers. */
std::atomic<bool> active; /**< Controls whether the pool is running. */

public:
/**
Expand All @@ -42,6 +36,13 @@ class ThreadPool
* @param job A callable with no arguments or return value.
*/
void acceptJob(std::function<void()> job);

private:
std::queue<std::function<void()>> jobs; /**< Queue of pending jobs to be executed. */
std::vector<std::thread> threads; /**< Pool of worker threads. */
std::mutex m; /**< Mutex protecting the job queue. */
std::condition_variable cv; /**< Bell that wakes up sleeping workers. */
std::atomic<bool> active; /**< Controls whether the pool is running. */
};

#endif
10 changes: 7 additions & 3 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ int main()

Server server(port);

server.start();

std::signal(SIGTERM, handleSignal);
std::signal(SIGINT, handleSignal);

if (auto result = server.start(); !result)
{
std::cerr << "[MAIN] Failed to start server: " << result.error() << "\n";
return 1;
}

// Run accept loop in background thread
std::thread serverThread(&Server::acceptClients, &server);
std::jthread serverThread(&Server::acceptClients, &server);

std::cout << "[MAIN] Server running. Type 'stop' to shut it down.\n";

Expand Down
Loading