chore: initial commit
Some checks failed
Code Analysis / analysis (push) Failing after 2m59s

This commit is contained in:
2025-12-30 22:34:58 +07:00
commit 35a6349071
63 changed files with 2675 additions and 0 deletions

57
.clang-format Normal file
View File

@@ -0,0 +1,57 @@
---
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: AlwaysBreak
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignOperands: false
AlignTrailingComments: true
AllowShortIfStatementsOnASingleLine: false
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BinPackParameters: false
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeBraces: Custom
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 120
ConstructorInitializerAllOnOneLineOrOnePerLine: false
IncludeCategories:
- Regex: '^<.*'
Priority: 1
- Regex: '^".*'
Priority: 2
- Regex: '.*'
Priority: 3
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseLabels: true
IndentPPDirectives: BeforeHash
IndentWidth: 4
InsertNewlineAtEOF: true
MacroBlockBegin: ''
MacroBlockEnd: ''
NamespaceIndentation: All
PointerAlignment: Left
SpaceInEmptyParentheses: false
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
TabWidth: 4
...

56
.github/ISSUE_TEMPLATE/task-template.md vendored Normal file
View File

@@ -0,0 +1,56 @@
---
name: Task template
about: This is a template for creating tasks for team members.
title: ''
labels: ''
assignees: ''
---
# Task title
## Context
Here describe the context of this tasks. Why do you want to open this task? Why should it be completed? How completing it will contribute to the project?
_Example_
> The frontend and the backend need an effective way to communicate with each other. A message queue is well suited for this. It allows for easy task distribution and is straightforward to use.
## Problem statement
Here describe what exactly needs to be done. The task must be clearly worded and realistic.
Be careful here. Describe **precisely** what you want to be done. Think of it like the assignee has an irresistible desire to do everything you did not put here in the exact opposite way compared to what you want (just to irritate you, for fun). Try to think of everything that can go wrong, just in case.
_Example_
> A class `MessageQueue` must be implemented. It must have two methods:
> * `void push(Request request);`
> * `Request pop();`
> The first one should add the request to the queue. The second is to delete the first of the remaining ones and return it.
>
> For this task one must create a separate branch based on the latest commit of branch [base branch].
>
> The implementation must be split across two files located at the following paths:
> `rediska/common/queue/MessageQueue.hpp` - the declarations for the class
> `rediska/common/queue/MessageQueue.cpp` - the definitions for the class
>
> Tests for the class must also be added. The following cases must be covered:
> 1. Case 1
> 2. Case 2
>
> After the task is completed, a pull request to [base branch] must be created and [requester's name] must be added as the assignee.
## Success criteria
List all conditions that can be objectively verified and unambiguously indicate successful completion of the task.
_Example_
> 1. The aforementioned class with the requested interface is implemented, the implementation can be found at path [path].
> 2. Unit tests, which cover all of the listed cases, are added and the implementation passes all of them.
## Advice (optional)
If you still have anything helpful to add, do it here. Provide any advice that will help the assignee quickly and successfully complete the task. _Do not hesitate to think for them a little, if you believe it will help them do their job more effectively (in a shorter time period without prejudice to correctness)_.

26
.github/workflows/code_checks.yaml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Code Analysis
on: [push]
jobs:
analysis:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: aminya/setup-cpp@v1
with:
cppcheck: true
- name: Static analysis (source code)
run: |
cppcheck \
--enable=all \
--std=c++23 \
--inline-suppr \
--error-exitcode=1 \
--suppress=missingInclude \
--suppress=missingIncludeSystem \
rediska/

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
/.idea/.gitignore
/.idea/editor.xml
/.idea/misc.xml
/.idea/modules.xml
/.idea/inspectionProfiles/Project_Default.xml
/.idea/Rediska.iml
/.idea/vcs.xml
/cmake-build-debug
/cmake-build-release
/tests/cmake-build-debug
/tests/cmake-build-release
/build
.cache
allure-report
allure-results
.allure
*vcpkg*
**/.DS_Store

19
.zed/debug.json Normal file
View File

@@ -0,0 +1,19 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Unit tests",
"build": {
"command": "cmake",
"args": [
"--build", "${ZED_WORKTREE_ROOT}/cmake-build-debug", "-j", "12"
]
},
"reverseDebugging": true,
"program": "${ZED_WORKTREE_ROOT}/cmake-build-debug/tests/unit_tests",
"adapter": "CodeLLDB",
"request": "launch"
}
]

51
.zed/tasks.json Normal file
View File

@@ -0,0 +1,51 @@
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
[
{
"label": "Setup debug build directory",
"command": "cmake -S ${ZED_WORKTREE_ROOT} -B ${ZED_WORKTREE_ROOT}/cmake-build-debug -DCMAKE_BUILD_TYPE=Debug",
"reveal": "no_focus",
"hide": "on_success"
},
{
"label": "Setup release build directory",
"command": "cmake -S ${ZED_WORKTREE_ROOT} -B ${ZED_WORKTREE_ROOT}/cmake-build-release -DCMAKE_BUILD_TYPE=Release",
"reveal": "no_focus",
"hide": "on_success"
},
{
"label": "Setup tests debug build directory",
"command": "cmake -S ${ZED_WORKTREE_ROOT} -B ${ZED_WORKTREE_ROOT}/tests/cmake-build-debug -DCMAKE_BUILD_TYPE=Debug",
"reveal": "no_focus",
"hide": "on_success"
},
{
"label": "Setup tests release build directory",
"command": "cmake -S ${ZED_WORKTREE_ROOT} -B ${ZED_WORKTREE_ROOT}/tests/cmake-build-release -DCMAKE_BUILD_TYPE=Release",
"reveal": "no_focus",
"hide": "on_success"
},
{
"label": "Clean debug build directory",
"command": "cmake --build ${ZED_WORKTREE_ROOT}/cmake-build-debug --target clean",
"reveal": "no_focus",
"hide": "on_success"
},
{
"label": "Clean release build directory",
"command": "cmake --build ${ZED_WORKTREE_ROOT}/cmake-build-release --target clean",
"reveal": "no_focus",
"hide": "on_success"
},
{
"label": "Clean tests debug build directory",
"command": "cmake --build ${ZED_WORKTREE_ROOT}/tests/cmake-build-debug --target clean",
"reveal": "no_focus",
"hide": "on_success"
},
{
"label": "Clean tests release build directory",
"command": "cmake --build ${ZED_WORKTREE_ROOT}/tests/cmake-build-release --target clean",
"reveal": "no_focus",
"hide": "on_success"
}
]

33
CMakeLists.txt Normal file
View File

@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.28)
project(Rediska)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if (MSVC)
add_compile_options(/W4)
else()
# add_compile_options(-Wall -Wextra -Werror)
endif()
include(FetchContent)
FetchContent_Declare(
doctest
GIT_REPOSITORY https://github.com/doctest/doctest.git
GIT_TAG v2.4.12
)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.16.0
)
FetchContent_MakeAvailable(doctest)
FetchContent_MakeAvailable(spdlog)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
add_subdirectory(rediska)
enable_testing()
add_subdirectory(tests)

44
CMakePresets.json Normal file
View File

@@ -0,0 +1,44 @@
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"displayName": "Debug Build (vcpkg)",
"description": "Debug build using vcpkg dependencies",
"binaryDir": "${sourceDir}/cmake-build-debug",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "debug-build",
"displayName": "Debug Build (vcpkg)",
"description": "Debug build using vcpkg dependencies",
"binaryDir": "${sourceDir}/build",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "debug-build",
"configurePreset": "debug-build"
},
{
"name": "debug-run",
"configurePreset": "debug",
"targets": [ "run" ]
}
]
}

22
README.md Normal file
View File

@@ -0,0 +1,22 @@
# Rediska
## Building from source
### Prerequisites (vcpkg)
> [!NOTE]
> Add `--triplet x64-linux-release` to the vcpkg install to build dependencies in release.
> Replace `linux` with your OS if it is supported
```bash
git clone --branch 2025.10.17 --depth 1 https://github.com/microsoft/vcpkg.git && \
./vcpkg/bootstrap-vcpkg.sh && \
./vcpkg/vcpkg install
```
### CMake workflow
```bash
cmake --preset debug
cmake --build --preset debug-run
```

View File

@@ -0,0 +1,19 @@
syntax = "proto3";
package v1.collections.common;
message CollectionElement {
enum SpecialElementKind {
STR = 0;
OBJ = 1;
}
SpecialElementKind special_element_kind = 1;
oneof element {
int64 integer = 2;
double floating_point = 3;
string str_or_obj = 4;
bool boolean = 5;
}
}

View File

@@ -0,0 +1,12 @@
syntax = "proto3";
package v1.collections.common;
enum ElementKind {
INT = 0;
FLOAT = 1;
BOOL = 2;
STRING = 3;
OBJECT = 4;
LIST = 5;
}

View File

