Browse Source

feat: FileCache use LRUCache

ithewei 2 months ago
parent
commit
e2ba81baa2
7 changed files with 332 additions and 44 deletions
  1. 1 0
      Makefile.vars
  2. 1 0
      cmake/vars.cmake
  3. 312 0
      cpputil/LRUCache.h
  4. 0 1
      docs/PLAN.md
  5. 12 35
      http/server/FileCache.cpp
  6. 5 7
      http/server/FileCache.h
  7. 1 1
      http/server/HttpHandler.cpp

+ 1 - 0
Makefile.vars

@@ -49,6 +49,7 @@ CPPUTIL_HEADERS = cpputil/hmap.h\
 				cpputil/iniparser.h\
 				cpputil/json.hpp\
 				cpputil/singleton.h\
+				cpputil/LRUCache.h\
 				cpputil/ThreadLocalStorage.h\
 
 EVPP_HEADERS  = evpp/Buffer.h\

+ 1 - 0
cmake/vars.cmake

@@ -50,6 +50,7 @@ set(CPPUTIL_HEADERS
     cpputil/iniparser.h
     cpputil/json.hpp
     cpputil/singleton.h
+    cpputil/LRUCache.h
     cpputil/ThreadLocalStorage.h
 )
 

+ 312 - 0
cpputil/LRUCache.h

