Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
PrivMX Endpoint.
Copyright © 2026 Simplito sp. z o.o.

This file is part of the PrivMX Platform (https://privmx.dev).
This software is Licensed under the PrivMX Free License.

See the License for the specific language governing permissions and
limitations under the License.
*/

#ifndef _PRIVMXLIB_ENDPOINT_STORE_CACHE_BACKEND_IN_MEMORY_HPP_
#define _PRIVMXLIB_ENDPOINT_STORE_CACHE_BACKEND_IN_MEMORY_HPP_

#include <map>
#include <memory>
#include <optional>
#include <string>

#include <privmx/endpoint/store/cache/CacheBackendInterface.hpp>

namespace privmx {
namespace endpoint {
namespace store {

class CacheBackendInMemory : public CacheBackendInterface {
public:
std::optional<core::Buffer> get(const std::string& key) override;
void put(const std::string& key, const core::Buffer& value) override;
void del(const std::string& key) override;
void clear() override;
std::unique_ptr<Iterator> seek(const std::string& prefix) override;

private:
std::map<std::string, core::Buffer> _store;
};

} // namespace store
} // namespace endpoint
} // namespace privmx

#endif // _PRIVMXLIB_ENDPOINT_STORE_CACHE_BACKEND_IN_MEMORY_HPP_
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
PrivMX Endpoint.
Copyright © 2026 Simplito sp. z o.o.

This file is part of the PrivMX Platform (https://privmx.dev).
This software is Licensed under the PrivMX Free License.

See the License for the specific language governing permissions and
limitations under the License.
*/

#ifndef _PRIVMXLIB_ENDPOINT_STORE_CACHE_SLRU_BACKED_CACHE_HPP_
#define _PRIVMXLIB_ENDPOINT_STORE_CACHE_SLRU_BACKED_CACHE_HPP_

#include <cstdint>
#include <optional>
#include <string>
#include <tuple>

#include <privmx/endpoint/store/cache/CacheBackendInterface.hpp>
#include <privmx/endpoint/store/cache/CacheInterface.hpp>
#include <privmx/endpoint/store/cache/SlruPolicy.hpp>

namespace privmx {
namespace endpoint {
namespace store {

/**
* CacheInterface implementation backed by a persistent CacheBackendInterface,
* with a configurable size cap and SLRU eviction.
*
* Both cached values and SLRU metadata are stored in the backend, so eviction
* order and segment membership survive process restarts. On construction the
* SLRU state is restored by reading the metadata from the backend.
*
* Internal backend key layout (invisible to callers):
* "d;" + user_key → raw cached value
* "slru;" + user_key → 17-byte metadata: [seq: uint64_le][size: uint64_le][segment: uint8]
*
* Both prefixes use ';' which never appears in UUIDs, Base58, or Base64,
* so data keys and metadata keys never collide with each other or with external entries.
*/
class SlruBackedCache : public CacheInterface {
public:
static constexpr double DEFAULT_PROTECTED_RATIO = 0.8;

explicit SlruBackedCache(CacheBackendInterface& backend,
size_t maxBytes = CacheBackendInterface::DEFAULT_MAX_BYTES,
double protectedRatio = DEFAULT_PROTECTED_RATIO);

std::optional<core::Buffer> get(const std::string& key) override;
void put(const std::string& key, const core::Buffer& data) override;
void del(const std::string& key) override;

private:
static constexpr const char* DATA_PREFIX = "d;";
static constexpr const char* META_PREFIX = "slru;";

static std::string dataKey(const std::string& key);
static std::string metaKey(const std::string& key);
static core::Buffer encodeMeta(uint64_t seq, uint64_t size, Segment segment);
static std::tuple<uint64_t, uint64_t, Segment> decodeMeta(const core::Buffer& raw);

void loadFromBackend();
void persistMeta(const std::string& key, size_t size, Segment segment);
void evictUntilFit(size_t incomingSize);

CacheBackendInterface& _backend;
size_t _maxBytes;
uint64_t _seq = 0;
SlruPolicy _slru;
};

} // namespace store
} // namespace endpoint
} // namespace privmx

#endif // _PRIVMXLIB_ENDPOINT_STORE_CACHE_SLRU_BACKED_CACHE_HPP_
104 changes: 104 additions & 0 deletions endpoint/store/include/privmx/endpoint/store/cache/SlruPolicy.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
PrivMX Endpoint.
Copyright © 2026 Simplito sp. z o.o.

This file is part of the PrivMX Platform (https://privmx.dev).
This software is Licensed under the PrivMX Free License.

See the License for the specific language governing permissions and
limitations under the License.
*/

#ifndef _PRIVMXLIB_ENDPOINT_STORE_CACHE_SLRU_POLICY_HPP_
#define _PRIVMXLIB_ENDPOINT_STORE_CACHE_SLRU_POLICY_HPP_

#include <cstdint>
#include <list>
#include <optional>
#include <string>
#include <unordered_map>

namespace privmx {
namespace endpoint {
namespace store {

enum class Segment : uint8_t { Probationary = 0, Protected = 1 };

/**
* In-memory Segmented LRU (SLRU) eviction policy tracker.
*
* Entries live in one of two segments:
* - Probationary: newly inserted or demoted entries; eviction target.
* - Protected: entries promoted after a second access; shielded from eviction.
*
* On touch:
* - New entry → head of Probationary
* - Probationary hit → promoted to head of Protected;
* if Protected overflows its budget, its LRU tail
* is demoted to head of Probationary
* - Protected hit → moved to head of Protected
*
* evictOne() always targets the tail of Probationary; if Probationary is
* empty it falls back to the tail of Protected.
*
* All operations are O(1).
*/
class SlruPolicy {
public:
struct Entry {
std::string key;
size_t size;
};

/**
* @param maxBytes Total byte budget for both segments combined.
* @param protectedRatio Fraction of maxBytes reserved for the Protected segment [0, 1].
*/
explicit SlruPolicy(size_t maxBytes, double protectedRatio = 0.8);

/**
* Marks @p key as most-recently-used, applying promotion logic.
* Returns the segment the key ended up in.
*/
Segment touch(const std::string& key, size_t size);

/**
* Inserts @p key directly into @p segment without triggering promotion.
* Used when restoring persisted state on startup.
*/
void restore(const std::string& key, size_t size, Segment segment);

/**
* Removes and returns the eviction candidate (tail of Probationary,
* or tail of Protected if Probationary is empty).
* Returns std::nullopt if both segments are empty.
*/
std::optional<Entry> evictOne();

void remove(const std::string& key);
bool contains(const std::string& key) const;
size_t totalSize() const;
bool empty() const;

private:
struct SegmentState {
std::list<Entry> list; // front = MRU, back = LRU
std::unordered_map<std::string, std::list<Entry>::iterator> index;
size_t totalSize = 0;
size_t maxBytes = 0;
};

void insertHead(SegmentState& seg, const std::string& key, size_t size);
void removeFrom(SegmentState& seg,
std::unordered_map<std::string, std::list<Entry>::iterator>::iterator indexIt);
void rebalanceProtected();

SegmentState _probationary;
SegmentState _protected;
};

} // namespace store
} // namespace endpoint
} // namespace privmx

#endif // _PRIVMXLIB_ENDPOINT_STORE_CACHE_SLRU_POLICY_HPP_
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
PrivMX Endpoint.
Copyright © 2026 Simplito sp. z o.o.

This file is part of the PrivMX Platform (https://privmx.dev).
This software is Licensed under the PrivMX Free License.

See the License for the specific language governing permissions and
limitations under the License.
*/

#ifndef _PRIVMXLIB_ENDPOINT_STORE_CACHE_BACKEND_INTERFACE_HPP_
#define _PRIVMXLIB_ENDPOINT_STORE_CACHE_BACKEND_INTERFACE_HPP_

#include <cstddef>
#include <memory>
#include <optional>
#include <string>

#include <privmx/endpoint/core/Buffer.hpp>

namespace privmx {
namespace endpoint {
namespace store {

/**
* Low-level storage backend for cache implementations.
*
* CacheInterface implementations delegate raw persistence to this backend.
* The backend operates on plain string keys and binary values; any namespacing,
* eviction, or size accounting is handled by the CacheInterface layer above.
*/
class CacheBackendInterface {
public:
/** Default maximum total size of cached data (512 MB) used by backend-backed cache implementations. */
static constexpr size_t DEFAULT_MAX_BYTES = 512ULL * 1024 * 1024;

/**
* Forward-only iterator over a range of entries sharing a common key prefix.
* Obtained via CacheBackendInterface::seek().
*/
class Iterator {
public:
virtual ~Iterator() = default;

/**
* Returns true if the iterator points to a valid entry.
* Must be checked before calling key(), value(), or next().
*/
virtual bool valid() const = 0;

/**
* Advances the iterator to the next matching entry.
* Calling next() when valid() is false is undefined behaviour.
*/
virtual void next() = 0;

/**
* Returns the key of the current entry.
* Calling key() when valid() is false is undefined behaviour.
*/
virtual const std::string& key() const = 0;

/**
* Returns the value of the current entry.
* Calling value() when valid() is false is undefined behaviour.
*/
virtual core::Buffer value() const = 0;
};

virtual ~CacheBackendInterface() = default;

/**
* Returns the stored value for the given key, or std::nullopt if absent.
*/
virtual std::optional<core::Buffer> get(const std::string& key) = 0;

/**
* Inserts or replaces the value for the given key.
*/
virtual void put(const std::string& key, const core::Buffer& value) = 0;

/**
* Removes the entry for the given key. No-op if the key does not exist.
*/
virtual void del(const std::string& key) = 0;

/**
* Removes all entries from the backend.
*/
virtual void clear() = 0;

/**
* Returns an iterator positioned at the first entry whose key is >= @p prefix,
* iterating in key order until the end of all entries.
* An empty prefix iterates over all entries from the beginning.
*/
virtual std::unique_ptr<Iterator> seek(const std::string& prefix) = 0;
};

} // namespace store
} // namespace endpoint
} // namespace privmx

#endif // _PRIVMXLIB_ENDPOINT_STORE_CACHE_BACKEND_INTERFACE_HPP_
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
#include <memory>
#include <mutex>

#include <privmx/endpoint/store/cache/CacheBackendInterface.hpp>
#include <privmx/endpoint/store/cache/CacheInterface.hpp>

namespace privmx {
Expand All @@ -38,20 +39,32 @@ class GlobalCache {
public:
/**
* Enables or disables the built-in in-memory cache.
* Must be called before Connection::connect(). Has no effect if setChunksCache() was already called.
* Must be called before Connection::connect(). Has no effect if setChunksCache() or
* setChunksCacheBackend() was already called.
*
* @param enabled true to use the built-in CacheInMemory (default), false to use a no-op cache
*/
static void setChunksCacheEnabled(bool enabled);

/**
* Sets a custom cache implementation for the entire runtime.
* Must be called before Connection::connect(). Takes precedence over setChunksCacheEnabled().
* Must be called before Connection::connect(). Takes precedence over all other setup methods.
*
* @param cache custom CacheInterface implementation (e.g. disk-backed or Redis cache)
*/
static void setChunksCache(std::shared_ptr<CacheInterface> cache);

/**
* Configures the built-in SLRU cache backed by the given persistent backend.
* Must be called before Connection::connect(). Takes precedence over setChunksCacheEnabled()
* but is overridden by setChunksCache().
*
* @param backend storage backend (e.g. LevelDB wrapper); GlobalCache takes shared ownership
* @param maxBytes maximum total size of cached data before SLRU eviction kicks in
*/
static void setChunksCacheBackend(std::shared_ptr<CacheBackendInterface> backend,
size_t maxBytes = CacheBackendInterface::DEFAULT_MAX_BYTES);

/**
* Returns the shared cache instance, initializing it on the first call.
* After the first call the instance is frozen — subsequent setChunksCache()
Expand All @@ -64,6 +77,8 @@ class GlobalCache {
private:
static bool _isChunksCacheEnabled;
static std::shared_ptr<CacheInterface> _chunksCacheImpl;
static std::shared_ptr<CacheBackendInterface> _chunksCacheBackend;
static size_t _chunksCacheMaxBytes;
static std::once_flag _chunksCacheInitFlag;
};

Expand Down
Loading