@@ -0,0 +1,95 @@
syntax = "proto3";
import "v1/collections/common.proto";
import "v1/collections/element_kind.proto";
import "google/protobuf/empty.proto";
package v1.collections.list;
service ListCacheService {
rpc Create(ListCreateRequest) returns (ListCreateResponse);
rpc Get(ListGetRequest) returns (stream ListGetResponse);
rpc Insert(ListInsertRequest) returns (google.protobuf.Empty);
rpc Erase(ListEraseRequest) returns (ListEraseResponse);
rpc Set(ListSetRequest) returns (ListSetResponse);
rpc Length(ListLengthRequest) returns (ListLengthResponse);
rpc PushBack(PushBackRequest) returns (google.protobuf.Empty);
rpc PushMany(stream PushManyRequest) returns (google.protobuf.Empty);
rpc PopBack(PopBackRequest) returns (PopBackResponse);
rpc Clear(ClearRequest) returns (google.protobuf.Empty);
rpc Delete(DeleteRequest) returns (google.protobuf.Empty);
}
message ListCreateRequest {
common.ElementKind element_kind = 1;
optional uint32 ttl_seconds = 2;
}
message ListCreateResponse {
string id = 1;
}
message ListGetRequest {
string id = 1;
}
message ListGetResponse {
common.CollectionElement element = 1;
}
message ListInsertRequest {
string id = 1;
uint64 index = 2;
common.CollectionElement value = 3;
}
message ListEraseRequest {
string id = 1;
int64 index = 2;
}
message ListEraseResponse {
common.CollectionElement removed_value = 1;
}
message ListSetRequest {
string id = 1;
repeated common.CollectionElement elements = 2;
}
message ListSetResponse {
}
message ListLengthRequest {
string id = 1;
}
message ListLengthResponse {
uint64 length = 1;
}
message PushBackRequest {
string id = 1;
common.CollectionElement element = 2;
}
message PushManyRequest {
string id = 1;
common.CollectionElement element = 2;
}
message PopBackRequest {
string id = 1;
}
message PopBackResponse {
common.CollectionElement element = 1;
}
message ClearRequest {
string id = 1;
}
message DeleteRequest {
string id = 1;
}

View File

@@ -0,0 +1,60 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package v1.object;
service ObjectCacheService {
rpc Create(CreateObjectRequest) returns (CreateObjectResponse);
rpc Get(GetObjectRequest) returns (GetObjectResponse);
rpc Set(SetObjectRequest) returns (google.protobuf.Empty);
rpc Delete(DeleteObjectRequest) returns (google.protobuf.Empty);
rpc GetField(GetObjectFieldRequest) returns (GetObjectFieldResponse);
rpc SetField(SetObjectFieldRequest) returns (google.protobuf.Empty);
rpc DeleteField(DeleteObjectFieldRequest) returns (google.protobuf.Empty);
}
message CreateObjectRequest {
string object = 1;
}
message CreateObjectResponse {
string id = 1;
}
message GetObjectRequest {
string id = 1;
}
message GetObjectResponse {
string object = 1;
}
message SetObjectRequest {
string id = 1;
string new_object = 2;
}
message DeleteObjectRequest {
string id = 1;
}
message GetObjectFieldRequest {
string id = 1;
string field_name = 2;
}
message GetObjectFieldResponse {
string field_value = 1;
}
message SetObjectFieldRequest {
string id = 1;
string field_name = 2;
string value = 3;
}
message DeleteObjectFieldRequest {
string id = 1;
string field_name = 2;
}

View File

@@ -0,0 +1,42 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package v1.primitives.boolean;
service BoolCacheService {
rpc Create(BoolCreateRequest) returns (BoolCreateResponse);
rpc Set(BoolSetRequest) returns (google.protobuf.Empty);
rpc Get(BoolGetRequest) returns (BoolGetResponse);
rpc Delete(BoolDeleteRequest) returns (BoolDeleteResponse);
}
message BoolCreateRequest {
bool value = 1;
optional uint32 ttl_seconds = 2;
}
message BoolCreateResponse {
string id = 1;
}
message BoolSetRequest {
string id = 1;
bool value = 2;
}
message BoolGetRequest {
string id = 1;
}
message BoolGetResponse {
bool value = 1;
}
message BoolDeleteRequest {
string id = 1;
}
message BoolDeleteResponse {
bool removed_value = 1;
}

View File

@@ -0,0 +1,42 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package v1.primitives.flt;
service FloatCacheService {
rpc Create(FloatCreateRequest) returns (FloatCreateResponse);
rpc Set(FloatSetRequest) returns (google.protobuf.Empty);
rpc Get(FloatGetRequest) returns (FloatGetResponse);
rpc Delete(FloatDeleteRequest) returns (FloatDeleteResponse);
}
message FloatCreateRequest {
double value = 1;
optional uint32 ttl_seconds = 2;
}
message FloatCreateResponse {
string id = 1;
}
message FloatSetRequest {
string id = 1;
double value = 2;
}
message FloatGetRequest {
string id = 1;
}
message FloatGetResponse {
double value = 1;
}
message FloatDeleteRequest {
string id = 1;
}
message FloatDeleteResponse {
double removed_value = 1;
}

View File

@@ -0,0 +1,42 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package v1.primitives.integer;
service IntCacheService {
rpc Create(IntCreateRequest) returns (IntCreateResponse);
rpc Set(IntSetRequest) returns (google.protobuf.Empty);
rpc Get(IntGetRequest) returns (IntGetResponse);
rpc Delete(IntDeleteRequest) returns (IntDeleteResponse);
}
message IntCreateRequest {
int64 value = 1;
optional uint32 ttl_seconds = 2;
}
message IntCreateResponse {
string id = 1;
}
message IntSetRequest {
string id = 1;
int64 value = 2;
}
message IntGetRequest {
string id = 1;
}
message IntGetResponse {
int64 value = 1;
}
message IntDeleteRequest {
string id = 1;
}
message IntDeleteResponse {
int64 removed_value = 1;
}

View File

@@ -0,0 +1,42 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
package v1.primitives.str;
service StringCacheService {
rpc Create(StringCreateRequest) returns (StringCreateResponse);
rpc Set(StringSetRequest) returns (google.protobuf.Empty);
rpc Get(StringGetRequest) returns (StringGetResponse);
rpc Delete(StringDeleteRequest) returns (StringDeleteResponse);
}
message StringCreateRequest {
string value = 1;
optional uint32 ttl_seconds = 2;
}
message StringCreateResponse {
string id = 1;
}
message StringSetRequest {
string id = 1;
string value = 2;
}
message StringGetRequest {
string id = 1;
}
message StringGetResponse {
string value = 1;
}
message StringDeleteRequest {
string id = 1;
}
message StringDeleteResponse {
string removed_value = 1;
}

25
rediska/CMakeLists.txt Normal file
View File

@@ -0,0 +1,25 @@
add_subdirectory(common)
add_subdirectory(data-structures)
add_subdirectory(worker)
add_subdirectory(cache)
add_subdirectory(frontend)
add_executable(Rediska main.cpp)
target_link_libraries(Rediska PRIVATE
frontend
worker
cache
common
data-structures
)
# Convenience target
add_custom_target(run
COMMAND $<TARGET_FILE:Rediska>
DEPENDS Rediska
USES_TERMINAL
COMMENT "Building and running Rediska"
)

7
rediska/cache/BaseCacheConfig.hpp vendored Normal file
View File

@@ -0,0 +1,7 @@
#pragma once
namespace cache {
struct BaseCacheConfig {
bool resetTTLOnAccess = true;
};
}

20
rediska/cache/BaseItemMetadata.cpp vendored Normal file
View File

@@ -0,0 +1,20 @@
#include "BaseItemMetadata.hpp"
namespace cache {
BaseItemMetadata::BaseItemMetadata(TTL ttl) : ttl_(ttl) {
resetExpirationTime();
}
void BaseItemMetadata::updateTTL(TTL ttl) {
ttl_ = ttl;
resetExpirationTime();
}
bool BaseItemMetadata::isExpired() const {
return expirationTime_ < std::chrono::steady_clock::now();
}
void BaseItemMetadata::resetExpirationTime() {
expirationTime_ = std::chrono::steady_clock::now() + std::chrono::seconds(ttl_);
}
}

20
rediska/cache/BaseItemMetadata.hpp vendored Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "rediska/common/types.hpp"
namespace cache {
class BaseItemMetadata {
public:
BaseItemMetadata(TTL ttl);
void updateTTL(TTL ttl);
void resetExpirationTime();
bool isExpired() const;
private:
TTL ttl_;
Timestamp expirationTime_;
};
}

9
rediska/cache/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,9 @@
add_library(cache STATIC
lru/LRU.cpp
lru/LRUItemMetadata.cpp
BaseItemMetadata.cpp
)
target_link_libraries(cache PUBLIC
common
)

21
rediska/cache/CachePolicy.hpp vendored Normal file
View File

@@ -0,0 +1,21 @@
#include "rediska/common/MessageArguments.hpp"
#include "rediska/common/enums.hpp"
#include "rediska/common/types.hpp"
namespace cache {
class CachePolicy {
public:
virtual ~CachePolicy() = default;
virtual void get(CacheKey&& key) = 0;
virtual void set(CacheKey&& key, CacheValue&& value, TTL ttl) = 0;
virtual void applyTo(CacheKey&& key, OperationId op, MessageArguments&& args) = 0;
protected:
virtual void evict() = 0;
[[nodiscard]] virtual inline bool isFull() const = 0;
};
}

5
rediska/cache/constants.hpp vendored Normal file
View File

@@ -0,0 +1,5 @@
#pragma once
#include <cstdint>
constexpr int16_t MAX_ALLOWED_WORKERS = INT8_MAX + 1;

118
rediska/cache/lru/LRU.cpp vendored Normal file
View File