@@ -0,0 +1,312 @@
+#ifndef HV_LRU_CACHE_H_
+#define HV_LRU_CACHE_H_
+
+#include <unordered_map>
+#include <list>
+#include <mutex>
+#include <memory>
+#include <functional>
+
+namespace hv {
+
+/**
+ * @brief Thread-safe LRU (Least Recently Used) Cache template
+ * 
+ * This template provides a generic LRU cache implementation with the following features:
+ * - Thread-safe operations using mutex
+ * - Configurable capacity with automatic eviction
+ * - O(1) get, put, and remove operations
+ * - Optional eviction callback for cleanup
+ * 
+ * @tparam Key The key type (must be hashable)
+ * @tparam Value The value type
+ */
+template<typename Key, typename Value>
+class LRUCache {
+public:
+    using key_type = Key;
+    using value_type = Value;
+    using eviction_callback_t = std::function<void(const Key&, const Value&)>;
+
+private:
+    // Double-linked list node for LRU ordering
+    struct Node {
+        Key key;
+        Value value;
+        
+        Node(const Key& k, const Value& v) : key(k), value(v) {}
+    };
+    
+    using node_list_t = std::list<Node>;
+    using node_iterator_t = typename node_list_t::iterator;
+    using hash_map_t = std::unordered_map<Key, node_iterator_t>;
+
+public:
+    /**
+     * @brief Construct LRUCache with specified capacity
+     * @param capacity Maximum number of items to cache (default: 100)
+     */
+    explicit LRUCache(size_t capacity = 100) 
+        : capacity_(capacity), eviction_callback_(nullptr) {
+        if (capacity_ == 0) {
+            capacity_ = 1; // Minimum capacity of 1
+        }
+    }
+
+    /**
+     * @brief Destructor
+     */
+    virtual ~LRUCache() {
+        clear();
+    }
+
+    // Disable copy constructor and assignment operator
+    LRUCache(const LRUCache&) = delete;
+    LRUCache& operator=(const LRUCache&) = delete;
+
+    /**
+     * @brief Set eviction callback function
+     * @param callback Function to call when items are evicted
+     */
+    void set_eviction_callback(eviction_callback_t callback) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        eviction_callback_ = callback;
+    }
+
+    /**
+     * @brief Get value by key
+     * @param key The key to search for
+     * @param value Output parameter for the value
+     * @return true if key exists, false otherwise
+     */
+    bool get(const Key& key, Value& value) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        auto it = hash_map_.find(key);
+        if (it == hash_map_.end()) {
+            return false;
+        }
+        
+        // Move to front (most recently used)
+        move_to_front(it->second);
+        value = it->second->value;
+        return true;
+    }
+
+    /**
+     * @brief Get value by key (alternative interface)
+     * @param key The key to search for
+     * @return Pointer to value if exists, nullptr otherwise
+     */
+    Value* get(const Key& key) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        auto it = hash_map_.find(key);
+        if (it == hash_map_.end()) {
+            return nullptr;
+        }
+        
+        // Move to front (most recently used)
+        move_to_front(it->second);
+        return &(it->second->value);
+    }
+
+    /**
+     * @brief Put key-value pair into cache
+     * @param key The key
+     * @param value The value
+     * @return true if new item was added, false if existing item was updated
+     */
+    bool put(const Key& key, const Value& value) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        auto it = hash_map_.find(key);
+        
+        if (it != hash_map_.end()) {
+            // Update existing item
+            it->second->value = value;
+            move_to_front(it->second);
+            return false;
+        }
+        
+        // Add new item
+        if (node_list_.size() >= capacity_) {
+            evict_lru();
+        }
+        
+        node_list_.emplace_front(key, value);
+        hash_map_[key] = node_list_.begin();
+        return true;
+    }
+
+    /**
+     * @brief Remove item by key
+     * @param key The key to remove
+     * @return true if item was removed, false if key not found
+     */
+    bool remove(const Key& key) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        auto it = hash_map_.find(key);
+        if (it == hash_map_.end()) {
+            return false;
+        }
+        
+        // Call eviction callback if set
+        if (eviction_callback_) {
+            eviction_callback_(it->second->key, it->second->value);
+        }
+        
+        node_list_.erase(it->second);
+        hash_map_.erase(it);
+        return true;
+    }
+
+    /**
+     * @brief Check if key exists in cache
+     * @param key The key to check
+     * @return true if key exists, false otherwise
+     */
+    bool contains(const Key& key) const {
+        std::lock_guard<std::mutex> lock(mutex_);
+        return hash_map_.find(key) != hash_map_.end();
+    }
+
+    /**
+     * @brief Clear all items from cache
+     */
+    void clear() {
+        std::lock_guard<std::mutex> lock(mutex_);
+        if (eviction_callback_) {
+            for (const auto& node : node_list_) {
+                eviction_callback_(node.key, node.value);
+            }
+        }
+        node_list_.clear();
+        hash_map_.clear();
+    }
+
+    /**
+     * @brief Get current cache size
+     * @return Number of items in cache
+     */
+    size_t size() const {
+        std::lock_guard<std::mutex> lock(mutex_);
+        return node_list_.size();
+    }
+
+    /**
+     * @brief Get cache capacity
+     * @return Maximum number of items cache can hold
+     */
+    size_t capacity() const {
+        return capacity_;
+    }
+
+    /**
+     * @brief Check if cache is empty
+     * @return true if cache is empty, false otherwise
+     */
+    bool empty() const {
+        std::lock_guard<std::mutex> lock(mutex_);
+        return node_list_.empty();
+    }
+
+    /**
+     * @brief Set new capacity (may trigger eviction)
+     * @param new_capacity New capacity value
+     */
+    void set_capacity(size_t new_capacity) {
+        if (new_capacity == 0) {
+            new_capacity = 1; // Minimum capacity of 1
+        }
+        
+        std::lock_guard<std::mutex> lock(mutex_);
+        capacity_ = new_capacity;
+        
+        // Evict excess items if necessary
+        while (node_list_.size() > capacity_) {
+            evict_lru();
+        }
+    }
+
+    /**
+     * @brief Apply a function to all cached items (for iteration)
+     * @param func Function to apply to each key-value pair
+     * Note: This is provided for compatibility but should be used carefully
+     * as it may affect performance due to locking
+     */
+    template<typename Func>
+    void for_each(Func func) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        for (const auto& node : node_list_) {
+            func(node.key, node.value);
+        }
+    }
+
+    /**
+     * @brief Remove items that match a predicate
+     * @param predicate Function that returns true for items to remove
+     * @return Number of items removed
+     */
+    template<typename Predicate>
+    size_t remove_if(Predicate predicate) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        size_t removed_count = 0;
+        
+        auto it = node_list_.begin();
+        while (it != node_list_.end()) {
+            if (predicate(it->key, it->value)) {
+                // Call eviction callback if set
+                if (eviction_callback_) {
+                    eviction_callback_(it->key, it->value);
+                }
+                
+                hash_map_.erase(it->key);
+                it = node_list_.erase(it);
+                removed_count++;
+            } else {
+                ++it;
+            }
+        }
+        
+        return removed_count;
+    }
+
+protected:
+    /**
+     * @brief Move node to front of list (most recently used position)
+     * @param it Iterator to the node to move
+     */
+    void move_to_front(node_iterator_t it) {
+        if (it != node_list_.begin()) {
+            node_list_.splice(node_list_.begin(), node_list_, it);
+        }
+    }
+
+    /**
+     * @brief Evict least recently used item
+     */
+    void evict_lru() {
+        if (node_list_.empty()) {
+            return;
+        }
+        
+        auto last = std::prev(node_list_.end());
+        
+        // Call eviction callback if set
+        if (eviction_callback_) {
+            eviction_callback_(last->key, last->value);
+        }
+        
+        hash_map_.erase(last->key);
+        node_list_.erase(last);
+    }
+
+protected:
+    size_t capacity_;                           // Maximum cache capacity
+    mutable std::mutex mutex_;                  // Mutex for thread safety
+    node_list_t node_list_;                     // Doubly-linked list for LRU ordering
+    hash_map_t hash_map_;                       // Hash map for O(1) access
+    eviction_callback_t eviction_callback_;     // Optional eviction callback
+};
+
+} // namespace hv
+
+#endif // HV_LRU_CACHE_H_

