Mercurial > hg > orthanc-webviewer
diff Plugin/Cache/CacheManager.cpp @ 0:02f7a0400a91
initial commit
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 25 Feb 2015 13:45:35 +0100 |
parents | |
children | 7a0af291cc90 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugin/Cache/CacheManager.cpp Wed Feb 25 13:45:35 2015 +0100 @@ -0,0 +1,592 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "CacheManager.h" + +#include "../../Orthanc/Uuid.h" +#include "../../Orthanc/SQLite/Transaction.h" + +#include <boost/lexical_cast.hpp> + + +namespace OrthancPlugins +{ + class CacheManager::Bundle + { + private: + uint32_t count_; + uint64_t space_; + + public: + Bundle() : count_(0), space_(0) + { + } + + Bundle(uint32_t count, + uint64_t space) : + count_(count), space_(space) + { + } + + uint32_t GetCount() const + { + return count_; + } + + uint64_t GetSpace() const + { + return space_; + } + + void Remove(uint64_t fileSize) + { + if (count_ == 0 || + space_ < fileSize) + { + throw std::runtime_error("Internal error"); + } + + count_ -= 1; + space_ -= fileSize; + } + + void Add(uint64_t fileSize) + { + count_ += 1; + space_ += fileSize; + } + }; + + + class CacheManager::BundleQuota + { + private: + uint32_t maxCount_; + uint64_t maxSpace_; + + public: + BundleQuota(uint32_t maxCount, + uint64_t maxSpace) : + maxCount_(maxCount), maxSpace_(maxSpace) + { + } + + BundleQuota() + { + // Default quota + maxCount_ = 0; // No limit on the number of files + maxSpace_ = 100 * 1024 * 1024; // Max 100MB per bundle + } + + uint32_t GetMaxCount() const + { + return maxCount_; + } + + uint64_t GetMaxSpace() const + { + return maxSpace_; + } + + bool IsSatisfied(const Bundle& bundle) const + { + if (maxCount_ != 0 && + bundle.GetCount() > maxCount_) + { + return false; + } + + if (maxSpace_ != 0 && + bundle.GetSpace() > maxSpace_) + { + return false; + } + + return true; + } + }; + + + struct CacheManager::PImpl + { + Orthanc::SQLite::Connection& db_; + Orthanc::FilesystemStorage& storage_; + + bool sanityCheck_; + Bundles bundles_; + BundleQuota defaultQuota_; + BundleQuotas quotas_; + + PImpl(Orthanc::SQLite::Connection& db, + Orthanc::FilesystemStorage& storage) : + db_(db), + storage_(storage), + sanityCheck_(false) + { + } + }; + + + const CacheManager::BundleQuota& CacheManager::GetBundleQuota(int bundleIndex) const + { + BundleQuotas::const_iterator found = pimpl_->quotas_.find(bundleIndex); + + if (found == pimpl_->quotas_.end()) + { + return pimpl_->defaultQuota_; + } + else + { + return found->second; + } + } + + + CacheManager::Bundle CacheManager::GetBundle(int bundleIndex) const + { + Bundles::const_iterator it = pimpl_->bundles_.find(bundleIndex); + + if (it == pimpl_->bundles_.end()) + { + return Bundle(); + } + else + { + return it->second; + } + } + + + void CacheManager::MakeRoom(Bundle& bundle, + std::list<std::string>& toRemove, + int bundleIndex, + const BundleQuota& quota) + { + using namespace Orthanc; + + toRemove.clear(); + + // Make room in the bundle + while (!quota.IsSatisfied(bundle)) + { + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? ORDER BY seq"); + s.BindInt(0, bundleIndex); + + if (s.Step()) + { + SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); + t.BindInt64(0, s.ColumnInt64(0)); + t.Run(); + + toRemove.push_back(s.ColumnString(1)); + bundle.Remove(s.ColumnInt64(2)); + } + else + { + // Should never happen + throw std::runtime_error("Internal error"); + } + } + } + + + + void CacheManager::EnsureQuota(int bundleIndex, + const BundleQuota& quota) + { + using namespace Orthanc; + + // Remove the cached files that exceed the quota + std::auto_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); + transaction->Begin(); + + Bundle bundle = GetBundle(bundleIndex); + + std::list<std::string> toRemove; + MakeRoom(bundle, toRemove, bundleIndex, quota); + + transaction->Commit(); + for (std::list<std::string>::const_iterator + it = toRemove.begin(); it != toRemove.end(); it++) + { + pimpl_->storage_.Remove(*it); + } + + pimpl_->bundles_[bundleIndex] = bundle; + } + + + + void CacheManager::ReadBundleStatistics() + { + using namespace Orthanc; + + pimpl_->bundles_.clear(); + + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT bundle,COUNT(*),SUM(fileSize) FROM Cache GROUP BY bundle"); + while (s.Step()) + { + int index = s.ColumnInt(0); + Bundle bundle(static_cast<uint32_t>(s.ColumnInt(1)), + static_cast<uint64_t>(s.ColumnInt64(2))); + pimpl_->bundles_[index] = bundle; + } + } + + + + void CacheManager::SanityCheck() + { + if (!pimpl_->sanityCheck_) + { + return; + } + + using namespace Orthanc; + + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT bundle,COUNT(*),SUM(fileSize) FROM Cache GROUP BY bundle"); + while (s.Step()) + { + const Bundle& bundle = GetBundle(s.ColumnInt(0)); + if (bundle.GetCount() != static_cast<uint32_t>(s.ColumnInt(1)) || + bundle.GetSpace() != static_cast<uint64_t>(s.ColumnInt64(2))) + { + throw std::runtime_error("SANITY ERROR in cache: " + boost::lexical_cast<std::string>(bundle.GetCount()) + + "/" + boost::lexical_cast<std::string>(bundle.GetSpace()) + + " vs " + boost::lexical_cast<std::string>(s.ColumnInt(1)) + "/" + + boost::lexical_cast<std::string>(s.ColumnInt64(2))); + } + } + } + + + + CacheManager::CacheManager(Orthanc::SQLite::Connection& db, + Orthanc::FilesystemStorage& storage) : + pimpl_(new PImpl(db, storage)) + { + Open(); + ReadBundleStatistics(); + } + + + void CacheManager::SetSanityCheckEnabled(bool enabled) + { + pimpl_->sanityCheck_ = enabled; + } + + + void CacheManager::Open() + { + if (!pimpl_->db_.DoesTableExist("Cache")) + { + pimpl_->db_.Execute("CREATE TABLE Cache(seq INTEGER PRIMARY KEY, bundle INTEGER, item TEXT, fileUuid TEXT, fileSize INT);"); + pimpl_->db_.Execute("CREATE INDEX CacheBundles ON Cache(bundle);"); + pimpl_->db_.Execute("CREATE INDEX CacheIndex ON Cache(bundle, item);"); + } + + // Performance tuning of SQLite with PRAGMAs + // http://www.sqlite.org/pragma.html + pimpl_->db_.Execute("PRAGMA SYNCHRONOUS=OFF;"); + pimpl_->db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); + pimpl_->db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); + } + + + void CacheManager::Store(int bundleIndex, + const std::string& item, + const std::string& content) + { + SanityCheck(); + + const BundleQuota quota = GetBundleQuota(bundleIndex); + + if (quota.GetMaxSpace() > 0 && + content.size() > quota.GetMaxSpace()) + { + // Cannot store such a large instance into the cache, forget about it + return; + } + + using namespace Orthanc; + + std::auto_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); + transaction->Begin(); + + Bundle bundle = GetBundle(bundleIndex); + + std::list<std::string> toRemove; + bundle.Add(content.size()); + MakeRoom(bundle, toRemove, bundleIndex, quota); + + // Store the cached content on the disk + const char* data = content.size() ? &content[0] : NULL; + std::string uuid = Toolbox::GenerateUuid(); + pimpl_->storage_.Create(uuid, data, content.size()); + + bool ok = true; + + // Remove the previous cached value. This might happen if the same + // item is accessed very quickly twice: Another factory could have + // been cached a value before the check for existence in Access(). + { + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? AND item=?"); + s.BindInt(0, bundleIndex); + s.BindString(1, item); + if (s.Step()) + { + SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); + t.BindInt64(0, s.ColumnInt64(0)); + t.Run(); + + toRemove.push_back(s.ColumnString(1)); + bundle.Remove(s.ColumnInt64(2)); + } + } + + if (ok) + { + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "INSERT INTO Cache VALUES(NULL, ?, ?, ?, ?)"); + s.BindInt(0, bundleIndex); + s.BindString(1, item); + s.BindString(2, uuid); + s.BindInt64(3, content.size()); + + if (!s.Run()) + { + ok = false; + } + } + + if (!ok) + { + // Error: Remove the stored file + pimpl_->storage_.Remove(uuid); + } + else + { + transaction->Commit(); + + pimpl_->bundles_[bundleIndex] = bundle; + + for (std::list<std::string>::const_iterator + it = toRemove.begin(); it != toRemove.end(); it++) + { + pimpl_->storage_.Remove(*it); + } + } + + SanityCheck(); + } + + + + bool CacheManager::LocateInCache(std::string& uuid, + uint64_t& size, + int bundle, + const std::string& item) + { + using namespace Orthanc; + SanityCheck(); + + std::auto_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); + transaction->Begin(); + + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? AND item=?"); + s.BindInt(0, bundle); + s.BindString(1, item); + if (!s.Step()) + { + return false; + } + + int64_t seq = s.ColumnInt64(0); + uuid = s.ColumnString(1); + size = s.ColumnInt64(2); + + // Touch the cache to fulfill the LRU scheme. + SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); + t.BindInt64(0, seq); + if (t.Run()) + { + SQLite::Statement u(pimpl_->db_, SQLITE_FROM_HERE, "INSERT INTO Cache VALUES(NULL, ?, ?, ?, ?)"); + u.BindInt(0, bundle); + u.BindString(1, item); + u.BindString(2, uuid); + u.BindInt64(3, size); + if (u.Run()) + { + // Everything was OK. Commit the changes to the cache. + transaction->Commit(); + return true; + } + } + + return false; + } + + + bool CacheManager::IsCached(int bundle, + const std::string& item) + { + std::string uuid; + uint64_t size; + return LocateInCache(uuid, size, bundle, item); + } + + + bool CacheManager::Access(std::string& content, + int bundle, + const std::string& item) + { + std::string uuid; + uint64_t size; + if (!LocateInCache(uuid, size, bundle, item)) + { + return false; + } + + bool ok; + try + { + pimpl_->storage_.Read(content, uuid); + ok = (content.size() == size); + } + catch (std::runtime_error&) + { + ok = false; + } + + if (ok) + { + return true; + } + else + { + throw std::runtime_error("Error in the filesystem"); + } + } + + + void CacheManager::Invalidate(int bundleIndex, + const std::string& item) + { + using namespace Orthanc; + SanityCheck(); + + std::auto_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); + transaction->Begin(); + + Bundle bundle = GetBundle(bundleIndex); + + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? AND item=?"); + s.BindInt(0, bundleIndex); + s.BindString(1, item); + if (s.Step()) + { + int64_t seq = s.ColumnInt64(0); + const std::string uuid = s.ColumnString(1); + uint64_t expectedSize = s.ColumnInt64(2); + bundle.Remove(expectedSize); + + SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); + t.BindInt64(0, seq); + if (t.Run()) + { + transaction->Commit(); + pimpl_->bundles_[bundleIndex] = bundle; + pimpl_->storage_.Remove(uuid); + } + } + } + + + + void CacheManager::SetBundleQuota(int bundle, + uint32_t maxCount, + uint64_t maxSpace) + { + SanityCheck(); + + const BundleQuota quota(maxCount, maxSpace); + EnsureQuota(bundle, quota); + pimpl_->quotas_[bundle] = quota; + + SanityCheck(); + } + + void CacheManager::SetDefaultQuota(uint32_t maxCount, + uint64_t maxSpace) + { + using namespace Orthanc; + SanityCheck(); + + pimpl_->defaultQuota_ = BundleQuota(maxCount, maxSpace); + + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT DISTINCT bundle FROM Cache"); + while (s.Step()) + { + EnsureQuota(s.ColumnInt(0), pimpl_->defaultQuota_); + } + + SanityCheck(); + } + + + void CacheManager::Clear() + { + using namespace Orthanc; + SanityCheck(); + + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT fileUuid FROM Cache"); + while (s.Step()) + { + pimpl_->storage_.Remove(s.ColumnString(0)); + } + + SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache"); + t.Run(); + + ReadBundleStatistics(); + SanityCheck(); + } + + + + void CacheManager::Clear(int bundle) + { + using namespace Orthanc; + SanityCheck(); + + SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT fileUuid FROM Cache WHERE bundle=?"); + s.BindInt(0, bundle); + while (s.Step()) + { + pimpl_->storage_.Remove(s.ColumnString(0)); + } + + SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE bundle=?"); + t.BindInt(0, bundle); + t.Run(); + + ReadBundleStatistics(); + SanityCheck(); + } +}