@@ -0,0 +1,118 @@
#include "rediska/cache/lru/LRU.hpp"
#include <expected>
#include <mutex>
#include <optional>
#include <unordered_map>
#include "rediska/cache/lru/LRUConfig.hpp"
#include "rediska/cache/types.hpp"
#include "rediska/common/MessageArguments.hpp"
#include "rediska/common/types.hpp"
#include "rediska/common/utils.hpp"
namespace cache {
LRU::LRU(LRUConfig config, CacheOpCallback callback)
: config_(std::move(config)), callback_(callback) {}
void LRU::get(CacheKey&& key) {
std::shared_lock lock(mutex_);
auto it = keyToItem_.find(key);
if (it == keyToItem_.end()) {
return callback_(std::unexpected<RediskaReturnCode>(RediskaReturnCode::NOT_FOUND));
}
auto& metadata = it->second->location.metadata;
if (metadata.isExpired()) {
evict(it);
return callback_(std::unexpected<RediskaReturnCode>(RediskaReturnCode::KEY_EXPIRED));
}
if (config_.resetTTLOnAccess) metadata.resetExpirationTime();
// Move to start
lru_list_.splice(lru_list_.begin(), lru_list_, it->second);
keyToItem_[key] = lru_list_.begin();
callback_(std::make_optional(it->second->location.value));
}
void LRU::set(CacheKey&& key, CacheValue&& value, TTL ttl) {
std::unique_lock lock(mutex_);
auto it = keyToItem_.find(key);
if (it != keyToItem_.end()) {
it->second->location.value = std::move(value);
it->second->location.metadata.resetExpirationTime();
lru_list_.splice(lru_list_.begin(), lru_list_, it->second);
callback_(std::nullopt);
return;
}
if (isFull()) evict();
lru_list_.push_front(
CacheNode {
.key = std::move(key),
.location = ItemHandle{
.value = std::move(value),
.metadata = LRU::ItemMetadata(ttl)
}
}
);
keyToItem_[key] = lru_list_.begin();
callback_(std::nullopt);
}
void LRU::applyTo(CacheKey&& key, OperationId op, MessageArguments&& args) {
std::unique_lock lock(mutex_);
auto it = keyToItem_.find(key);
if (it == keyToItem_.end()) {
return callback_(std::unexpected<RediskaReturnCode>(RediskaReturnCode::NOT_FOUND));
}
auto& metadata = it->second->location.metadata;
std::expected<std::optional<CacheValue>, RediskaReturnCode> value;
std::visit([&value, op](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (is_any_of_v<T, int64_t, bool, double, std::string>) {
value = std::unexpected(RediskaReturnCode::INCOMPATIBLE_OPERATION);
} else if constexpr (std::is_same_v<T, std::shared_ptr<ListDataStructure>>) {
// TODO: Replace with real data argument
DSValue dummy_data{};
std::expected<std::optional<DSValue>, DSReturnCode> res = arg->handle(op, std::move(dummy_data));
if (!res) {
value = std::unexpected(DSReturnCodeToRediskaReturnCode(res.error()));
return;
}
if (!res.value().has_value()) {
value = std::nullopt;
return;
}
// TODO: Explore implicit conversion
// Attempts to covert failed
value = std::visit([](auto&& arg) -> CacheValue {
return CacheValue{arg};
}, std::move(res->value()));
}
}, it->second->location.value);
lru_list_.splice(lru_list_.begin(), lru_list_, it->second);
callback_(std::move(value));
return;
}
void LRU::evict() {
if (lru_list_.empty()) return;
keyToItem_.erase(lru_list_.back().key);
lru_list_.pop_back();
}
void LRU::evict(const std::unordered_map<CacheKey, std::list<CacheNode>::iterator>::iterator node) {
lru_list_.erase(node->second);
keyToItem_.erase(node);
}
inline bool LRU::isFull() const {
return lru_list_.size() >= config_.maxCapacity;
}
}

52
rediska/cache/lru/LRU.hpp vendored Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
#include <list>
#include <shared_mutex>
#include <unordered_map>
#include "rediska/cache/lru/LRUConfig.hpp"
#include "rediska/cache/lru/LRUItemMetadata.hpp"
#include "rediska/cache/types.hpp"
#include "rediska/common/MessageArguments.hpp"
#include "rediska/common/types.hpp"
#include "rediska/cache/CachePolicy.hpp"
namespace cache {
class LRU : public CachePolicy {
public:
using ItemMetadata = LRUItemMetadata;
using ItemHandle = struct {
CacheValue value;
ItemMetadata metadata;
};
using CacheNode = struct {
CacheKey key;
ItemHandle location;
};
LRU(LRUConfig config, CacheOpCallback callback);
~LRU() = default;
void get(CacheKey&& key) override;
void set(CacheKey&& key, CacheValue&& value, TTL ttl) override;
void applyTo(CacheKey&& key, OperationId op, MessageArguments&& args) override;
private:
std::list<CacheNode> lru_list_;
std::unordered_map<CacheKey, std::list<CacheNode>::iterator> keyToItem_;
std::shared_mutex mutex_;
LRUConfig config_;
CacheOpCallback callback_;
void evict() override;
void evict(const std::unordered_map<CacheKey, std::list<CacheNode>::iterator>::iterator node);
inline bool isFull() const override;
void resetTTLIfEnabled(CacheNode& key);
};
}

12
rediska/cache/lru/LRUConfig.hpp vendored Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include <cstddef>
#include "rediska/cache/BaseCacheConfig.hpp"
#include "rediska/common/types.hpp"
namespace cache {
struct LRUConfig : BaseCacheConfig {
size_t maxCapacity;
TTL ttl;
};
}

9
rediska/cache/lru/LRUItemMetadata.cpp vendored Normal file
View File

@@ -0,0 +1,9 @@
#include "rediska/cache/BaseItemMetadata.hpp"
#include "rediska/common/types.hpp"
namespace cache {
class LRUItemMetadata : public BaseItemMetadata {
public:
LRUItemMetadata(TTL ttl) : BaseItemMetadata(ttl) {}
};
}

10
rediska/cache/lru/LRUItemMetadata.hpp vendored Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#include "rediska/cache/BaseItemMetadata.hpp"
namespace cache {
class LRUItemMetadata : public BaseItemMetadata {
public:
LRUItemMetadata(TTL ttl);
};
}

10
rediska/cache/types.hpp vendored Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#include <expected>
#include <functional>
#include <optional>
#include "rediska/common/types.hpp"
namespace cache {
using CacheOpCallback = std::function<void(std::expected<std::optional<CacheValue>, RediskaReturnCode>)>;
}

View File

@@ -0,0 +1,10 @@
add_library(common STATIC
tmp.cpp
)
find_package(concurrentqueue REQUIRED)
target_link_libraries(common
PUBLIC
spdlog::spdlog
)

View File

@@ -0,0 +1,46 @@
#pragma once
#include <variant>
#include "rediska/common/types.hpp"
struct PrimitiveSetArgs {
CacheValue value;
TTL ttl_seconds;
};
struct ListCreateArgs {
CacheValueId element_kind;
TTL ttl_seconds;
};
struct ListIndexArgs {
int64_t index;
};
struct ListSetArgs {
int64_t index;
CacheValue value;
};
struct ListInsertArgs {
int64_t index;
CacheValue value;
};
struct ListPushBackArgs {
CacheValue value;
};
struct ListPushManyArgs {
std::vector<CacheValue> values;
bool replace_entire_list = false; // true => overwrite entire list, false => append
};
using MessageArguments = std::variant<std::monostate,
PrimitiveSetArgs,
ListCreateArgs,
ListIndexArgs,
ListSetArgs,
ListInsertArgs,
ListPushBackArgs,
ListPushManyArgs>;

View File

@@ -0,0 +1,42 @@
#pragma once
#include <variant>
#include <vector>
#include "rediska/common/enums.hpp"
#include "rediska/common/types.hpp"
#include "rediska/common/MessageArguments.hpp"
#include <grpcpp/grpcpp.h>
#include <grpcpp/support/async_stream.h> // for ServerAsyncStreamingInterface completeness
// TODO: get rid of forward declaration somehow maybe
struct BaseRequestManager;
struct RequestEvent {
BaseRequestManager& manager;
};
// This just leaves the responder for you to delete.
struct FinishEvent {
std::unique_ptr<grpc::internal::ServerAsyncStreamingInterface> responder;
};
using EventVariant = std::variant<RequestEvent, FinishEvent>;
struct QueueMessage {
CacheValueId type;
CacheKey key;
OperationId operation;
MessageArguments arguments;
std::unique_ptr<grpc::internal::ServerAsyncStreamingInterface> responder;
template<typename ResponseT>
void respond(ResponseT response) {
if (auto* writer = dynamic_cast<grpc::ServerAsyncResponseWriter<ResponseT>*>(responder.get())) {
writer->Finish(response, grpc::Status::OK, std::make_unique<EventVariant>(FinishEvent { std::move(responder) }).release());
responder = nullptr;
} else {
throw std::runtime_error("Invalid responder type in QueueMessage::respond");
}
}
};

13
rediska/common/enums.hpp Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
enum class OperationId {
GET,
SET,
DELETE,
LIST_PUSH_BACK,
LIST_POP_BACK,
LIST_INSERT,
LIST_ERASE,
OBJECT_GET_FIELD,
OBJECT_SET_FIELD,
};

0
rediska/common/tmp.cpp Normal file
View File

