changeset 3557:4d809b2e1141

better cache toolbox: MemoryObjectCache
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 04 Nov 2019 15:18:38 +0100
parents 87940f7156e0
children 36c0ce0188cc
files Core/Cache/ICachePageProvider.h Core/Cache/ICacheable.h Core/Cache/MemoryCache.cpp Core/Cache/MemoryCache.h Core/Cache/MemoryObjectCache.cpp Core/Cache/MemoryObjectCache.h Core/Cache/MemoryStringCache.cpp Core/Cache/MemoryStringCache.h Core/Cache/SharedArchive.h OrthancServer/ServerContext.h Resources/CMake/OrthancFrameworkConfiguration.cmake UnitTestsSources/MemoryCacheTests.cpp
diffstat 12 files changed, 719 insertions(+), 85 deletions(-) [+]
line wrap: on
line diff
--- a/Core/Cache/ICachePageProvider.h	Thu Oct 31 14:00:39 2019 +0100
+++ b/Core/Cache/ICachePageProvider.h	Mon Nov 04 15:18:38 2019 +0100
@@ -38,13 +38,16 @@
 
 namespace Orthanc
 {
-  class ICachePageProvider
+  namespace Deprecated
   {
-  public:
-    virtual ~ICachePageProvider()
+    class ICachePageProvider
     {
-    }
+    public:
+      virtual ~ICachePageProvider()
+      {
+      }
 
-    virtual IDynamicObject* Provide(const std::string& id) = 0;
-  };
+      virtual IDynamicObject* Provide(const std::string& id) = 0;
+    };
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/ICacheable.h	Mon Nov 04 15:18:38 2019 +0100
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ICacheable : public boost::noncopyable
+  {
+  public:
+    virtual ~ICacheable()
+    {
+    }
+
+    virtual size_t GetMemoryUsage() const = 0;
+  };
+}
--- a/Core/Cache/MemoryCache.cpp	Thu Oct 31 14:00:39 2019 +0100
+++ b/Core/Cache/MemoryCache.cpp	Mon Nov 04 15:18:38 2019 +0100
@@ -38,71 +38,74 @@
 
 namespace Orthanc
 {
-  MemoryCache::Page& MemoryCache::Load(const std::string& id)
+  namespace Deprecated
   {
-    // Reuse the cache entry if it already exists
-    Page* p = NULL;
-    if (index_.Contains(id, p))
+    MemoryCache::Page& MemoryCache::Load(const std::string& id)
     {
-      VLOG(1) << "Reusing a cache page";
-      assert(p != NULL);
-      index_.MakeMostRecent(id);
+      // Reuse the cache entry if it already exists
+      Page* p = NULL;
+      if (index_.Contains(id, p))
+      {
+        VLOG(1) << "Reusing a cache page";
+        assert(p != NULL);
+        index_.MakeMostRecent(id);
+        return *p;
+      }
+
+      // The id is not in the cache yet. Make some room if the cache
+      // is full.
+      if (index_.GetSize() == cacheSize_)
+      {
+        VLOG(1) << "Dropping the oldest cache page";
+        index_.RemoveOldest(p);
+        delete p;
+      }
+
+      // Create a new cache page
+      std::auto_ptr<Page> result(new Page);
+      result->id_ = id;
+      result->content_.reset(provider_.Provide(id));
+
+      // Add the newly create page to the cache
+      VLOG(1) << "Registering new data in a cache page";
+      p = result.release();
+      index_.Add(id, p);
       return *p;
     }
 
-    // The id is not in the cache yet. Make some room if the cache
-    // is full.
-    if (index_.GetSize() == cacheSize_)
+    MemoryCache::MemoryCache(ICachePageProvider& provider,
+                             size_t cacheSize) : 
+      provider_(provider),
+      cacheSize_(cacheSize)
     {
-      VLOG(1) << "Dropping the oldest cache page";
-      index_.RemoveOldest(p);
-      delete p;
     }
 
-    // Create a new cache page
-    std::auto_ptr<Page> result(new Page);
-    result->id_ = id;
-    result->content_.reset(provider_.Provide(id));
-
-    // Add the newly create page to the cache
-    VLOG(1) << "Registering new data in a cache page";
-    p = result.release();
-    index_.Add(id, p);
-    return *p;
-  }
+    void MemoryCache::Invalidate(const std::string& id)
+    {
+      Page* p = NULL;
+      if (index_.Contains(id, p))
+      {
+        VLOG(1) << "Invalidating a cache page";
+        assert(p != NULL);
+        delete p;
+        index_.Invalidate(id);
+      }
+    }
 
-  MemoryCache::MemoryCache(ICachePageProvider& provider,
-                           size_t cacheSize) : 
-    provider_(provider),
-    cacheSize_(cacheSize)
-  {
-  }
+    MemoryCache::~MemoryCache()
+    {
+      while (!index_.IsEmpty())
+      {
+        Page* element = NULL;
+        index_.RemoveOldest(element);
+        assert(element != NULL);
+        delete element;
+      }
+    }
 
-  void MemoryCache::Invalidate(const std::string& id)
-  {
-    Page* p = NULL;
-    if (index_.Contains(id, p))
+    IDynamicObject& MemoryCache::Access(const std::string& id)
     {
-      VLOG(1) << "Invalidating a cache page";
-      assert(p != NULL);
-      delete p;
-      index_.Invalidate(id);
+      return *Load(id).content_;
     }
   }
-
-  MemoryCache::~MemoryCache()
-  {
-    while (!index_.IsEmpty())
-    {
-      Page* element = NULL;
-      index_.RemoveOldest(element);
-      assert(element != NULL);
-      delete element;
-    }
-  }
-
-  IDynamicObject& MemoryCache::Access(const std::string& id)
-  {
-    return *Load(id).content_;
-  }
 }
