1 /** 2 * Orthanc - A Lightweight, RESTful DICOM Store 3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics 4 * Department, University Hospital of Liege, Belgium 5 * Copyright (C) 2017-2020 Osimis S.A., Belgium 6 * 7 * This program is free software: you can redistribute it and/or 8 * modify it under the terms of the GNU Affero General Public License 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * This program is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 * Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with this program. If not, see <http://www.gnu.org/licenses/>. 19 **/ 20 21 22 #include "CacheManager.h" 23 24 #include <Compatibility.h> 25 #include <Toolbox.h> 26 #include <SQLite/Transaction.h> 27 28 #include <boost/lexical_cast.hpp> 29 30 31 namespace OrthancPlugins 32 { 33 class CacheManager::Bundle 34 { 35 private: 36 uint32_t count_; 37 uint64_t space_; 38 39 public: Bundle()40 Bundle() : count_(0), space_(0) 41 { 42 } 43 Bundle(uint32_t count,uint64_t space)44 Bundle(uint32_t count, 45 uint64_t space) : 46 count_(count), space_(space) 47 { 48 } 49 GetCount() const50 uint32_t GetCount() const 51 { 52 return count_; 53 } 54 GetSpace() const55 uint64_t GetSpace() const 56 { 57 return space_; 58 } 59 Remove(uint64_t fileSize)60 void Remove(uint64_t fileSize) 61 { 62 if (count_ == 0 || 63 space_ < fileSize) 64 { 65 throw std::runtime_error("Internal error"); 66 } 67 68 count_ -= 1; 69 space_ -= fileSize; 70 } 71 Add(uint64_t fileSize)72 void Add(uint64_t fileSize) 73 { 74 count_ += 1; 75 space_ += fileSize; 76 } 77 }; 78 79 80 class CacheManager::BundleQuota 81 { 82 private: 83 uint32_t maxCount_; 84 uint64_t maxSpace_; 85 86 public: BundleQuota(uint32_t maxCount,uint64_t maxSpace)87 BundleQuota(uint32_t maxCount, 88 uint64_t maxSpace) : 89 maxCount_(maxCount), maxSpace_(maxSpace) 90 { 91 } 92 BundleQuota()93 BundleQuota() 94 { 95 // Default quota 96 maxCount_ = 0; // No limit on the number of files 97 maxSpace_ = 100 * 1024 * 1024; // Max 100MB per bundle 98 } 99 GetMaxCount() const100 uint32_t GetMaxCount() const 101 { 102 return maxCount_; 103 } 104 GetMaxSpace() const105 uint64_t GetMaxSpace() const 106 { 107 return maxSpace_; 108 } 109 IsSatisfied(const Bundle & bundle) const110 bool IsSatisfied(const Bundle& bundle) const 111 { 112 if (maxCount_ != 0 && 113 bundle.GetCount() > maxCount_) 114 { 115 return false; 116 } 117 118 if (maxSpace_ != 0 && 119 bundle.GetSpace() > maxSpace_) 120 { 121 return false; 122 } 123 124 return true; 125 } 126 }; 127 128 129 struct CacheManager::PImpl 130 { 131 OrthancPluginContext* context_; 132 Orthanc::SQLite::Connection& db_; 133 Orthanc::FilesystemStorage& storage_; 134 135 bool sanityCheck_; 136 Bundles bundles_; 137 BundleQuota defaultQuota_; 138 BundleQuotas quotas_; 139 PImplOrthancPlugins::CacheManager::PImpl140 PImpl(OrthancPluginContext* context, 141 Orthanc::SQLite::Connection& db, 142 Orthanc::FilesystemStorage& storage) : 143 context_(context), 144 db_(db), 145 storage_(storage), 146 sanityCheck_(false) 147 { 148 } 149 }; 150 151 GetBundleQuota(int bundleIndex) const152 const CacheManager::BundleQuota& CacheManager::GetBundleQuota(int bundleIndex) const 153 { 154 BundleQuotas::const_iterator found = pimpl_->quotas_.find(bundleIndex); 155 156 if (found == pimpl_->quotas_.end()) 157 { 158 return pimpl_->defaultQuota_; 159 } 160 else 161 { 162 return found->second; 163 } 164 } 165 166 GetBundle(int bundleIndex) const167 CacheManager::Bundle CacheManager::GetBundle(int bundleIndex) const 168 { 169 Bundles::const_iterator it = pimpl_->bundles_.find(bundleIndex); 170 171 if (it == pimpl_->bundles_.end()) 172 { 173 return Bundle(); 174 } 175 else 176 { 177 return it->second; 178 } 179 } 180 181 MakeRoom(Bundle & bundle,std::list<std::string> & toRemove,int bundleIndex,const BundleQuota & quota)182 void CacheManager::MakeRoom(Bundle& bundle, 183 std::list<std::string>& toRemove, 184 int bundleIndex, 185 const BundleQuota& quota) 186 { 187 using namespace Orthanc; 188 189 toRemove.clear(); 190 191 // Make room in the bundle 192 while (!quota.IsSatisfied(bundle)) 193 { 194 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? ORDER BY seq"); 195 s.BindInt(0, bundleIndex); 196 197 if (s.Step()) 198 { 199 SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); 200 t.BindInt64(0, s.ColumnInt64(0)); 201 t.Run(); 202 203 toRemove.push_back(s.ColumnString(1)); 204 bundle.Remove(s.ColumnInt64(2)); 205 } 206 else 207 { 208 // Should never happen 209 throw std::runtime_error("Internal error"); 210 } 211 } 212 } 213 214 215 EnsureQuota(int bundleIndex,const BundleQuota & quota)216 void CacheManager::EnsureQuota(int bundleIndex, 217 const BundleQuota& quota) 218 { 219 using namespace Orthanc; 220 221 // Remove the cached files that exceed the quota 222 std::unique_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); 223 transaction->Begin(); 224 225 Bundle bundle = GetBundle(bundleIndex); 226 227 std::list<std::string> toRemove; 228 MakeRoom(bundle, toRemove, bundleIndex, quota); 229 230 transaction->Commit(); 231 for (std::list<std::string>::const_iterator 232 it = toRemove.begin(); it != toRemove.end(); ++it) 233 { 234 pimpl_->storage_.Remove(*it, Orthanc::FileContentType_Unknown); 235 } 236 237 pimpl_->bundles_[bundleIndex] = bundle; 238 } 239 240 241 ReadBundleStatistics()242 void CacheManager::ReadBundleStatistics() 243 { 244 using namespace Orthanc; 245 246 pimpl_->bundles_.clear(); 247 248 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT bundle,COUNT(*),SUM(fileSize) FROM Cache GROUP BY bundle"); 249 while (s.Step()) 250 { 251 int index = s.ColumnInt(0); 252 Bundle bundle(static_cast<uint32_t>(s.ColumnInt(1)), 253 static_cast<uint64_t>(s.ColumnInt64(2))); 254 pimpl_->bundles_[index] = bundle; 255 } 256 } 257 258 259 SanityCheck()260 void CacheManager::SanityCheck() 261 { 262 if (!pimpl_->sanityCheck_) 263 { 264 return; 265 } 266 267 using namespace Orthanc; 268 269 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT bundle,COUNT(*),SUM(fileSize) FROM Cache GROUP BY bundle"); 270 while (s.Step()) 271 { 272 const Bundle& bundle = GetBundle(s.ColumnInt(0)); 273 if (bundle.GetCount() != static_cast<uint32_t>(s.ColumnInt(1)) || 274 bundle.GetSpace() != static_cast<uint64_t>(s.ColumnInt64(2))) 275 { 276 throw std::runtime_error("SANITY ERROR in cache: " + boost::lexical_cast<std::string>(bundle.GetCount()) 277 + "/" + boost::lexical_cast<std::string>(bundle.GetSpace()) 278 + " vs " + boost::lexical_cast<std::string>(s.ColumnInt(1)) + "/" 279 + boost::lexical_cast<std::string>(s.ColumnInt64(2))); 280 } 281 } 282 } 283 284 285 CacheManager(OrthancPluginContext * context,Orthanc::SQLite::Connection & db,Orthanc::FilesystemStorage & storage)286 CacheManager::CacheManager(OrthancPluginContext* context, 287 Orthanc::SQLite::Connection& db, 288 Orthanc::FilesystemStorage& storage) : 289 pimpl_(new PImpl(context, db, storage)) 290 { 291 Open(); 292 ReadBundleStatistics(); 293 } 294 295 GetPluginContext() const296 OrthancPluginContext* CacheManager::GetPluginContext() const 297 { 298 return pimpl_->context_; 299 } 300 301 SetSanityCheckEnabled(bool enabled)302 void CacheManager::SetSanityCheckEnabled(bool enabled) 303 { 304 pimpl_->sanityCheck_ = enabled; 305 } 306 307 Open()308 void CacheManager::Open() 309 { 310 if (!pimpl_->db_.DoesTableExist("Cache")) 311 { 312 pimpl_->db_.Execute("CREATE TABLE Cache(seq INTEGER PRIMARY KEY, bundle INTEGER, item TEXT, fileUuid TEXT, fileSize INT);"); 313 pimpl_->db_.Execute("CREATE INDEX CacheBundles ON Cache(bundle);"); 314 pimpl_->db_.Execute("CREATE INDEX CacheIndex ON Cache(bundle, item);"); 315 } 316 317 if (!pimpl_->db_.DoesTableExist("CacheProperties")) 318 { 319 pimpl_->db_.Execute("CREATE TABLE CacheProperties(property INTEGER PRIMARY KEY, value TEXT);"); 320 } 321 322 // Performance tuning of SQLite with PRAGMAs 323 // http://www.sqlite.org/pragma.html 324 pimpl_->db_.Execute("PRAGMA SYNCHRONOUS=OFF;"); 325 pimpl_->db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); 326 pimpl_->db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); 327 } 328 329 Store(int bundleIndex,const std::string & item,const std::string & content)330 void CacheManager::Store(int bundleIndex, 331 const std::string& item, 332 const std::string& content) 333 { 334 SanityCheck(); 335 336 const BundleQuota quota = GetBundleQuota(bundleIndex); 337 338 if (quota.GetMaxSpace() > 0 && 339 content.size() > quota.GetMaxSpace()) 340 { 341 // Cannot store such a large instance into the cache, forget about it 342 return; 343 } 344 345 using namespace Orthanc; 346 347 std::unique_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); 348 transaction->Begin(); 349 350 Bundle bundle = GetBundle(bundleIndex); 351 352 std::list<std::string> toRemove; 353 bundle.Add(content.size()); 354 MakeRoom(bundle, toRemove, bundleIndex, quota); 355 356 // Store the cached content on the disk 357 const char* data = content.size() ? &content[0] : NULL; 358 std::string uuid = Toolbox::GenerateUuid(); 359 pimpl_->storage_.Create(uuid, data, content.size(), Orthanc::FileContentType_Unknown); 360 361 // Remove the previous cached value. This might happen if the same 362 // item is accessed very quickly twice: Another factory could have 363 // been cached a value before the check for existence in Access(). 364 { 365 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? AND item=?"); 366 s.BindInt(0, bundleIndex); 367 s.BindString(1, item); 368 if (s.Step()) 369 { 370 SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); 371 t.BindInt64(0, s.ColumnInt64(0)); 372 t.Run(); 373 374 toRemove.push_back(s.ColumnString(1)); 375 bundle.Remove(s.ColumnInt64(2)); 376 } 377 } 378 379 { 380 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "INSERT INTO Cache VALUES(NULL, ?, ?, ?, ?)"); 381 s.BindInt(0, bundleIndex); 382 s.BindString(1, item); 383 s.BindString(2, uuid); 384 s.BindInt64(3, content.size()); 385 386 if (!s.Run()) 387 { 388 // Error: Remove the stored file 389 pimpl_->storage_.Remove(uuid, Orthanc::FileContentType_Unknown); 390 } 391 else 392 { 393 transaction->Commit(); 394 395 pimpl_->bundles_[bundleIndex] = bundle; 396 397 for (std::list<std::string>::const_iterator 398 it = toRemove.begin(); it != toRemove.end(); ++it) 399 { 400 pimpl_->storage_.Remove(*it, Orthanc::FileContentType_Unknown); 401 } 402 } 403 } 404 405 SanityCheck(); 406 } 407 408 409 LocateInCache(std::string & uuid,uint64_t & size,int bundle,const std::string & item)410 bool CacheManager::LocateInCache(std::string& uuid, 411 uint64_t& size, 412 int bundle, 413 const std::string& item) 414 { 415 using namespace Orthanc; 416 SanityCheck(); 417 418 std::unique_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); 419 transaction->Begin(); 420 421 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? AND item=?"); 422 s.BindInt(0, bundle); 423 s.BindString(1, item); 424 if (!s.Step()) 425 { 426 return false; 427 } 428 429 int64_t seq = s.ColumnInt64(0); 430 uuid = s.ColumnString(1); 431 size = s.ColumnInt64(2); 432 433 // Touch the cache to fulfill the LRU scheme. 434 SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); 435 t.BindInt64(0, seq); 436 if (t.Run()) 437 { 438 SQLite::Statement u(pimpl_->db_, SQLITE_FROM_HERE, "INSERT INTO Cache VALUES(NULL, ?, ?, ?, ?)"); 439 u.BindInt(0, bundle); 440 u.BindString(1, item); 441 u.BindString(2, uuid); 442 u.BindInt64(3, size); 443 if (u.Run()) 444 { 445 // Everything was OK. Commit the changes to the cache. 446 transaction->Commit(); 447 return true; 448 } 449 } 450 451 return false; 452 } 453 454 IsCached(int bundle,const std::string & item)455 bool CacheManager::IsCached(int bundle, 456 const std::string& item) 457 { 458 std::string uuid; 459 uint64_t size; 460 return LocateInCache(uuid, size, bundle, item); 461 } 462 463 Access(std::string & content,int bundle,const std::string & item)464 bool CacheManager::Access(std::string& content, 465 int bundle, 466 const std::string& item) 467 { 468 std::string uuid; 469 uint64_t size; 470 if (!LocateInCache(uuid, size, bundle, item)) 471 { 472 return false; 473 } 474 475 bool ok; 476 try 477 { 478 pimpl_->storage_.Read(content, uuid, Orthanc::FileContentType_Unknown); 479 ok = (content.size() == size); 480 } 481 catch (std::runtime_error&) 482 { 483 ok = false; 484 } 485 486 if (ok) 487 { 488 return true; 489 } 490 else 491 { 492 throw std::runtime_error("Error in the filesystem"); 493 } 494 } 495 496 Invalidate(int bundleIndex,const std::string & item)497 void CacheManager::Invalidate(int bundleIndex, 498 const std::string& item) 499 { 500 using namespace Orthanc; 501 SanityCheck(); 502 503 std::unique_ptr<SQLite::Transaction> transaction(new SQLite::Transaction(pimpl_->db_)); 504 transaction->Begin(); 505 506 Bundle bundle = GetBundle(bundleIndex); 507 508 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT seq, fileUuid, fileSize FROM Cache WHERE bundle=? AND item=?"); 509 s.BindInt(0, bundleIndex); 510 s.BindString(1, item); 511 if (s.Step()) 512 { 513 int64_t seq = s.ColumnInt64(0); 514 const std::string uuid = s.ColumnString(1); 515 uint64_t expectedSize = s.ColumnInt64(2); 516 bundle.Remove(expectedSize); 517 518 SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE seq=?"); 519 t.BindInt64(0, seq); 520 if (t.Run()) 521 { 522 transaction->Commit(); 523 pimpl_->bundles_[bundleIndex] = bundle; 524 pimpl_->storage_.Remove(uuid, Orthanc::FileContentType_Unknown); 525 } 526 } 527 } 528 529 530 SetBundleQuota(int bundle,uint32_t maxCount,uint64_t maxSpace)531 void CacheManager::SetBundleQuota(int bundle, 532 uint32_t maxCount, 533 uint64_t maxSpace) 534 { 535 SanityCheck(); 536 537 const BundleQuota quota(maxCount, maxSpace); 538 EnsureQuota(bundle, quota); 539 pimpl_->quotas_[bundle] = quota; 540 541 SanityCheck(); 542 } 543 SetDefaultQuota(uint32_t maxCount,uint64_t maxSpace)544 void CacheManager::SetDefaultQuota(uint32_t maxCount, 545 uint64_t maxSpace) 546 { 547 using namespace Orthanc; 548 SanityCheck(); 549 550 pimpl_->defaultQuota_ = BundleQuota(maxCount, maxSpace); 551 552 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT DISTINCT bundle FROM Cache"); 553 while (s.Step()) 554 { 555 EnsureQuota(s.ColumnInt(0), pimpl_->defaultQuota_); 556 } 557 558 SanityCheck(); 559 } 560 561 Clear()562 void CacheManager::Clear() 563 { 564 using namespace Orthanc; 565 SanityCheck(); 566 567 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT fileUuid FROM Cache"); 568 while (s.Step()) 569 { 570 pimpl_->storage_.Remove(s.ColumnString(0), Orthanc::FileContentType_Unknown); 571 } 572 573 SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache"); 574 t.Run(); 575 576 ReadBundleStatistics(); 577 SanityCheck(); 578 } 579 580 581 Clear(int bundle)582 void CacheManager::Clear(int bundle) 583 { 584 using namespace Orthanc; 585 SanityCheck(); 586 587 SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, "SELECT fileUuid FROM Cache WHERE bundle=?"); 588 s.BindInt(0, bundle); 589 while (s.Step()) 590 { 591 pimpl_->storage_.Remove(s.ColumnString(0), Orthanc::FileContentType_Unknown); 592 } 593 594 SQLite::Statement t(pimpl_->db_, SQLITE_FROM_HERE, "DELETE FROM Cache WHERE bundle=?"); 595 t.BindInt(0, bundle); 596 t.Run(); 597 598 ReadBundleStatistics(); 599 SanityCheck(); 600 } 601 602 SetProperty(CacheProperty property,const std::string & value)603 void CacheManager::SetProperty(CacheProperty property, 604 const std::string& value) 605 { 606 Orthanc::SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, 607 "INSERT OR REPLACE INTO CacheProperties VALUES(?, ?)"); 608 s.BindInt(0, property); 609 s.BindString(1, value); 610 s.Run(); 611 } 612 613 LookupProperty(std::string & target,CacheProperty property)614 bool CacheManager::LookupProperty(std::string& target, 615 CacheProperty property) 616 { 617 Orthanc::SQLite::Statement s(pimpl_->db_, SQLITE_FROM_HERE, 618 "SELECT value FROM CacheProperties WHERE property=?"); 619 s.BindInt(0, property); 620 621 if (!s.Step()) 622 { 623 return false; 624 } 625 else 626 { 627 target = s.ColumnString(0); 628 return true; 629 } 630 } 631 } 632