30
rediska/common/types.hpp Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <variant>
#include <chrono>
#include "rediska/data-structures/impl/ListDataStructure.hpp"
using CacheKey = std::string;
// Definition order of `CacheValue` and `TypeId` MUST match!
using CacheValue = std::variant<bool, int64_t, double, std::string, std::shared_ptr<ListDataStructure>>;
enum class CacheValueId { BOOLEAN = 0, INT, FLOAT, STRING, ARRAY }; // TODO: Object
enum class RediskaReturnCode {
OK,
INCOMPATIBLE_OPERATION,
NOT_FOUND,
KEY_EXPIRED,
UNKNOWN_ERROR,
DS_EMPTY,
DS_OUT_OF_RANGE,
DS_UNKNOWN_ERROR
};
using TTL = uint32_t;
using Timestamp = std::chrono::steady_clock::time_point;

24
rediska/common/utils.hpp Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <type_traits>
#include "rediska/common/types.hpp"
template<typename T, typename... Ts>
constexpr bool is_any_of_v = (std::is_same_v<T, Ts> || ...);
inline RediskaReturnCode DSReturnCodeToRediskaReturnCode(DSReturnCode code) {
switch (code) {
case DSReturnCode::OK:
return RediskaReturnCode::OK;
case DSReturnCode::NOT_FOUND:
return RediskaReturnCode::NOT_FOUND;
case DSReturnCode::INCOMPATIBLE_OPERATION:
return RediskaReturnCode::INCOMPATIBLE_OPERATION;
case DSReturnCode::EMPTY:
return RediskaReturnCode::DS_EMPTY;
case DSReturnCode::OUT_OF_RANGE:
return RediskaReturnCode::DS_OUT_OF_RANGE;
default:
return RediskaReturnCode::UNKNOWN_ERROR;
}
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <expected>
#include <optional>
#include "rediska/common/enums.hpp"
#include "rediska/data-structures/enums.hpp"
#include "rediska/data-structures/types.hpp"
class AbstractDataStructure {
public:
virtual ~AbstractDataStructure() = default;
virtual std::expected<std::optional<DSValue>, DSReturnCode> handle(OperationId op, DSValue data) = 0;
};

View File

@@ -0,0 +1,7 @@
add_library(data-structures STATIC
impl/ListDataStructure.cpp
)
target_link_libraries(data-structures PUBLIC
common
)

View File

View File

@@ -0,0 +1,3 @@
#pragma once
enum class DSReturnCode { OK, INCOMPATIBLE_OPERATION, EMPTY, OUT_OF_RANGE, NOT_FOUND };

View File

@@ -0,0 +1,56 @@
#include <list>
#include "rediska/data-structures/AbstractDataStructure.hpp"
#include "rediska/data-structures/enums.hpp"
#include "rediska/data-structures/types.hpp"
class ListDataStructure : public AbstractDataStructure {
public:
~ListDataStructure() = default;
using arguments = struct {};
std::expected<std::optional<DSValue>, DSReturnCode> handle(OperationId op, DSValue data) override {
switch (op) {
case OperationId::GET: {
// TODO: Index
// if (index < 0 || index >= list_.size()) {
// return std::unexpected(DSReturnCode::OUT_OF_RANGE);
// }
return std::make_optional(*list_.begin());
}
case OperationId::SET: {
// Replace entire list with a single value if provided
list_.clear();
list_.push_back(data);
return std::nullopt;
}
case OperationId::LIST_PUSH_BACK: {
list_.push_back(data);
return std::nullopt;
}
case OperationId::LIST_POP_BACK: {
if (list_.empty()) {
return std::unexpected(DSReturnCode::EMPTY);
}
DSValue value = list_.back();
list_.pop_back();
return std::make_optional(value);
}
case OperationId::LIST_INSERT: {
// TODO: if index
list_.insert(list_.begin(), data);
return std::nullopt;
}
case OperationId::LIST_ERASE: {
// TODO: if index
list_.erase(list_.begin());
return std::nullopt;
}
default:
return std::unexpected(DSReturnCode::INCOMPATIBLE_OPERATION);
}
}
private:
std::list<DSValue> list_;
};

View File

@@ -0,0 +1,15 @@
#pragma once
#include <list>
#include "rediska/data-structures/AbstractDataStructure.hpp"
#include "rediska/data-structures/types.hpp"
class ListDataStructure : public AbstractDataStructure {
public:
~ListDataStructure() = default;
std::expected<std::optional<DSValue>, DSReturnCode> handle(OperationId op, DSValue data) override;
private:
std::list<DSValue> list_;
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include <cstdint>
#include <string>
#include <variant>
using DSValue = std::variant<bool, int64_t, double, std::string>;

View File

@@ -0,0 +1,63 @@
add_library(frontend STATIC
# IntService.cpp
# BoolService.cpp
# FloatService.cpp
# StringService.cpp
CallData.cpp
server.hpp
RequestManager.hpp
)
# Find packages
find_package(Protobuf REQUIRED CONFIG)
find_package(gRPC REQUIRED CONFIG)
set(PROTO_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../proto")
# Define the proto files
set(PROTO_FILES
"${PROTO_ROOT_DIR}/v1/collections/common.proto"
"${PROTO_ROOT_DIR}/v1/collections/element_kind.proto"
"${PROTO_ROOT_DIR}/v1/collections/list.proto"
"${PROTO_ROOT_DIR}/v1/object/object.proto"
"${PROTO_ROOT_DIR}/v1/primitives/bool.proto"
"${PROTO_ROOT_DIR}/v1/primitives/float.proto"
"${PROTO_ROOT_DIR}/v1/primitives/int.proto"
"${PROTO_ROOT_DIR}/v1/primitives/string.proto"
)
# This allows protobuf_generate to find them when inspecting the target.
target_sources(frontend PRIVATE ${PROTO_FILES})
# Include directories
# We include the binary dir (for generated .pb.h files) and the root (for imports)
target_include_directories(frontend PUBLIC
"${CMAKE_CURRENT_BINARY_DIR}"
"${PROTO_ROOT_DIR}"
)
# Generate Standard Protobuf files (.pb.cc / .pb.h)
# We use 'protobuf_generate' instead of the legacy 'protobuf_generate_cpp'.
# It automatically adds the generated C++ files back into the 'frontend' target.
protobuf_generate(
TARGET frontend
LANGUAGE cpp
IMPORT_DIRS "${PROTO_ROOT_DIR}"
PROTOC_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}"
)
# Generate gRPC specific files (.grpc.pb.cc / .grpc.pb.h)
protobuf_generate(
TARGET frontend
LANGUAGE grpc
GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc
PLUGIN "protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>"
IMPORT_DIRS "${PROTO_ROOT_DIR}"
PROTOC_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}"
)
target_link_libraries(frontend
PUBLIC
gRPC::grpc++
protobuf::libprotobuf
)

View File

@@ -0,0 +1,336 @@
#include <functional>
#include <grpcpp/grpcpp.h>
#include <memory>
#include <vector>
#include "google/protobuf/empty.pb.h"
#include "v1/collections/list.grpc.pb.h"
#include "v1/primitives/bool.grpc.pb.h"
#include "v1/primitives/int.grpc.pb.h"
#include "v1/primitives/string.grpc.pb.h"
#include "rediska/common/QueueMessage.hpp"
#include "rediska/frontend/RequestManager.hpp"
#include "rediska/frontend/server.hpp"
namespace {
CacheValue CollectionElementToCacheValue(const v1::collections::common::CollectionElement& element) {
if (element.has_integer())
return static_cast<int64_t>(element.integer());
if (element.has_floating_point())
return element.floating_point();
if (element.has_boolean())
return element.boolean();
if (element.has_str_or_obj())
return element.str_or_obj();
return std::string{};
}
} // namespace
using BoolService = v1::primitives::boolean::BoolCacheService::AsyncService;
using IntService = v1::primitives::integer::IntCacheService::AsyncService;
using StringService = v1::primitives::str::StringCacheService::AsyncService;
using ListService = v1::collections::list::ListCacheService::AsyncService;
struct BoolSetRequestManager : RequestManager<
BoolSetRequestManager,
v1::primitives::boolean::BoolSetRequest,
grpc::ServerAsyncResponseWriter<google::protobuf::Empty>,
BoolService,
&BoolService::RequestSet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::BOOLEAN;
msg.key = this->request.id();
msg.operation = OperationId::SET;
msg.arguments = PrimitiveSetArgs{.value = CacheValue{this->request.value()}, .ttl_seconds = 0};
msg.responder = this->TakeResponder();
return msg;
}
};
struct BoolGetRequestManager : RequestManager<
BoolGetRequestManager,
v1::primitives::boolean::BoolGetRequest,
grpc::ServerAsyncResponseWriter<v1::primitives::boolean::BoolGetResponse>,
BoolService,
&BoolService::RequestGet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::BOOLEAN;
msg.key = this->request.id();
msg.operation = OperationId::GET;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
struct BoolDeleteRequestManager : RequestManager<
BoolDeleteRequestManager,
v1::primitives::boolean::BoolDeleteRequest,
grpc::ServerAsyncResponseWriter<v1::primitives::boolean::BoolDeleteResponse>,
BoolService,
&BoolService::RequestDelete> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::BOOLEAN;
msg.key = this->request.id();
msg.operation = OperationId::DELETE;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
struct IntSetRequestManager : RequestManager<
IntSetRequestManager,
v1::primitives::integer::IntSetRequest,
grpc::ServerAsyncResponseWriter<google::protobuf::Empty>,
IntService,
&IntService::RequestSet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::INT;
msg.key = this->request.id();
msg.operation = OperationId::SET;
msg.arguments = PrimitiveSetArgs{.value = CacheValue{this->request.value()}, .ttl_seconds = 0};
msg.responder = this->TakeResponder();
return msg;
}
};
struct IntGetRequestManager : RequestManager<
IntGetRequestManager,
v1::primitives::integer::IntGetRequest,
grpc::ServerAsyncResponseWriter<v1::primitives::integer::IntGetResponse>,
IntService,
&IntService::RequestGet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::INT;
msg.key = this->request.id();
msg.operation = OperationId::GET;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
struct IntDeleteRequestManager : RequestManager<
IntDeleteRequestManager,
v1::primitives::integer::IntDeleteRequest,
grpc::ServerAsyncResponseWriter<v1::primitives::integer::IntDeleteResponse>,
IntService,
&IntService::RequestDelete> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::INT;
msg.key = this->request.id();
msg.operation = OperationId::DELETE;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
struct StringSetRequestManager : RequestManager<
StringSetRequestManager,
v1::primitives::str::StringSetRequest,
grpc::ServerAsyncResponseWriter<google::protobuf::Empty>,
StringService,
&StringService::RequestSet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::STRING;
msg.key = this->request.id();
msg.operation = OperationId::SET;
msg.arguments = PrimitiveSetArgs{.value = CacheValue{this->request.value()}, .ttl_seconds = 0};
msg.responder = this->TakeResponder();
return msg;
}
};
struct StringGetRequestManager : RequestManager<
StringGetRequestManager,
v1::primitives::str::StringGetRequest,
grpc::ServerAsyncResponseWriter<v1::primitives::str::StringGetResponse>,
StringService,
&StringService::RequestGet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::STRING;
msg.key = this->request.id();
msg.operation = OperationId::GET;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
struct StringDeleteRequestManager : RequestManager<
StringDeleteRequestManager,
v1::primitives::str::StringDeleteRequest,
grpc::ServerAsyncResponseWriter<v1::primitives::str::StringDeleteResponse>,
StringService,
&StringService::RequestDelete> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::STRING;
msg.key = this->request.id();
msg.operation = OperationId::DELETE;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
struct ListSetRequestManager : RequestManager<
ListSetRequestManager,
v1::collections::list::ListSetRequest,
grpc::ServerAsyncResponseWriter<v1::collections::list::ListSetResponse>,
ListService,
&ListService::RequestSet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
std::vector<CacheValue> values;
values.reserve(static_cast<size_t>(this->request.elements_size()));
for (const auto& el : this->request.elements())
values.emplace_back(CollectionElementToCacheValue(el));
QueueMessage msg;
msg.type = CacheValueId::ARRAY;
msg.key = this->request.id();
msg.operation = OperationId::SET;
msg.arguments = ListPushManyArgs{.values = std::move(values), .replace_entire_list = true};
msg.responder = this->TakeResponder();
return msg;
}
};
struct ListGetRequestManager : RequestManager<
ListGetRequestManager,
v1::collections::list::ListGetRequest,
grpc::ServerAsyncWriter<v1::collections::list::ListGetResponse>,
ListService,
&ListService::RequestGet> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::ARRAY;
msg.key = this->request.id();
msg.operation = OperationId::GET;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
struct ListDeleteRequestManager : RequestManager<
ListDeleteRequestManager,
v1::collections::list::DeleteRequest,
grpc::ServerAsyncResponseWriter<google::protobuf::Empty>,
ListService,
&ListService::RequestDelete> {
using RequestManager::RequestManager;
QueueMessage BuildMessage() {
QueueMessage msg;
msg.type = CacheValueId::ARRAY;
msg.key = this->request.id();
msg.operation = OperationId::DELETE;
msg.arguments = std::monostate{};
msg.responder = this->TakeResponder();
return msg;
}
};
void RunFrontendServer(const std::string& address, std::function<void(QueueMessage)> callback_enqueue_message) {
grpc::ServerBuilder builder;
BoolService bool_service;
IntService int_service;
StringService string_service;
ListService list_service;
builder.AddListeningPort(address, grpc::InsecureServerCredentials());
builder.RegisterService(&bool_service);
builder.RegisterService(&int_service);
builder.RegisterService(&string_service);
builder.RegisterService(&list_service);
std::unique_ptr<grpc::ServerCompletionQueue> cq = builder.AddCompletionQueue();
std::unique_ptr<grpc::Server> server = builder.BuildAndStart();
BoolSetRequestManager _BoolSetRequestManager(bool_service);
_BoolSetRequestManager.ListenForOne(*cq);
BoolGetRequestManager _BoolGetRequestManager(bool_service);
_BoolGetRequestManager.ListenForOne(*cq);
BoolDeleteRequestManager _BoolDeleteRequestManager(bool_service);
_BoolDeleteRequestManager.ListenForOne(*cq);
IntSetRequestManager _IntSetRequestManager(int_service);
_IntSetRequestManager.ListenForOne(*cq);
IntGetRequestManager _IntGetRequestManager(int_service);
_IntGetRequestManager.ListenForOne(*cq);
IntDeleteRequestManager _IntDeleteRequestManager(int_service);
_IntDeleteRequestManager.ListenForOne(*cq);
StringSetRequestManager _StringSetRequestManager(string_service);
_StringSetRequestManager.ListenForOne(*cq);
StringGetRequestManager _StringGetRequestManager(string_service);
_StringGetRequestManager.ListenForOne(*cq);
StringDeleteRequestManager _StringDeleteRequestManager(string_service);
_StringDeleteRequestManager.ListenForOne(*cq);
ListSetRequestManager _ListSetRequestManager(list_service);
_ListSetRequestManager.ListenForOne(*cq);
ListGetRequestManager _ListGetRequestManager(list_service);
_ListGetRequestManager.ListenForOne(*cq);
ListDeleteRequestManager _ListDeleteRequestManager(list_service);
_ListDeleteRequestManager.ListenForOne(*cq);
void* tag;
bool ok;
while (cq->Next(&tag, &ok)) {
if (!tag)
continue;
// taking ownership as per the manager api
std::unique_ptr<EventVariant> event(static_cast<EventVariant*>(tag));
if (!ok) {
// idk could happen, but we do take ownership of the event anyway
(void)event;
continue;
}
if (auto* req = std::get_if<RequestEvent>(event.get())) {
auto& manager = req->manager;
if (auto msg = manager.ConsumeMessage(); msg.has_value()) {
callback_enqueue_message(std::move(*msg));
}
manager.ListenForOne(*cq);
} else if (auto* fin = std::get_if<FinishEvent>(event.get())) {
// we just destroy
std::cout << "Finished a request\n" << std::flush;
(void)fin;
}
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <grpcpp/grpcpp.h>
#include <grpcpp/support/async_stream.h>
#include <memory>
#include <optional>
#include "rediska/common/QueueMessage.hpp"
// One global context kept alive as long as the server lives.
inline grpc::ServerContext& GlobalServerContext() {
static grpc::ServerContext ctx;
return ctx;
}
// The thing recieved from the completion queue in case of a request.
struct BaseRequestManager {
virtual void ListenForOne(grpc::ServerCompletionQueue& cq) = 0;
virtual std::optional<QueueMessage> ConsumeMessage() = 0;
virtual ~BaseRequestManager() = default;
};
// This is basically storage for an incoming request.
template <typename Derived, typename RequestT, typename ResponderT, typename ServiceT, auto RequestMethod>
class RequestManager : public BaseRequestManager {
protected:
RequestT request;
std::unique_ptr<ResponderT> responder;
ServiceT& service;
std::unique_ptr<grpc::internal::ServerAsyncStreamingInterface> TakeResponder() {
return std::unique_ptr<grpc::internal::ServerAsyncStreamingInterface>(std::move(responder));
}
public:
explicit RequestManager(ServiceT& svc) : service(svc) { }
// Sets up listening for a new request.
// You must ensure this object lives until the request is consumed, or grpc will be unhappy.
// The tag you recieve from the queue is a EventVariant* that you must take ownership of.
void ListenForOne(grpc::ServerCompletionQueue& cq) override {
request = RequestT{};
// FIXME: just leak this stupid shit for now idk how to fix I'm done with this stupid language
auto* ctx = new grpc::ServerContext();
responder = std::make_unique<ResponderT>(ctx);
(service.*RequestMethod)(ctx, &request, responder.get(), &cq, &cq, std::make_unique<EventVariant>(RequestEvent{*this}).release());
}
std::optional<QueueMessage> ConsumeMessage() override {
QueueMessage msg = static_cast<Derived*>(this)->BuildMessage();
return msg;
}
};

View File

@@ -0,0 +1,11 @@
#pragma once
#include <functional>
#include <string>
#include "rediska/common/QueueMessage.hpp"
// Starts the gRPC frontend server on the given address and blocks.
// Each incoming request is transformed into a QueueMessage with its responder
// moved inside; the provided callback decides when/how to finish it.
void RunFrontendServer(const std::string& address,
std::function<void(QueueMessage)> on_request);

5
rediska/main.cpp Normal file
View File

@@ -0,0 +1,5 @@
#include "rediska/worker/FrontendWorker.cpp"
int main() {
return run_frontend_server();
}

View File

@@ -0,0 +1,157 @@
#include "rediska/worker/AsyncWorker.hpp"
#include <google/protobuf/empty.pb.h>
#include "v1/primitives/bool.grpc.pb.h"
#include "v1/primitives/bool.pb.h"
#include "v1/primitives/int.grpc.pb.h"
#include "v1/primitives/int.pb.h"
#include "v1/primitives/string.grpc.pb.h"
#include "v1/primitives/string.pb.h"
#include "v1/collections/list.grpc.pb.h"
#include "v1/collections/list.pb.h"
namespace rediska::worker {
AsyncWorker::AsyncWorker(size_t num_workers, size_t cache_capacity)
: num_workers_(num_workers) {
cache_config_.maxCapacity = cache_capacity;
cache_config_.ttl = 0;
cache_config_.resetTTLOnAccess = true;
InitializeCache();
}
AsyncWorker::~AsyncWorker() {
Stop();
}
void AsyncWorker::Start() {
if (running_) return;
running_ = true;
workers_.reserve(num_workers_);
for (size_t i = 0; i < num_workers_; ++i) {
workers_.emplace_back(&AsyncWorker::WorkerLoop, this);
}
}
void AsyncWorker::Stop() {
if (!running_) return;
running_ = false;
queue_cv_.notify_all();
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
}
void AsyncWorker::Enqueue(QueueMessage msg) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
message_queue_.push(std::move(msg));
}
queue_cv_.notify_one();
}
void AsyncWorker::WorkerLoop() {
while (running_) {
QueueMessage msg;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait(lock, [this] { return !message_queue_.empty() || !running_; });
if (!running_) break;
msg = std::move(message_queue_.front());
message_queue_.pop();
}
ProcessMessage(msg);
}
}
void AsyncWorker::ProcessMessage(QueueMessage& msg) {
if (!msg.responder) return;
try {
switch (msg.operation) {
case OperationId::SET: {
if (msg.type == CacheValueId::ARRAY) {
v1::collections::list::ListSetResponse response;
msg.respond<v1::collections::list::ListSetResponse>(response);
} else {
google::protobuf::Empty response;
msg.respond<google::protobuf::Empty>(response);
}
break;
}
case OperationId::GET: {
cache_->get(std::string(msg.key));
// заглушка, пока кэш не интегрирован полностью TODO
if (msg.type == CacheValueId::BOOLEAN) {
v1::primitives::boolean::BoolGetResponse response;
response.set_value(false);
msg.respond<v1::primitives::boolean::BoolGetResponse>(response);
} else if (msg.type == CacheValueId::INT) {
v1::primitives::integer::IntGetResponse response;
response.set_value(0);
msg.respond<v1::primitives::integer::IntGetResponse>(response);
} else if (msg.type == CacheValueId::STRING) {
v1::primitives::str::StringGetResponse response;
response.set_value("cached_value");
msg.respond<v1::primitives::str::StringGetResponse>(response);
} else if (msg.type == CacheValueId::ARRAY) {
v1::collections::list::ListGetResponse response;
msg.respond<v1::collections::list::ListGetResponse>(response);
}
break;
}
case OperationId::DELETE: {
if (msg.type == CacheValueId::BOOLEAN) {
v1::primitives::boolean::BoolDeleteResponse response;
response.set_removed_value(false);
msg.respond<v1::primitives::boolean::BoolDeleteResponse>(response);
} else if (msg.type == CacheValueId::INT) {
v1::primitives::integer::IntDeleteResponse response;
response.set_removed_value(0);
msg.respond<v1::primitives::integer::IntDeleteResponse>(response);
} else if (msg.type == CacheValueId::STRING) {
v1::primitives::str::StringDeleteResponse response;
response.set_removed_value("");
msg.respond<v1::primitives::str::StringDeleteResponse>(response);
} else if (msg.type == CacheValueId::ARRAY) {
google::protobuf::Empty response;
msg.respond<google::protobuf::Empty>(response);
}
break;
}
case OperationId::LIST_PUSH_BACK:
case OperationId::LIST_POP_BACK:
case OperationId::LIST_INSERT:
case OperationId::LIST_ERASE: {
google::protobuf::Empty response;
msg.respond<google::protobuf::Empty>(response);
break;
}
default:
break;
}
} catch (const std::exception& e) {
std::cout << "Error processing message: " << e.what() << std::endl;
}
}
void AsyncWorker::InitializeCache() {
auto callback = [this](auto result) {
// TODO
};
cache_ = std::make_unique<cache::LRU>(cache_config_, callback);
}
} // namespace rediska::worker

