shard session_store

This commit is contained in:
Sky Johnson 2025-06-14 14:25:01 -05:00
parent 652053128d
commit 7e4c8d12cc

View File

@ -8,20 +8,27 @@
#include <array> #include <array>
#include <chrono> #include <chrono>
#include <optional> #include <optional>
#include <atomic>
#include <list>
class SessionStore { class SessionStore {
private: private:
static constexpr size_t SHARD_COUNT = 16; static constexpr size_t SHARD_COUNT = 16;
static constexpr size_t MAX_SESSIONS_PER_SHARD = 64;
static constexpr size_t MAX_SESSION_DATA_SIZE = 16;
static constexpr size_t MAX_VALUE_SIZE = 1024;
static constexpr auto SESSION_TIMEOUT = std::chrono::hours(24); static constexpr auto SESSION_TIMEOUT = std::chrono::hours(24);
struct SessionData { struct SessionData {
std::unordered_map<std::string, std::string> data; std::unordered_map<std::string, std::string> data;
std::chrono::steady_clock::time_point last_access; std::chrono::steady_clock::time_point last_access;
std::list<std::string>::iterator lru_iter;
bool dirty = false; bool dirty = false;
}; };
std::array<std::mutex, SHARD_COUNT> mutexes_; std::array<std::mutex, SHARD_COUNT> mutexes_;
std::array<std::unordered_map<std::string, SessionData>, SHARD_COUNT> shards_; std::array<std::unordered_map<std::string, SessionData>, SHARD_COUNT> shards_;
std::array<std::list<std::string>, SHARD_COUNT> lru_lists_;
KeyValueStore store_; KeyValueStore store_;
std::mt19937 rng_; std::mt19937 rng_;
@ -29,24 +36,66 @@ private:
return std::hash<std::string>{}(id) % SHARD_COUNT; return std::hash<std::string>{}(id) % SHARD_COUNT;
} }
void evict_lru_session(size_t shard) {
auto& shard_map = shards_[shard];
auto& lru_list = lru_lists_[shard];
if (lru_list.empty()) return;
std::string oldest_id = lru_list.back();
lru_list.pop_back();
auto it = shard_map.find(oldest_id);
if (it != shard_map.end()) {
// Save dirty session before eviction
if (it->second.dirty) {
for (const auto& [key, value] : it->second.data) {
store_.set(oldest_id + ":" + key, value);
}
}
shard_map.erase(it);
}
}
void touch_session(const std::string& id, size_t shard) {
auto& session = shards_[shard][id];
auto& lru_list = lru_lists_[shard];
// Move to front of LRU
lru_list.erase(session.lru_iter);
lru_list.push_front(id);
session.lru_iter = lru_list.begin();
session.last_access = std::chrono::steady_clock::now();
}
void lazy_load_session(const std::string& id, size_t shard) { void lazy_load_session(const std::string& id, size_t shard) {
auto& shard_map = shards_[shard]; auto& shard_map = shards_[shard];
if (shard_map.find(id) != shard_map.end()) return; if (shard_map.find(id) != shard_map.end()) return;
// Load from persistent store // Check session limit
if (shard_map.size() >= MAX_SESSIONS_PER_SHARD) {
evict_lru_session(shard);
}
SessionData session; SessionData session;
session.last_access = std::chrono::steady_clock::now(); session.last_access = std::chrono::steady_clock::now();
// Try to load existing session data // Load from persistent store
for (const auto& [key, value] : store_.get_all()) { for (const auto& [key, value] : store_.get_all()) {
if (key.starts_with(id + ":")) { if (key.starts_with(id + ":")) {
std::string session_key = key.substr(id.length() + 1); std::string session_key = key.substr(id.length() + 1);
if (auto* str_val = std::get_if<std::string>(&value)) { if (auto* str_val = std::get_if<std::string>(&value)) {
session.data[session_key] = *str_val; if (session.data.size() < MAX_SESSION_DATA_SIZE) {
session.data[session_key] = *str_val;
}
} }
} }
} }
// Add to LRU
lru_lists_[shard].push_front(id);
session.lru_iter = lru_lists_[shard].begin();
shard_map[id] = std::move(session); shard_map[id] = std::move(session);
} }
@ -56,9 +105,11 @@ private:
for (size_t i = 0; i < SHARD_COUNT; ++i) { for (size_t i = 0; i < SHARD_COUNT; ++i) {
std::lock_guard<std::mutex> lock(mutexes_[i]); std::lock_guard<std::mutex> lock(mutexes_[i]);
auto& shard = shards_[i]; auto& shard = shards_[i];
auto& lru_list = lru_lists_[i];
for (auto it = shard.begin(); it != shard.end();) { for (auto it = shard.begin(); it != shard.end();) {
if (now - it->second.last_access > SESSION_TIMEOUT) { if (now - it->second.last_access > SESSION_TIMEOUT) {
lru_list.erase(it->second.lru_iter);
it = shard.erase(it); it = shard.erase(it);
} else { } else {
++it; ++it;
@ -73,7 +124,6 @@ public:
} }
bool needs_session(const Request& req) const { bool needs_session(const Request& req) const {
// Skip sessions for static content
if (req.path.starts_with("/static") || if (req.path.starts_with("/static") ||
req.path.starts_with("/assets") || req.path.starts_with("/assets") ||
req.path.ends_with(".css") || req.path.ends_with(".css") ||
@ -90,7 +140,6 @@ public:
return false; return false;
} }
// Need sessions for API and admin routes
return req.path.starts_with("/api") || return req.path.starts_with("/api") ||
req.path.starts_with("/admin") || req.path.starts_with("/admin") ||
req.method == HttpMethod::POST || req.method == HttpMethod::POST ||
@ -103,11 +152,21 @@ public:
size_t shard = get_shard(id); size_t shard = get_shard(id);
std::lock_guard<std::mutex> lock(mutexes_[shard]); std::lock_guard<std::mutex> lock(mutexes_[shard]);
// Check session limit
if (shards_[shard].size() >= MAX_SESSIONS_PER_SHARD) {
evict_lru_session(shard);
}
SessionData session; SessionData session;
session.last_access = std::chrono::steady_clock::now(); session.last_access = std::chrono::steady_clock::now();
session.dirty = true; session.dirty = true;
shards_[shard][id] = std::move(session);
// Add to LRU
lru_lists_[shard].push_front(id);
session.lru_iter = lru_lists_[shard].begin();
shards_[shard][id] = std::move(session);
return id; return id;
} }
@ -120,21 +179,30 @@ public:
auto it = shards_[shard].find(id); auto it = shards_[shard].find(id);
if (it == shards_[shard].end()) return std::nullopt; if (it == shards_[shard].end()) return std::nullopt;
it->second.last_access = std::chrono::steady_clock::now(); touch_session(id, shard);
auto data_it = it->second.data.find(key); auto data_it = it->second.data.find(key);
return data_it != it->second.data.end() ? return data_it != it->second.data.end() ?
std::optional<std::string>{data_it->second} : std::nullopt; std::optional<std::string>{data_it->second} : std::nullopt;
} }
void set(const std::string& id, const std::string& key, const std::string& value) { void set(const std::string& id, const std::string& key, const std::string& value) {
if (value.size() > MAX_VALUE_SIZE) return;
size_t shard = get_shard(id); size_t shard = get_shard(id);
std::lock_guard<std::mutex> lock(mutexes_[shard]); std::lock_guard<std::mutex> lock(mutexes_[shard]);
lazy_load_session(id, shard); lazy_load_session(id, shard);
auto& session = shards_[shard][id]; auto& session = shards_[shard][id];
// Check data limit
if (session.data.size() >= MAX_SESSION_DATA_SIZE && session.data.find(key) == session.data.end()) {
return;
}
session.data[key] = value; session.data[key] = value;
session.last_access = std::chrono::steady_clock::now(); touch_session(id, shard);
session.dirty = true; session.dirty = true;
} }
@ -142,18 +210,20 @@ public:
size_t shard = get_shard(id); size_t shard = get_shard(id);
std::lock_guard<std::mutex> lock(mutexes_[shard]); std::lock_guard<std::mutex> lock(mutexes_[shard]);
bool found = shards_[shard].erase(id) > 0; auto it = shards_[shard].find(id);
if (it == shards_[shard].end()) return false;
lru_lists_[shard].erase(it->second.lru_iter);
shards_[shard].erase(it);
// Remove from persistent store // Remove from persistent store
if (found) { for (const auto& [key, value] : store_.get_all()) {
for (const auto& [key, value] : store_.get_all()) { if (key.starts_with(id + ":")) {
if (key.starts_with(id + ":")) { store_.del(key);
store_.del(key);
}
} }
} }
return found; return true;
} }
size_t size() { size_t size() {
@ -171,11 +241,9 @@ public:
void load() { void load() {
store_.load(); store_.load();
// Sessions loaded lazily on first access
} }
void save() { void save() {
// Save only dirty sessions
for (size_t i = 0; i < SHARD_COUNT; ++i) { for (size_t i = 0; i < SHARD_COUNT; ++i) {
std::lock_guard<std::mutex> lock(mutexes_[i]); std::lock_guard<std::mutex> lock(mutexes_[i]);
@ -193,7 +261,6 @@ public:
store_.save(); store_.save();
} }
// Use existing cookie helpers
static std::string_view extract_session_id(const Request& req) { static std::string_view extract_session_id(const Request& req) {
return req.get_cookie("session_id"); return req.get_cookie("session_id");
} }