Mercurial > hg > orthanc-webviewer
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 } |