View File

@@ -0,0 +1,42 @@
#pragma once
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
#include <grpcpp/grpcpp.h>
#include "rediska/cache/lru/LRU.hpp"
#include "rediska/common/QueueMessage.hpp"
#include "rediska/common/types.hpp"
namespace rediska::worker {
class AsyncWorker {
public:
explicit AsyncWorker(size_t num_workers = 4, size_t cache_capacity = 1000);
~AsyncWorker();
void Start();
void Stop();
void Enqueue(QueueMessage msg);
bool IsRunning() const { return running_; }
private:
void WorkerLoop();
void ProcessMessage(QueueMessage& msg);
void InitializeCache();
std::vector<std::thread> workers_;
std::queue<QueueMessage> message_queue_;
std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::atomic<bool> running_{false};
size_t num_workers_;
std::unique_ptr<cache::LRU> cache_;
cache::LRUConfig cache_config_;
};
} // namespace rediska::worker

View File

@@ -0,0 +1,14 @@
#include "rediska/worker/AsyncWorker.hpp"
#include "rediska/frontend/server.hpp"
#include <iostream>
int run_async_server() {
rediska::worker::AsyncWorker worker(4, 1000);
worker.Start();
RunFrontendServer("0.0.0.0:50051", [&worker](QueueMessage msg) {
worker.Enqueue(std::move(msg));
});
return 0;
}

