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