--- a/Core/Cache/MemoryCache.h	Thu Oct 31 14:00:39 2019 +0100
+++ b/Core/Cache/MemoryCache.h	Mon Nov 04 15:18:38 2019 +0100
@@ -39,32 +39,35 @@
 
 namespace Orthanc
 {
-  /**
-   * WARNING: This class is NOT thread-safe.
-   **/
-  class MemoryCache
+  namespace Deprecated
   {
-  private:
-    struct Page
+    /**
+     * WARNING: This class is NOT thread-safe.
+     **/
+    class MemoryCache
     {
-      std::string id_;
-      std::auto_ptr<IDynamicObject> content_;
-    };
+    private:
+      struct Page
+      {
+        std::string id_;
+        std::auto_ptr<IDynamicObject> content_;
+      };
 
-    ICachePageProvider& provider_;
-    size_t cacheSize_;
-    LeastRecentlyUsedIndex<std::string, Page*>  index_;
+      ICachePageProvider& provider_;
+      size_t cacheSize_;
+      LeastRecentlyUsedIndex<std::string, Page*>  index_;
 
-    Page& Load(const std::string& id);
+      Page& Load(const std::string& id);
 
-  public:
-    MemoryCache(ICachePageProvider& provider,
-                size_t cacheSize);
+    public:
+      MemoryCache(ICachePageProvider& provider,
+                  size_t cacheSize);
 
-    ~MemoryCache();
+      ~MemoryCache();
 
-    IDynamicObject& Access(const std::string& id);
+      IDynamicObject& Access(const std::string& id);
 
-    void Invalidate(const std::string& id);
-  };
+      void Invalidate(const std::string& id);
+    };
+  }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/MemoryObjectCache.cpp	Mon Nov 04 15:18:38 2019 +0100
@@ -0,0 +1,259 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryObjectCache.h"
+
+namespace Orthanc
+{
+  class MemoryObjectCache::Item : public boost::noncopyable
+  {
+  private:
+    ICacheable*               value_;
+    boost::posix_time::ptime  time_;
+
+  public:
+    explicit Item(ICacheable* value) :   // Takes ownership
+    value_(value),
+    time_(boost::posix_time::second_clock::local_time())
+    {
+      if (value == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    ~Item()
+    {
+      assert(value_ != NULL);
+      delete value_;
+    }
+
+    ICacheable& GetValue() const
+    {
+      assert(value_ != NULL);
+      return *value_;
+    }
+
+    const boost::posix_time::ptime& GetTime() const
+    {
+      return time_;
+    }
+  };
+
+
+  void MemoryObjectCache::Recycle(size_t targetSize)
+  {
+    // WARNING: "cacheMutex_" must be locked
+    while (currentSize_ > targetSize)
+    {
+      assert(!content_.IsEmpty());
+        
+      Item* item = NULL;
+      content_.RemoveOldest(item);
+
+      assert(item != NULL);
+      const size_t size = item->GetValue().GetMemoryUsage();
+      delete item;
+
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    // Post-condition: "currentSize_ <= targetSize"
+  }
+    
+
+  MemoryObjectCache::MemoryObjectCache() :
+    currentSize_(0),
+    maxSize_(100 * 1024 * 1024)  // 100 MB
+  {
+  }
+
+
+  MemoryObjectCache::~MemoryObjectCache()
+  {
+    Recycle(0);
+    assert(content_.IsEmpty());
+  }
+
+
+  size_t MemoryObjectCache::GetMaximumSize()
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(cacheMutex_);
+#endif
+
+    return maxSize_;
+  }
+
+
+  void MemoryObjectCache::SetMaximumSize(size_t size)
+  {
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+      
+#if !defined(__EMSCRIPTEN__)
+    // 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_);
+#endif
+
+    Recycle(size);
+    maxSize_ = size;
+  }
+
+
+  void MemoryObjectCache::Acquire(const std::string& key,
+                                  ICacheable* value)
+  {
+    std::auto_ptr<Item> item(new Item(value));
+
+    if (value == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+#if !defined(__EMSCRIPTEN__)
+      // 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_);
+#endif
+
+      const size_t size = item->GetValue().GetMemoryUsage();
+
+      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
+        content_.MakeMostRecent(key);
+      }
+      else
+      {
+        Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
+        assert(currentSize_ + size <= maxSize_);
+
+        content_.Add(key, item.release());
+        currentSize_ += size;
+      }
+    }
+  }
+
+
+  void MemoryObjectCache::Invalidate(const std::string& key)
+  {
+#if !defined(__EMSCRIPTEN__)
+    // Make sure no accessor is currently open (as it may correspond
+    // to the key to remove)
+    WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+#endif
+
+    Item* item = NULL;
+    if (content_.Contains(key, item))
+    {
+      assert(item != NULL);
+      const size_t size = item->GetValue().GetMemoryUsage();
+      delete item;
+
+      content_.Invalidate(key);
+          
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+  }
+
+
+  MemoryObjectCache::Reader::Reader(MemoryObjectCache& cache,
+                                    const std::string& key) :
+#if !defined(__EMSCRIPTEN__)
+    contentLock_(cache.contentMutex_),
+    cacheLock_(cache.cacheMutex_),
+#endif
+    item_(NULL)
+  {
+    if (cache.content_.Contains(key, item_))
+    {
+      cache.content_.MakeMostRecent(key);
+    }
+        
+#if !defined(__EMSCRIPTEN__)
+    cacheLock_.unlock();
+
+    if (item_ == NULL)
+    {
+      contentLock_.unlock();
+    }
+#endif
+  }
+
+
+  ICacheable& MemoryObjectCache::Reader::GetValue() const
+  {
+    if (IsValid())
+    {
+      return item_->GetValue();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const boost::posix_time::ptime& MemoryObjectCache::Reader::GetTime() const
+  {
+    if (IsValid())
+    {
+      return item_->GetTime();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }        
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/MemoryObjectCache.h	Mon Nov 04 15:18:38 2019 +0100
@@ -0,0 +1,109 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ICacheable.h"
+#include "LeastRecentlyUsedIndex.h"
+
+#if !defined(__EMSCRIPTEN__)
+// Multithreading is not supported in WebAssembly
+#  include <boost/thread/shared_mutex.hpp>
+#endif
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+
+namespace Orthanc
+{
+  class MemoryObjectCache : public boost::noncopyable
+  {
+  private:
+    class Item;
+
+#if !defined(__EMSCRIPTEN__)
+    typedef boost::unique_lock<boost::shared_mutex> WriterLock;
+    typedef boost::shared_lock<boost::shared_mutex> ReaderLock;
+
+    // This mutex protects modifications to the structure of the cache (monitor)
+    boost::mutex   cacheMutex_;
+
+    // This mutex protects modifications to the items that are stored in the cache
+    boost::shared_mutex contentMutex_;
+#endif
+
+    size_t currentSize_;
+    size_t maxSize_;
+    LeastRecentlyUsedIndex<std::string, Item*>  content_;
+
+    void Recycle(size_t targetSize);
+    
+  public:
+    MemoryObjectCache();
+
+    ~MemoryObjectCache();
+
+    size_t GetMaximumSize();
+
+    void SetMaximumSize(size_t size);
+
+    void Acquire(const std::string& key,
+                 ICacheable* value);
+
+    void Invalidate(const std::string& key);
+
+    class Reader : public boost::noncopyable
+    {
+    private:
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock                 contentLock_;
+      boost::mutex::scoped_lock  cacheLock_;
+#endif
+      
+      Item*         item_;
+
+    public:
+      Reader(MemoryObjectCache& cache,
+             const std::string& key);
+
+      bool IsValid() const
+      {
+        return item_ != NULL;
+      }
+
+      ICacheable& GetValue() const;
+
+      const boost::posix_time::ptime& GetTime() const;
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/MemoryStringCache.cpp	Mon Nov 04 15:18:38 2019 +0100
@@ -0,0 +1,84 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryStringCache.h"
+
+namespace Orthanc
+{
+  class MemoryStringCache::StringValue : public ICacheable
+  {
+  private:
+    std::string  content_;
+
+  public:
+    StringValue(const std::string& content) :
+      content_(content)
+    {
+    }
+      
+    const std::string& GetContent() const
+    {
+      return content_;
+    }
+
+    virtual size_t GetMemoryUsage() const
+    {
+      return content_.size();
+    }      
+  };
+
+
+  void MemoryStringCache::Add(const std::string& key,
+                              const std::string& value)
+  {
+    cache_.Acquire(key, new StringValue(value));
+  }
+
+  
+  bool MemoryStringCache::Fetch(std::string& value,
+                                const std::string& key)
+  {
+    MemoryObjectCache::Reader reader(cache_, key);
+
+    if (reader.IsValid())
+    {
+      value = dynamic_cast<StringValue&>(reader.GetValue()).GetContent();
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/MemoryStringCache.h	Mon Nov 04 15:18:38 2019 +0100
@@ -0,0 +1,73 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "MemoryObjectCache.h"
+
+namespace Orthanc
+{
+  /**
+   * Facade object around "MemoryObjectCache" that caches a dictionary
+   * of strings, using the "fetch/add" paradigm of memcached.
+   **/
+  class MemoryStringCache : public boost::noncopyable
+  {
+  private:
+    class StringValue;
+
+    MemoryObjectCache  cache_;
+
+  public:
+    size_t GetMaximumSize()
+    {
+      return cache_.GetMaximumSize();
+    }
+    
+    void SetMaximumSize(size_t size)
+    {
+      cache_.SetMaximumSize(size);
+    }
+
+    void Add(const std::string& key,
+             const std::string& value);
+    
+    void Invalidate(const std::string& key)
+    {
+      cache_.Invalidate(key);
+    }
+
+    bool Fetch(std::string& value,
+               const std::string& key);
+  };
+}
--- a/Core/Cache/SharedArchive.h	Thu Oct 31 14:00:39 2019 +0100
+++ b/Core/Cache/SharedArchive.h	Mon Nov 04 15:18:38 2019 +0100
@@ -57,7 +57,7 @@
     size_t         maxSize_;
     boost::mutex   mutex_;
     Archive        archive_;
-    Orthanc::LeastRecentlyUsedIndex<std::string> lru_;
+    LeastRecentlyUsedIndex<std::string> lru_;
 
     void RemoveInternal(const std::string& id);
 
--- a/OrthancServer/ServerContext.h	Thu Oct 31 14:00:39 2019 +0100
+++ b/OrthancServer/ServerContext.h	Mon Nov 04 15:18:38 2019 +0100
@@ -120,7 +120,7 @@
       }
     };
     
-    class DicomCacheProvider : public ICachePageProvider
+    class DicomCacheProvider : public Deprecated::ICachePageProvider  // TODO
     {
     private:
       ServerContext& context_;
@@ -186,7 +186,7 @@
     
     DicomCacheProvider provider_;
     boost::mutex dicomCacheMutex_;
-    MemoryCache dicomCache_;
+    Deprecated::MemoryCache dicomCache_;  // TODO
 
     LuaScripting mainLua_;
     LuaScripting filterLua_;
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Oct 31 14:00:39 2019 +0100
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Mon Nov 04 15:18:38 2019 +0100
@@ -123,6 +123,8 @@
 
 set(ORTHANC_CORE_SOURCES_INTERNAL
   ${ORTHANC_ROOT}/Core/Cache/MemoryCache.cpp
+  ${ORTHANC_ROOT}/Core/Cache/MemoryObjectCache.cpp
+  ${ORTHANC_ROOT}/Core/Cache/MemoryStringCache.cpp
   ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
   ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
   ${ORTHANC_ROOT}/Core/EnumerationDictionary.h
--- a/UnitTestsSources/MemoryCacheTests.cpp	Thu Oct 31 14:00:39 2019 +0100
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Mon Nov 04 15:18:38 2019 +0100
@@ -40,6 +40,7 @@
 #include <boost/lexical_cast.hpp>
 
 #include "../Core/Cache/MemoryCache.h"
+#include "../Core/Cache/MemoryStringCache.h"
 #include "../Core/Cache/SharedArchive.h"
 #include "../Core/IDynamicObject.h"
 #include "../Core/Logging.h"
@@ -212,7 +213,7 @@
     }
   };
 
-  class IntegerProvider : public Orthanc::ICachePageProvider
+  class IntegerProvider : public Orthanc::Deprecated::ICachePageProvider
   {
   public:
     std::string log_;
@@ -231,7 +232,7 @@
   IntegerProvider provider;
 
   {
-    Orthanc::MemoryCache cache(provider, 3);
+    Orthanc::Deprecated::MemoryCache cache(provider, 3);
     cache.Access("42");  // 42 -> exit
     cache.Access("43");  // 43, 42 -> exit
     cache.Access("45");  // 45, 43, 42 -> exit
@@ -317,3 +318,51 @@
 
   ASSERT_EQ(2u, count);
 }
+
+
+TEST(MemoryStringCache, Basic)
+{
+  Orthanc::MemoryStringCache c;
+  ASSERT_THROW(c.SetMaximumSize(0), Orthanc::OrthancException);
+  
+  c.SetMaximumSize(2);
+
+  std::string v;
+  ASSERT_FALSE(c.Fetch(v, "hello"));
+
+  c.Add("hello", "a");
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_FALSE(c.Fetch(v, "hello2"));
+  ASSERT_FALSE(c.Fetch(v, "hello3"));
+
+  c.Add("hello2", "b");
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_FALSE(c.Fetch(v, "hello3"));
+
+  c.Add("hello3", "too large value");
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_FALSE(c.Fetch(v, "hello3"));
+  
+  c.Add("hello3", "c");
+  ASSERT_FALSE(c.Fetch(v, "hello"));  // Recycled
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_TRUE(c.Fetch(v, "hello3"));  ASSERT_EQ("c", v);
+}
+
+
+TEST(MemoryStringCache, Invalidate)
+{
+  Orthanc::MemoryStringCache c;
+  c.Add("hello", "a");
+  c.Add("hello2", "b");
+
+  std::string v;
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+
+  c.Invalidate("hello");
+  ASSERT_FALSE(c.Fetch(v, "hello"));
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+}