View File

@@ -0,0 +1,15 @@
add_library(worker STATIC
tmp.cpp
AsyncWorker.cpp
AsyncWorker.hpp
AsyncWorkerMain.cpp
FrontendWorker.cpp
CacheWorker.cpp
)
target_link_libraries(worker PUBLIC
common
frontend
cache
data-structures
)

View File

@@ -0,0 +1,183 @@
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <unordered_map>
#include <shared_mutex>
#include <chrono>
#include <grpcpp/grpcpp.h>
#include "google/protobuf/empty.pb.h"
#include "v1/primitives/bool.grpc.pb.h"
#include "v1/primitives/bool.pb.h"
#include "v1/primitives/int.grpc.pb.h"
#include "v1/primitives/int.pb.h"
#include "v1/primitives/string.grpc.pb.h"
#include "v1/primitives/string.pb.h"
#include "v1/collections/list.grpc.pb.h"
#include "v1/collections/list.pb.h"
#include "rediska/common/QueueMessage.hpp"
#include "rediska/frontend/RequestManager.hpp"
#include "rediska/frontend/server.hpp"
#include "rediska/cache/lru/LRU.hpp"
#include "rediska/data-structures/impl/ListDataStructure.hpp"
namespace {
class CacheWorker {
public:
CacheWorker(size_t cache_capacity = 1000) {
cache_config_.maxCapacity = cache_capacity;
cache_config_.ttl = 0;
cache_config_.resetTTLOnAccess = true;
auto callback = [this](auto result) {
};
cache_ = std::make_unique<cache::LRU>(cache_config_, callback);
}
void Enqueue(QueueMessage msg) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(msg));
cv_.notify_one();
}
void Run() {
for (;;) {
QueueMessage msg;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [&] { return !queue_.empty(); });
msg = std::move(queue_.front());
queue_.pop();
}
std::cout << "[cache-worker] key=" << msg.key
<< " type=" << static_cast<int>(msg.type)
<< " op=" << static_cast<int>(msg.operation) << std::endl;
if (!msg.responder) continue;
try {
switch (msg.operation) {
case OperationId::SET: {
if (msg.type == CacheValueId::ARRAY) {
auto list = std::make_shared<ListDataStructure>();
if (std::holds_alternative<ListPushManyArgs>(msg.arguments)) {
auto& args = std::get<ListPushManyArgs>(msg.arguments);
for (auto& value : args.values) {
list->handle(OperationId::LIST_PUSH_BACK, DSValue{});
}
}
cache_->set(std::string(msg.key), CacheValue{list}, 0);
v1::collections::list::ListSetResponse response;
msg.respond<v1::collections::list::ListSetResponse>(response);
} else {
CacheValue value;
if (msg.arguments.index() != 0) {
auto& args = std::get<PrimitiveSetArgs>(msg.arguments);
value = args.value;
}
cache_->set(std::string(msg.key), std::move(value), 0);
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
}
break;
}
case OperationId::GET: {
cache_->get(std::string(msg.key));
// я не нашел пока полный callback, тут заглушки (TODO)
if (msg.type == CacheValueId::BOOLEAN) {
v1::primitives::boolean::BoolGetResponse response;
response.set_value(true);
msg.respond<v1::primitives::boolean::BoolGetResponse>(response);
} else if (msg.type == CacheValueId::INT) {
v1::primitives::integer::IntGetResponse response;
response.set_value(42);
msg.respond<v1::primitives::integer::IntGetResponse>(response);
} else if (msg.type == CacheValueId::STRING) {
v1::primitives::str::StringGetResponse response;
response.set_value("cached_string_value");
msg.respond<v1::primitives::str::StringGetResponse>(response);
} else if (msg.type == CacheValueId::ARRAY) {
v1::collections::list::ListGetResponse response;
msg.respond<v1::collections::list::ListGetResponse>(response);
}
break;
}
case OperationId::DELETE: {
cache_->applyTo(std::string(msg.key), OperationId::DELETE, std::move(msg.arguments));
if (msg.type == CacheValueId::BOOLEAN) {
v1::primitives::boolean::BoolDeleteResponse response;
response.set_removed_value(true);
msg.respond<v1::primitives::boolean::BoolDeleteResponse>(response);
} else if (msg.type == CacheValueId::INT) {
v1::primitives::integer::IntDeleteResponse response;
response.set_removed_value(42);
msg.respond<v1::primitives::integer::IntDeleteResponse>(response);
} else if (msg.type == CacheValueId::STRING) {
v1::primitives::str::StringDeleteResponse response;
response.set_removed_value("deleted_string");
msg.respond<v1::primitives::str::StringDeleteResponse>(response);
} else if (msg.type == CacheValueId::ARRAY) {
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
}
break;
}
case OperationId::LIST_PUSH_BACK: {
cache_->applyTo(std::string(msg.key), OperationId::LIST_PUSH_BACK, std::move(msg.arguments));
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
break;
}
case OperationId::LIST_POP_BACK: {
cache_->applyTo(std::string(msg.key), OperationId::LIST_POP_BACK, std::move(msg.arguments));
v1::collections::list::PopBackResponse response;
msg.respond<v1::collections::list::PopBackResponse>(response);
break;
}
case OperationId::LIST_INSERT: {
cache_->applyTo(std::string(msg.key), OperationId::LIST_INSERT, std::move(msg.arguments));
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
break;
}
case OperationId::LIST_ERASE: {
cache_->applyTo(std::string(msg.key), OperationId::LIST_ERASE, std::move(msg.arguments));
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
break;
}
default:
break;
}
} catch (const std::exception& e) {
std::cout << "Error processing message: " << e.what() << std::endl;
}
}
}
private:
std::queue<QueueMessage> queue_;
std::mutex mtx_;
std::condition_variable cv_;
std::unique_ptr<cache::LRU> cache_;
cache::LRUConfig cache_config_;
};
} // namespace
int run_cache_server() {
CacheWorker worker(1000);
std::thread t([&] { worker.Run(); });
RunFrontendServer("0.0.0.0:50051", [&](QueueMessage msg) {
worker.Enqueue(std::move(msg));
});
t.join();
return 0;
}

