diff OrthancFramework/Sources/Cache/MemoryStringCache.cpp @ 5420:d37dff2c0028 am-new-cache

Optimized the MemoryStringCache to prevent loading the same file multiple times if multiple users request the same file at the same time
author Alain Mazy <am@osimis.io>
date Mon, 13 Nov 2023 17:01:59 +0100
parents 0ea402b4d901
children c65e036d649b
line wrap: on
line diff
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Thu Nov 09 08:51:01 2023 +0100
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Mon Nov 13 17:01:59 2023 +0100
@@ -53,47 +53,221 @@
     }      
   };
 
+
+  MemoryStringCache::Accessor::Accessor(MemoryStringCache& cache)
+  : cache_(cache),
+    shouldAdd_(false)
+  {
+  }
+
+
+  MemoryStringCache::Accessor::~Accessor()
+  {
+    // if this accessor was the one in charge of loading and adding the data into the cache
+    // and it failed to add, remove the key from the list to make sure others accessor
+    // stop waiting for it.
+    if (shouldAdd_)
+    {
+      cache_.RemoveFromItemsBeingLoaded(keyToAdd_);
+    }
+  }
+
+
+  bool MemoryStringCache::Accessor::Fetch(std::string& value, const std::string& key)
+  {
+    // if multiple accessors are fetching at the same time:
+    // the first one will return false and will be in charge of adding to the cache.
+    // others will wait.
+    // if the first one fails to add, or, if the content was too large to fit in the cache,
+    // the next one will be in charge of adding ...
+    if (!cache_.Fetch(value, key))
+    {
+      shouldAdd_ = true;
+      keyToAdd_ = key;
+      return false;
+    }
+
+    shouldAdd_ = false;
+    keyToAdd_.clear();
+
+    return true;
+  }
+
+
+  void MemoryStringCache::Accessor::Add(const std::string& key, const std::string& value)
+  {
+    cache_.Add(key, value);
+    shouldAdd_ = false;
+  }
+
+
+  void MemoryStringCache::Accessor::Add(const std::string& key, const char* buffer, size_t size)
+  {
+    cache_.Add(key, buffer, size);
+    shouldAdd_ = false;
+  }
+
+
+  MemoryStringCache::MemoryStringCache() :
+    currentSize_(0),
+    maxSize_(100 * 1024 * 1024)  // 100 MB
+  {
+  }
+
+
+  MemoryStringCache::~MemoryStringCache()
+  {
+    Recycle(0);
+    assert(content_.IsEmpty());
+  }
+
+
   size_t MemoryStringCache::GetMaximumSize()
   {
-    return cache_.GetMaximumSize();
+    return maxSize_;
   }
 
+
   void MemoryStringCache::SetMaximumSize(size_t size)
   {
-    cache_.SetMaximumSize(size);
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+      
+    // // Make sure no accessor is currently open (as its data may be
+    // // removed if recycling is needed)
+    // WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    Recycle(size);
+    maxSize_ = size;
   }
 
+
   void MemoryStringCache::Add(const std::string& key,
-                              const std::string& value)
+                               const std::string& value)
   {
-    cache_.Acquire(key, new StringValue(value));
+    std::unique_ptr<StringValue> item(new StringValue(value));
+    size_t size = value.size();
+
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    if (size > maxSize_)
+    {
+      // This object is too large to be stored in the cache, discard it
+    }
+    else if (content_.Contains(key))
+    {
+      // Value already stored, don't overwrite the old value but put it on top of the cache
+      content_.MakeMostRecent(key);
+    }
+    else
+    {
+      Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
+      assert(currentSize_ + size <= maxSize_);
+
+      content_.Add(key, item.release());
+      currentSize_ += size;
+    }
+
+    RemoveFromItemsBeingLoadedInternal(key);
   }
 
+
   void MemoryStringCache::Add(const std::string& key,
                               const void* buffer,
                               size_t size)
   {
-    cache_.Acquire(key, new StringValue(reinterpret_cast<const char*>(buffer), size));
+    Add(key, std::string(reinterpret_cast<const char*>(buffer), size));
   }
 
+
   void MemoryStringCache::Invalidate(const std::string &key)
   {
-    cache_.Invalidate(key);
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    StringValue* item = NULL;
+    if (content_.Contains(key, item))
+    {
+      assert(item != NULL);
+      const size_t size = item->GetMemoryUsage();
+      delete item;
+
+      content_.Invalidate(key);
+          
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    RemoveFromItemsBeingLoadedInternal(key);
   }
-  
+
+
   bool MemoryStringCache::Fetch(std::string& value,
                                 const std::string& key)
   {
-    MemoryObjectCache::Accessor reader(cache_, key, false /* multiple readers are allowed */);
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    StringValue* item;
 
-    if (reader.IsValid())
+    // if another client is currently loading the item, wait for it.
+    while (itemsBeingLoaded_.find(key) != itemsBeingLoaded_.end() && !content_.Contains(key, item))
     {
-      value = dynamic_cast<StringValue&>(reader.GetValue()).GetContent();
+      cacheCond_.wait(cacheLock);
+    }
+
+    if (content_.Contains(key, item))
+    {
+      value = dynamic_cast<StringValue&>(*item).GetContent();
+      content_.MakeMostRecent(key);
+
       return true;
     }
     else
     {
+      // note that this accessor will be in charge of loading and adding.
+      itemsBeingLoaded_.insert(key);
       return false;
     }
   }
+
+
+  void MemoryStringCache::RemoveFromItemsBeingLoaded(const std::string& key)
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+    RemoveFromItemsBeingLoadedInternal(key);
+  }
+
+
+  void MemoryStringCache::RemoveFromItemsBeingLoadedInternal(const std::string& key)
+  {
+    // notify all waiting users, some of them potentially waiting for this item
+    itemsBeingLoaded_.erase(key);
+    cacheCond_.notify_all();
+  }
+
+  void MemoryStringCache::Recycle(size_t targetSize)
+  {
+    // WARNING: "cacheMutex_" must be locked
+    while (currentSize_ > targetSize)
+    {
+      assert(!content_.IsEmpty());
+        
+      StringValue* item = NULL;
+      content_.RemoveOldest(item);
+
+      assert(item != NULL);
+      const size_t size = item->GetMemoryUsage();
+      delete item;
+
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    // Post-condition: "currentSize_ <= targetSize"
+
+  }
 }