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