View File

@@ -0,0 +1,123 @@
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <grpcpp/grpcpp.h>
#include "google/protobuf/empty.pb.h"
#include "v1/primitives/bool.grpc.pb.h"
#include "v1/primitives/int.grpc.pb.h"
#include "v1/primitives/string.grpc.pb.h"
#include "v1/collections/list.grpc.pb.h"
#include "rediska/common/QueueMessage.hpp"
#include "rediska/frontend/RequestManager.hpp"
#include "rediska/frontend/server.hpp"
#include "rediska/cache/lru/LRU.hpp"
namespace {
class FrontendWorker {
public:
void Enqueue(QueueMessage msg) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(msg));
cv_.notify_one();
}
void Run() {
for (;;) {
QueueMessage msg;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [&] { return !queue_.empty(); });
msg = std::move(queue_.front());
queue_.pop();
}
std::cout << "[worker] key=" << msg.key
<< " type=" << static_cast<int>(msg.type)
<< " op=" << static_cast<int>(msg.operation) << std::endl;
if (!msg.responder) continue;
try {
switch (msg.operation) {
case OperationId::SET: {
if (msg.type == CacheValueId::ARRAY) {
v1::collections::list::ListSetResponse response;
msg.respond<v1::collections::list::ListSetResponse>(response);
} else {
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
}
break;
}
case OperationId::GET: {
if (msg.type == CacheValueId::BOOLEAN) {
v1::primitives::boolean::BoolGetResponse response;
response.set_value(false);
msg.respond<v1::primitives::boolean::BoolGetResponse>(response);
} else if (msg.type == CacheValueId::INT) {
v1::primitives::integer::IntGetResponse response;
response.set_value(0);
msg.respond<v1::primitives::integer::IntGetResponse>(response);
} else if (msg.type == CacheValueId::STRING) {
v1::primitives::str::StringGetResponse response;
response.set_value("cached_value");
msg.respond<v1::primitives::str::StringGetResponse>(response);
} else if (msg.type == CacheValueId::ARRAY) {
v1::collections::list::ListGetResponse response;
msg.respond<v1::collections::list::ListGetResponse>(response);
}
break;
}
case OperationId::DELETE: {
if (msg.type == CacheValueId::BOOLEAN) {
v1::primitives::boolean::BoolDeleteResponse response;
response.set_removed_value(false);
msg.respond<v1::primitives::boolean::BoolDeleteResponse>(response);
} else if (msg.type == CacheValueId::INT) {
v1::primitives::integer::IntDeleteResponse response;
response.set_removed_value(0);
msg.respond<v1::primitives::integer::IntDeleteResponse>(response);
} else if (msg.type == CacheValueId::STRING) {
v1::primitives::str::StringDeleteResponse response;
response.set_removed_value("");
msg.respond<v1::primitives::str::StringDeleteResponse>(response);
} else if (msg.type == CacheValueId::ARRAY) {
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
}
break;
}
case OperationId::LIST_PUSH_BACK:
case OperationId::LIST_POP_BACK:
case OperationId::LIST_INSERT:
case OperationId::LIST_ERASE: {
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
break;
}
default:
break;
}
} catch (const std::exception& e) {
std::cout << "Error processing message: " << e.what() << std::endl;
}
}
}
private:
std::queue<QueueMessage> queue_;
std::mutex mtx_;
std::condition_variable cv_;
};
} // namespace
int run_frontend_server() {
FrontendWorker worker;
std::thread t([&] { worker.Run(); });
RunFrontendServer("0.0.0.0:50051", [&](QueueMessage msg) {
worker.Enqueue(std::move(msg));
});
t.join();
return 0;
}

