diff --git a/session_store.hpp b/session_store.hpp index 47b1fe4..b0fe25b 100644 --- a/session_store.hpp +++ b/session_store.hpp @@ -8,20 +8,27 @@ #include #include #include +#include +#include class SessionStore { private: 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); struct SessionData { std::unordered_map data; std::chrono::steady_clock::time_point last_access; + std::list::iterator lru_iter; bool dirty = false; }; std::array mutexes_; std::array, SHARD_COUNT> shards_; + std::array, SHARD_COUNT> lru_lists_; KeyValueStore store_; std::mt19937 rng_; @@ -29,24 +36,66 @@ private: return std::hash{}(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) { auto& shard_map = shards_[shard]; 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; 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()) { if (key.starts_with(id + ":")) { std::string session_key = key.substr(id.length() + 1); if (auto* str_val = std::get_if(&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); } @@ -56,9 +105,11 @@ private: for (size_t i = 0; i < SHARD_COUNT; ++i) { std::lock_guard lock(mutexes_[i]); auto& shard = shards_[i]; + auto& lru_list = lru_lists_[i]; for (auto it = shard.begin(); it != shard.end();) { if (now - it->second.last_access > SESSION_TIMEOUT) { + lru_list.erase(it->second.lru_iter); it = shard.erase(it); } else { ++it; @@ -73,7 +124,6 @@ public: } bool needs_session(const Request& req) const { - // Skip sessions for static content if (req.path.starts_with("/static") || req.path.starts_with("/assets") || req.path.ends_with(".css") || @@ -90,7 +140,6 @@ public: return false; } - // Need sessions for API and admin routes return req.path.starts_with("/api") || req.path.starts_with("/admin") || req.method == HttpMethod::POST || @@ -103,11 +152,21 @@ public: size_t shard = get_shard(id); std::lock_guard lock(mutexes_[shard]); + + // Check session limit + if (shards_[shard].size() >= MAX_SESSIONS_PER_SHARD) { + evict_lru_session(shard); + } + SessionData session; session.last_access = std::chrono::steady_clock::now(); 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; } @@ -120,21 +179,30 @@ public: auto it = shards_[shard].find(id); 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); return data_it != it->second.data.end() ? std::optional{data_it->second} : std::nullopt; } 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); std::lock_guard lock(mutexes_[shard]); lazy_load_session(id, shard); 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.last_access = std::chrono::steady_clock::now(); + touch_session(id, shard); session.dirty = true; } @@ -142,18 +210,20 @@ public: size_t shard = get_shard(id); std::lock_guard 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 - if (found) { - for (const auto& [key, value] : store_.get_all()) { - if (key.starts_with(id + ":")) { - store_.del(key); - } + for (const auto& [key, value] : store_.get_all()) { + if (key.starts_with(id + ":")) { + store_.del(key); } } - return found; + return true; } size_t size() { @@ -171,11 +241,9 @@ public: void load() { store_.load(); - // Sessions loaded lazily on first access } void save() { - // Save only dirty sessions for (size_t i = 0; i < SHARD_COUNT; ++i) { std::lock_guard lock(mutexes_[i]); @@ -193,7 +261,6 @@ public: store_.save(); } - // Use existing cookie helpers static std::string_view extract_session_id(const Request& req) { return req.get_cookie("session_id"); } @@ -210,4 +277,4 @@ private: return id; } -}; +}; \ No newline at end of file