+ 0 - 1
docs/PLAN.md

@@ -12,7 +12,6 @@
 ## Improving
 
 - Path router: optimized matching via trie?
-- FileCache use LRUCache
 
 ## Plan
 

+ 12 - 35
http/server/FileCache.cpp

@@ -14,13 +14,12 @@
 
 #define ETAG_FMT    "\"%zx-%zx\""
 
-FileCache::FileCache() {
+FileCache::FileCache(size_t capacity) : hv::LRUCache<std::string, file_cache_ptr>(capacity) {
     stat_interval = 10; // s
     expired_time  = 60; // s
 }
 
 file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) {
-    std::lock_guard<std::mutex> locker(mutex_);
     file_cache_ptr fc = Get(filepath);
 #ifdef OS_WIN
     std::wstring wfilepath;
@@ -87,7 +86,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) {
                 time(&fc->open_time);
                 fc->stat_time = fc->open_time;
                 fc->stat_cnt = 1;
-                cached_files[filepath] = fc;
+                put(filepath, fc);
             }
             else {
                 param->error = ERR_MISMATCH;
@@ -136,47 +135,25 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) {
     return fc;
 }
 
-bool FileCache::Close(const char* filepath) {
-    std::lock_guard<std::mutex> locker(mutex_);
-    auto iter = cached_files.find(filepath);
-    if (iter != cached_files.end()) {
-        iter = cached_files.erase(iter);
-        return true;
-    }
-    return false;
+bool FileCache::Exists(const char* filepath) const {
+    return contains(filepath);
 }
 
-bool FileCache::Close(const file_cache_ptr& fc) {
-    std::lock_guard<std::mutex> locker(mutex_);
-    auto iter = cached_files.begin();
-    while (iter != cached_files.end()) {
-        if (iter->second == fc) {
-            iter = cached_files.erase(iter);
-            return true;
-        } else {
-            ++iter;
-        }
-    }
-    return false;
+bool FileCache::Close(const char* filepath) {
+    return remove(filepath);
 }
 
 file_cache_ptr FileCache::Get(const char* filepath) {
-    auto iter = cached_files.find(filepath);
-    if (iter != cached_files.end()) {
-        return iter->second;
+    file_cache_ptr fc;
+    if (get(filepath, fc)) {
+        return fc;
     }
     return NULL;
 }
 
 void FileCache::RemoveExpiredFileCache() {
-    std::lock_guard<std::mutex> locker(mutex_);
     time_t now = time(NULL);
-    auto iter = cached_files.begin();
-    while (iter != cached_files.end()) {
-        if (now - iter->second->stat_time > expired_time) {
-            iter = cached_files.erase(iter);
-        } else {
-            ++iter;
-        }
-    }
+    remove_if([this, now](const std::string& filepath, const file_cache_ptr& fc) {
+        return (now - fc->stat_time > expired_time);
+    });
 }

+ 5 - 7
http/server/FileCache.h

@@ -8,8 +8,10 @@
 
 #include "hbuf.h"
 #include "hstring.h"
+#include "LRUCache.h"
 
 #define HTTP_HEADER_MAX_LENGTH      1024        // 1K
+#define FILE_CACHE_MAX_NUM          100
 #define FILE_CACHE_MAX_SIZE         (1 << 22)   // 4M
 
 typedef struct file_cache_s {
@@ -55,17 +57,13 @@ typedef struct file_cache_s {
 } file_cache_t;
 
 typedef std::shared_ptr<file_cache_t>           file_cache_ptr;
-// filepath => file_cache_ptr
-typedef std::map<std::string, file_cache_ptr>   FileCacheMap;
 
-class FileCache {
+class FileCache : public hv::LRUCache<std::string, file_cache_ptr> {
 public:
-    FileCacheMap    cached_files;
-    std::mutex      mutex_;
     int             stat_interval;
     int             expired_time;
 
-    FileCache();
+    FileCache(size_t capacity = FILE_CACHE_MAX_NUM);
 
     struct OpenParam {
         bool need_read;
@@ -83,8 +81,8 @@ public:
         }
     };
     file_cache_ptr Open(const char* filepath, OpenParam* param);
+    bool Exists(const char* filepath) const;
     bool Close(const char* filepath);
-    bool Close(const file_cache_ptr& fc);
     void RemoveExpiredFileCache();
 
 protected:

+ 1 - 1
http/server/HttpHandler.cpp

@@ -827,7 +827,7 @@ return_header:
         {
             // NOTE: remove file cache if > FILE_CACHE_MAX_SIZE
             if (fc && fc->filebuf.len > FILE_CACHE_MAX_SIZE) {
-                files->Close(fc);
+                files->Close(fc->filepath.c_str());
             }
             fc = NULL;
             header.clear();