86
rediska/worker/tmp.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <grpcpp/grpcpp.h>
#include "google/protobuf/empty.pb.h"
#include "v1/primitives/bool.grpc.pb.h"
#include "v1/primitives/int.grpc.pb.h"
#include "v1/primitives/string.grpc.pb.h"
#include "v1/collections/list.grpc.pb.h"
#include "rediska/common/QueueMessage.hpp"
#include "rediska/frontend/RequestManager.hpp"
#include "rediska/frontend/server.hpp"
namespace {
class PrintWorker {
public:
void Enqueue(QueueMessage msg) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(msg));
cv_.notify_one();
}
void Run() {
for (;;) {
QueueMessage msg;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [&] { return !queue_.empty(); });
msg = std::move(queue_.front());
queue_.pop();
}
std::cout << "[worker] key=" << msg.key
<< " type=" << static_cast<int>(msg.type)
<< " op=" << static_cast<int>(msg.operation) << std::endl;
if (!msg.responder) continue;
switch (msg.operation) {
case OperationId::SET:
case OperationId::DELETE: {
msg.respond<google::protobuf::Empty>(google::protobuf::Empty{});
break;
}
case OperationId::GET: {
if (msg.type == CacheValueId::BOOLEAN) {
v1::primitives::boolean::BoolGetResponse r; r.set_value(false);
msg.respond<v1::primitives::boolean::BoolGetResponse>(r);
} else if (msg.type == CacheValueId::INT) {
v1::primitives::integer::IntGetResponse r; r.set_value(0);
msg.respond<v1::primitives::integer::IntGetResponse>(r);
} else if (msg.type == CacheValueId::STRING) {
v1::primitives::str::StringGetResponse r; r.set_value("test");
msg.respond<v1::primitives::str::StringGetResponse>(r);
} else if (msg.type == CacheValueId::ARRAY) {
v1::collections::list::ListGetResponse r;
msg.respond<v1::collections::list::ListGetResponse>(r);
}
break;
}
default:
break;
}
}
}
private:
std::queue<QueueMessage> queue_;
std::mutex mtx_;
std::condition_variable cv_;
};
} // namespace
int run_print_server() {
PrintWorker worker;
std::thread t([&] { worker.Run(); });
RunFrontendServer("0.0.0.0:50051", [&](QueueMessage msg) {
worker.Enqueue(std::move(msg));
});
t.join();
return 0;
}

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Quick smoke sender for the temp print server.
Requires python -m pip install grpcio grpcio-tools.
"""
import subprocess
import sys
import tempfile
from pathlib import Path
import grpc
def generate_stubs(proto_root: Path, out_dir: Path):
protos = [
"v1/primitives/bool.proto",
"v1/primitives/int.proto",
"v1/primitives/string.proto",
"v1/collections/list.proto",
"v1/collections/common.proto",
"v1/collections/element_kind.proto",
]
args = [
sys.executable,
"-m",
"grpc_tools.protoc",
f"-I{proto_root}",
f"--python_out={out_dir}",
f"--grpc_python_out={out_dir}",
] + protos
subprocess.check_call(args, cwd=proto_root)
def main():
repo_root = Path(__file__).resolve().parent.parent
proto_root = repo_root / "proto"
with tempfile.TemporaryDirectory() as td:
out_dir = Path(td)
generate_stubs(proto_root, out_dir)
sys.path.insert(0, str(out_dir))
from v1.primitives import bool_pb2_grpc as bool_grpc, bool_pb2
from v1.primitives import int_pb2_grpc as int_grpc, int_pb2
from v1.primitives import string_pb2_grpc as str_grpc, string_pb2
from v1.collections import list_pb2_grpc as list_grpc, list_pb2
channel = grpc.insecure_channel("localhost:50051")
bool_stub = bool_grpc.BoolCacheServiceStub(channel)
int_stub = int_grpc.IntCacheServiceStub(channel)
str_stub = str_grpc.StringCacheServiceStub(channel)
list_stub = list_grpc.ListCacheServiceStub(channel)
print("Sending Bool Set")
bool_stub.Set(bool_pb2.BoolSetRequest(id="b1", value=True))
print("Sending Int Set/Get/Delete")
int_stub.Set(int_pb2.IntSetRequest(id="i1", value=42))
int_resp = int_stub.Get(int_pb2.IntGetRequest(id="i1"))
print("Int get response:", int_resp.value)
int_stub.Delete(int_pb2.IntDeleteRequest(id="i1"))
print("Sending String Set")
str_stub.Set(string_pb2.StringSetRequest(id="s1", value="hello"))
print("Sending List Set")
list_stub.Set(list_pb2.ListSetRequest(id="l1", elements=[]))
print("Done. Check server stdout for printed messages.")
if __name__ == "__main__":
main()

171
tests/BasicTests.cpp Normal file
View File

@@ -0,0 +1,171 @@
#include <gtest/gtest.h>
#include <grpcpp/grpcpp.h>
#include <memory>
#include <thread>
#include <chrono>
#include "v1/primitives/bool.grpc.pb.h"
#include "v1/primitives/int.grpc.pb.h"
#include "v1/primitives/string.grpc.pb.h"
#include "v1/collections/list.grpc.pb.h"
class RediskaBasicTest : public ::testing::Test {
protected:
void SetUp() override {
channel_ = grpc::CreateChannel(grpc::InsecureChannel("localhost:50051"));
bool_stub_ = std::make_unique<v1::primitives::boolean::BoolCacheService::Stub>(channel_);
int_stub_ = std::make_unique<v1::primitives::integer::IntCacheService::Stub>(channel_);
string_stub_ = std::make_unique<v1::primitives::str::StringCacheService::Stub>(channel_);
list_stub_ = std::make_unique<v1::collections::list::ListCacheService::Stub>(channel_);
}
void TearDown() override {
channel_->Shutdown();
}
std::shared_ptr<grpc::Channel> channel_;
std::unique_ptr<v1::primitives::boolean::BoolCacheService::Stub> bool_stub_;
std::unique_ptr<v1::primitives::integer::IntCacheService::Stub> int_stub_;
std::unique_ptr<v1::primitives::str::StringCacheService::Stub> string_stub_;
std::unique_ptr<v1::collections::list::ListCacheService::Stub> list_stub_;
};
TEST_F(RediskaBasicTest, BooleanOperations) {
// Test SET operation
bool_stub_->Set(v1::primitives::boolean::BoolSetRequest(id="test_bool", value=true));
// Test GET operation
auto response = bool_stub_->Get(v1::primitives::BoolGetRequest(id="test_bool"));
EXPECT_TRUE(response.value());
// Test DELETE operation
auto delete_response = bool_stub_->Delete(v1::primitives::BoolDeleteRequest(id="test_bool"));
EXPECT_TRUE(delete_response.removed_value());
}
TEST_F(RediskaBasicTest, IntegerOperations) {
// Test SET operation
int_stub_->Set(v1::primitives::integer::IntSetRequest(id="test_int", value=42));
// Test GET operation
auto response = int_stub_->Get(v1::primitives::integer::IntGetRequest(id="test_int"));
EXPECT_EQ(response.value(), 42);
// Test DELETE operation
auto delete_response = int_stub_->Delete(v1::primitives::integer::IntDeleteRequest(id="test_int"));
EXPECT_EQ(delete_response.removed_value(), 42);
}
TEST_F(RediskaBasicTest, StringOperations) {
// Test SET operation
string_stub_->Set(v1::primitives::str::StringSetRequest(id="test_string", value="hello"));
// Test GET operation
auto response = string_stub_->Get(v1::primitives::str::StringGetRequest(id="test_string"));
EXPECT_EQ(response.value(), "hello");
// Test DELETE operation
auto delete_response = string_stub_->Delete(v1::primitives::str::StringDeleteRequest(id="test_string"));
EXPECT_EQ(delete_response.removed_value(), "hello");
}
TEST_FediskaBasicTest, ListOperations) {
// Test CREATE operation
auto create_response = list_stub_->Create(v1::collections::list::ListCreateRequest(
element_kind=v1::collections::common::ElementKind::INT,
ttl_seconds=3600
));
std::string list_id = create_response.id;
// Test SET operation
std::vector<v1::collections::common::CollectionElement> elements;
elements.push_back(v1::collections::common::CollectionElement(integer=1));
elements.push_back(v1::collections::common::CollectionElement(integer=2));
elements.push_back(v1::collections::common::CollectionElement(integer=3));
list_stub_->Set(v1::collections::list::ListSetRequest(id=list_id, elements=elements));
// Test GET operation (streaming)
auto get_response = list_stub_->Get(v1::collections::list::ListGetRequest(id=list_id));
int count = 0;
for (auto element : get_response) {
EXPECT_EQ(element.integer(), count + 1);
count++;
}
EXPECT_EQ(count, 3);
// Test LENGTH operation
auto length_response = list_stub_->Length(v1::collections::list::ListLengthRequest(id=list_id));
EXPECT_EQ(length_response.length(), 3);
// Test DELETE operation
list_stub_->Delete(v1::collections::DeleteRequest(id=list_id));
}
TEST_FediskaBasicTest, ConcurrentOperations) {
const int num_threads = 10;
const int operations_per_thread = 100;
std::vector<std::thread> threads;
std::vector<std::exception_ptr<std::exception>> exceptions;
// Запускаем несколько потоков одновременно
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&, i, operations_per_thread]() {
try {
for (int j = 0; j < operations_per_thread; ++j) {
std::string key = "test_" + std::to_string(i * operations_per_thread + j);
if (j % 3 == 0) {
bool_stub_->Set(v1::primitives::boolean::BoolSetRequest(key, true));
} else if (j % 3 == 1) {
int_stub_->Set(v1::primitives::integer::IntSetRequest(key, j));
} else {
string_stub_->Set(v1::primitives::str::StringSetRequest(key, "value_" + std::to_string(j)));
}
}
} catch (const std::exception& e) {
exceptions.emplace_back(std::current_exception());
}
});
}
// Ждем завершения всех потоков
for (auto& thread : threads) {
if (thread.joinable()) {
thread.join();
}
}
// Проверяем, что не было исключений
EXPECT_TRUE(exceptions.empty());
}
TEST_FediskaBasicTest, ServerStability) {
// Тест на стабильность сервера при множественных запросов
const int num_requests = 1000;
for (int i = 0; i < num_requests; ++i) {
std::string key = "stability_test_" + std::to_string(i);
if (i % 4 == 0) {
bool_stub_->Set(v1::primitives::boolean::BoolSetRequest(key, true));
} else if (i % 4 == 1) {
int_stub_->Set(v1::primitives::integer::IntSetRequest(key, i));
} else if (i % 4 == 2) {
string_stub_->Set(v1::primitives::str::StringSetRequest(key, "value_" + std::to_string(i)));
} else {
list_stub_->Set(v1::collections::ListSetRequest(
id="list_" + std::to_string(i),
elements={
v1::collections::common::CollectionElement(integer=i)
}
));
}
if (i % 10 == 0) {
auto response = bool_stub_->Get(v1::primitives::boolean::BoolGetRequest(key));
EXPECT_TRUE(response.value());
}
}
}

39
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,39 @@
add_executable(unit_tests
main.cpp
)
target_link_libraries(unit_tests
PRIVATE
worker
cache
frontend
doctest::doctest
)
list(APPEND CMAKE_MODULE_PATH "${doctest_SOURCE_DIR}/scripts/cmake")
include(doctest)
list(POP_BACK CMAKE_MODULE_PATH)
doctest_discover_tests(unit_tests
TEST_PREFIX ""
TEST_SUFFIX ""
)
find_program(CPPCHECK_EXECUTABLE cppcheck)
if(CPPCHECK_EXECUTABLE)
add_test(
NAME static_analysis
COMMAND ${CPPCHECK_EXECUTABLE}
--enable=all
--std=c++23
--error-exitcode=1
--inline-suppr
${CMAKE_SOURCE_DIR}/rediska
)
set_tests_properties(static_analysis PROPERTIES
LABELS "static"
TIMEOUT 120
)
else()
message(WARNING "Cppcheck not found — skipping static analysis test")
endif()

39
tests/README.md Normal file
View File

@@ -0,0 +1,39 @@
# Creating a test group
1. **Create a new test file** \
Place it under `/tests/frontend/` or `/tests/backend/`, following the [naming convention](#naming-convention) below.
2. **Include the testing framework** \
Add the following at the top of the file:
```C++
#include <doctest/doctest.h>
```
3. **Register the file in the build system** \
Add the new test file to `/tests/CMakeLists.txt` so its compiled and executed as part of the test suite.
4. **Start writing tests!** \
See [doctest docs](https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md) for more details.
# Naming convention
File name should match against the following pattern:
`<module>_<feature>_<test_type>_tests.cpp`
Where:
- `<module>` - the component or subsystem under test.
- Backend modules: `cache` | `worker`
- Frontend modules: `work in progress`
- `<feature>` - a concise, kebab-case description of the specific functionality.
- `<test_type>` - one of the following:
- `unit`
- `integration`
- `e2e`
# Examples
| Path | Description |
| :--------------------------------------------- | :------------------------------------------------------------- |
| `backend/cache_lru_unit_tests.cpp` | Unit tests for LRU eviction logic in the cache module |
| `backend/worker_handle-request_unit_tests.cpp` | Unit tests for the request-handling logic in the worker module |

0
tests/backend/.keep Normal file
View File

0
tests/frontend/.keep Normal file
View File

2
tests/main.cpp Normal file
View File

@@ -0,0 +1,2 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>