16 DiskPersistence::isCompressibleKey(
const std::string& key)
19 return simox::alg::ends_with(key,
".json");
23 DiskPersistence::toLogicalKey(
const std::string& key)
34 const std::string& writtenKey)
36 const std::string staleKey = simox::alg::ends_with(writtenKey,
GZIP_SUFFIX)
37 ? toLogicalKey(writtenKey)
43 std::filesystem::remove(getFullPath(
id) / staleKey, ec);
49 <<
" in " << getFullPath(
id) <<
": " << ec.message()
50 <<
". Reads of this item may return stale content.";
54 std::vector<unsigned char>
56 const std::string& compressedKey)
62 catch (
const std::exception& e)
64 ARMARX_ERROR <<
"Could not decompress " <<
id.str() <<
"/" << compressedKey <<
": "
66 return std::vector<unsigned char>();
70 std::vector<std::string>
73 std::vector<std::string> containers;
81 if (!
id.hasEntityName() ||
id.hasTimestamp())
83 std::vector<std::filesystem::path> dirs = getAllDirectories(
id);
86 for (
auto& path : dirs)
88 std::string container = path.filename().string();
90 if (!container.empty())
92 containers.emplace_back(container);
99 std::vector<std::filesystem::path> dayDirs = getAllDirectories(
id);
101 for (std::filesystem::path& dayDir : dayDirs)
105 ARMARX_WARNING <<
"Found a non-date folder inside an entity '" <<
id.str()
106 <<
"' with name '" << dayDir.filename() <<
"'. "
107 <<
"Ignoring this folder, however this is a bad situation.";
113 for (std::filesystem::path& secondDir : secondDirs)
117 ARMARX_WARNING <<
"Found a non-timestamp folder inside an entity '"
118 <<
id.str() <<
"' hours folder with name '"
119 << secondDir.filename() <<
"'. "
120 <<
"Ignoring this folder, however this is a bad situation.";
124 std::vector<std::filesystem::path> timestampDirs =
127 for (std::filesystem::path& timestampDir : timestampDirs)
132 <<
"Found a non-timestamp folder inside an entity '" <<
id.str()
133 <<
"' seconds folder with name '" << timestampDir.filename()
135 <<
"Ignoring this folder, however this is a bad situation.";
139 std::string container = timestampDir.filename().string();
141 if (!container.empty())
143 containers.emplace_back(container);
153 std::vector<std::string>
158 return std::vector<std::string>();
162 std::string directoryPath = getFullPath(
id).string();
164 std::mutex* dirMutex =
nullptr;
166 std::lock_guard mapLock(directoryMutexMapLock_);
167 auto& mutexPtr = directoryMutexes_[directoryPath];
170 mutexPtr = std::make_unique<std::mutex>();
172 dirMutex = mutexPtr.get();
175 std::lock_guard dirLock(*dirMutex);
177 std::vector<std::filesystem::path> files = getAllFiles(
id);
178 std::vector<std::string> filesStr;
179 std::set<std::string> seen;
181 for (
auto& path : files)
187 std::string item = toLogicalKey(path.filename().string());
191 if (!item.empty() && seen.insert(item).second)
193 filesStr.emplace_back(item);
209 auto path_to_id = getFullPath(
id);
210 auto correct_container_path = path_to_id / key;
213 return contains_container;
226 return fileExists(
id, key) or fileExists(
id, key +
GZIP_SUFFIX);
232 std::vector<unsigned char>&
data)
242 std::vector<unsigned char> compressed;
243 bool didCompress =
false;
244 if (compressionEnabled_.load(std::memory_order_acquire) and
245 data.size() >= compressionMinBytes_.load(std::memory_order_relaxed) and
246 isCompressibleKey(key))
251 data, compressionLevel_.load(std::memory_order_relaxed));
255 catch (
const std::exception& e)
258 ARMARX_WARNING <<
"Could not compress " <<
id.str() <<
"/" << key <<
": " << e.what()
259 <<
". Storing uncompressed.";
263 std::vector<unsigned char>& payload = didCompress ? compressed :
data;
266 if (batchWriteEnabled_.load(std::memory_order_acquire))
268 enqueueBatchItem(
id, key, std::move(payload));
276 std::string directoryPath = getFullPath(
id).string();
279 std::mutex* dirMutex =
nullptr;
281 std::lock_guard mapLock(directoryMutexMapLock_);
282 auto& mutexPtr = directoryMutexes_[directoryPath];
285 mutexPtr = std::make_unique<std::mutex>();
287 dirMutex = mutexPtr.get();
292 std::lock_guard dirLock(*dirMutex);
296 auto dir = getFullPath(
id);
297 auto parentDir = dir.parent_path();
302 ARMARX_ERROR <<
"No write permission for directory: " << parentDir
303 <<
". Cannot store " <<
id.str() <<
"/" << key;
304 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
308 ensureFullPathExists(
id,
true);
310 if (enoughDiskSpaceLeft())
312 writeDataToFile(
id, key, payload);
313 removeStaleVariant(
id, key);
317 ARMARX_ERROR <<
"Not enough disk space available for DiskPersistence. "
318 <<
"Skipping storage of " <<
id.str() <<
"/" << key;
319 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
322 catch (
const armarx::LocalException& e)
324 ARMARX_ERROR <<
"ArmarX exception while storing " <<
id.str() <<
"/" << key
326 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
328 catch (
const std::filesystem::filesystem_error& e)
330 ARMARX_ERROR <<
"Filesystem error while storing " <<
id.str() <<
"/" << key
331 <<
": " << e.what() <<
" (error code: " << e.code() <<
")";
332 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
334 catch (
const std::system_error& e)
336 ARMARX_ERROR <<
"System error while storing " <<
id.str() <<
"/" << key
337 <<
": " << e.what() <<
" (error code: " << e.code() <<
")";
338 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
340 catch (
const std::exception& e)
342 ARMARX_ERROR <<
"Unexpected exception while storing " <<
id.str() <<
"/" << key
344 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
348 ARMARX_ERROR <<
"Unknown exception while storing " <<
id.str() <<
"/" << key;
349 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
353 std::vector<unsigned char>
358 return std::vector<unsigned char>();
363 std::string directoryPath = getFullPath(
id).string();
365 std::mutex* dirMutex =
nullptr;
367 std::lock_guard mapLock(directoryMutexMapLock_);
368 auto& mutexPtr = directoryMutexes_[directoryPath];
371 mutexPtr = std::make_unique<std::mutex>();
373 dirMutex = mutexPtr.get();
376 std::lock_guard dirLock(*dirMutex);
378 const std::string compressedKey = key +
GZIP_SUFFIX;
379 const bool hasPlain = fileExists(
id, key);
380 const bool hasCompressed = fileExists(
id, compressedKey);
382 if (hasPlain and hasCompressed)
388 std::error_code ecPlain;
389 std::error_code ecCompressed;
390 const auto plainTime = std::filesystem::last_write_time(getFullPath(
id) / key, ecPlain);
391 const auto compressedTime =
392 std::filesystem::last_write_time(getFullPath(
id) / compressedKey, ecCompressed);
393 const bool preferCompressed = not ecPlain and not ecCompressed and
394 compressedTime > plainTime;
397 <<
" exist in " << getFullPath(
id) <<
". Reading the newer one ("
398 << (preferCompressed ? compressedKey : key)
399 <<
"); the other is stale and should be deleted.";
401 return preferCompressed ? readCompressedFile(
id, compressedKey)
402 : readDataFromFile(
id, key);
407 return readDataFromFile(
id, key);
411 return readCompressedFile(
id, compressedKey);
414 return std::vector<unsigned char>();
417 std::filesystem::path
418 DiskPersistence::getMemoryParentPath()
420 std::string p = memoryParentPath_.string();
427 std::filesystem::path
443 auto p = getFullPath(
id);
450 auto p = getFullPath(
id) / filename;
456 bool createIfNotExistent)
458 auto p = getFullPath(
id);
464 const std::string& filename,
465 bool createIfNotExistent)
467 auto p = getFullPath(
id) / filename;
472 DiskPersistence::writeDataToFile(
const armarx::armem::MemoryID&
id,
473 const std::string& filename,
474 const std::vector<unsigned char>& data)
476 auto p = getFullPath(
id) / filename;
480 std::vector<unsigned char>
481 DiskPersistence::readDataFromFile(
const armarx::armem::MemoryID&
id,
482 const std::string& filename)
484 auto p = getFullPath(
id) / filename;
488 std::vector<std::filesystem::path>
489 DiskPersistence::getAllFiles(
const armarx::armem::MemoryID&
id)
491 if (fullPathExists(
id))
493 auto p = getFullPath(
id);
497 return std::vector<std::filesystem::path>();
500 std::vector<std::filesystem::path>
501 DiskPersistence::getAllDirectories(
const armarx::armem::MemoryID&
id)
503 if (fullPathExists(
id))
505 auto p = getFullPath(
id);
509 return std::vector<std::filesystem::path>();
513 DiskPersistence::enoughDiskSpaceLeft()
515 const std::filesystem::path configured_path = this->getMemoryParentPath();
516 bool debug_info_output_enabled =
false;
521 std::filesystem::path path_to_disk = configured_path;
522 while (!path_to_disk.empty() && !std::filesystem::exists(path_to_disk, ec))
524 const std::filesystem::path parent = path_to_disk.parent_path();
525 if (parent == path_to_disk)
529 path_to_disk = parent;
532 if (!path_to_disk.empty() && std::filesystem::exists(path_to_disk, ec))
536 auto space_info = std::filesystem::space(path_to_disk);
537 int const conversion_factor = 1024;
539 auto available_space = space_info.available /
540 (conversion_factor * conversion_factor * conversion_factor);
542 if (debug_info_output_enabled)
545 << space_info.capacity /
546 (conversion_factor * conversion_factor * conversion_factor)
550 (conversion_factor * conversion_factor * conversion_factor)
553 << space_info.available /
554 (conversion_factor * conversion_factor * conversion_factor)
562 available_space >=
static_cast<std::uintmax_t
>(this->
minDiskSpace);
564 catch (
const std::filesystem::filesystem_error& e)
571 ARMARX_DEBUG <<
"Error while trying to get info on available disk space";
580 ARMARX_WARNING <<
"Cannot resolve any existing directory for '" << configured_path
581 <<
"' and thus cannot check the available disk space. "
582 <<
"Refusing to write.";
590 auto basePath = getMemoryParentPath();
595 <<
"' is not writable! Data will not be persisted.";
597 <<
" 1. Directory exists\n"
598 <<
" 2. Current user has write permissions\n"
599 <<
" 3. Filesystem is not read-only\n"
600 <<
" 4. No SELinux/AppArmor restrictions";
604 ARMARX_INFO <<
"LTM storage path validated: " << basePath;
613 bool wasEnabled = batchWriteEnabled_.exchange(
enable, std::memory_order_acq_rel);
615 if (
enable && !wasEnabled)
619 ARMARX_INFO <<
"Batch write mode ENABLED (threshold: " << batchSizeThreshold_
620 <<
" items or " << batchTimeThresholdMs_ <<
"ms)";
622 else if (!
enable && wasEnabled)
633 const std::string& key,
634 std::vector<unsigned char>
data)
636 bool shouldFlush =
false;
638 std::lock_guard<std::mutex> lock(batchMutex_);
641 if (batchBuffer_.empty())
643 batchStartTime_ = std::chrono::steady_clock::now();
647 batchBuffer_.push_back({id, key, std::move(
data), std::chrono::steady_clock::now()});
650 if (batchBuffer_.size() >= batchSizeThreshold_)
659 flushBatchInternal(0);
664 batchCondition_.notify_one();
671 flushBatchInternal(2);
677 std::lock_guard<std::mutex> lock(batchMutex_);
678 return batchBuffer_.size();
682 DiskPersistence::flushBatchInternal(
int reason)
684 std::vector<BatchWriteItem> itemsToWrite;
687 std::lock_guard<std::mutex> lock(batchMutex_);
688 if (batchBuffer_.empty())
692 itemsToWrite = std::move(batchBuffer_);
693 batchBuffer_.clear();
699 case 0: batchStats_.
flushBySize.fetch_add(1, std::memory_order_relaxed);
break;
700 case 1: batchStats_.
flushByTime.fetch_add(1, std::memory_order_relaxed);
break;
701 case 2: batchStats_.
flushByExplicit.fetch_add(1, std::memory_order_relaxed);
break;
705 auto startTime = std::chrono::steady_clock::now();
706 writeBatch(itemsToWrite);
707 auto endTime = std::chrono::steady_clock::now();
710 uint64_t durationNs = std::chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime).count();
712 batchStats_.
totalItemsBatched.fetch_add(itemsToWrite.size(), std::memory_order_relaxed);
713 batchStats_.
totalFlushTimeNs.fetch_add(durationNs, std::memory_order_relaxed);
716 uint64_t currentMax = batchStats_.
maxBatchSize.load(std::memory_order_relaxed);
717 while (itemsToWrite.size() > currentMax)
719 if (batchStats_.
maxBatchSize.compare_exchange_weak(currentMax, itemsToWrite.size(), std::memory_order_relaxed))
725 ARMARX_DEBUG <<
"Batch flush: " << itemsToWrite.size() <<
" items in "
726 << (durationNs / 1e6) <<
"ms (reason=" << reason <<
")";
730 DiskPersistence::writeBatch(std::vector<BatchWriteItem>& items)
738 if (!enoughDiskSpaceLeft())
740 ARMARX_ERROR <<
"Not enough disk space for batch write of " << items.size() <<
" items. Dropping batch!";
741 storageErrorCount_.fetch_add(items.size(), std::memory_order_relaxed);
746 std::unordered_map<std::string, std::vector<BatchWriteItem*>> itemsByDirectory;
747 for (
auto& item : items)
749 std::string dirPath = getFullPath(item.id).string();
750 itemsByDirectory[dirPath].push_back(&item);
754 std::set<std::string> createdDirectories;
755 std::vector<std::string> directoriesToSync;
757 for (
auto& [dirPath, dirItems] : itemsByDirectory)
760 std::mutex* dirMutex =
nullptr;
762 std::lock_guard mapLock(directoryMutexMapLock_);
763 auto& mutexPtr = directoryMutexes_[dirPath];
766 mutexPtr = std::make_unique<std::mutex>();
768 dirMutex = mutexPtr.get();
772 std::lock_guard dirLock(*dirMutex);
777 if (createdDirectories.find(dirPath) == createdDirectories.end())
780 ensureFullPathExists(dirItems[0]->
id,
true);
781 createdDirectories.insert(dirPath);
785 for (
auto* item : dirItems)
787 auto filePath = getFullPath(item->id) / item->key;
795 removeStaleVariant(item->id, item->key);
797 batchStats_.totalBytesWritten.fetch_add(item->data.size(), std::memory_order_relaxed);
801 directoriesToSync.push_back(dirPath);
803 catch (
const std::exception& e)
805 ARMARX_ERROR <<
"Error writing batch to directory " << dirPath <<
": " << e.what();
806 storageErrorCount_.fetch_add(dirItems.size(), std::memory_order_relaxed);
811 for (
const auto& dirPath : directoriesToSync)
813 int dfd = ::open(dirPath.c_str(), O_DIRECTORY | O_RDONLY);
823 DiskPersistence::startBatchWriter()
825 if (batchWriterThread_.joinable())
830 stopBatchWriter_.store(
false, std::memory_order_release);
831 batchWriterThread_ = std::thread(&DiskPersistence::batchWriterThread,
this);
835 DiskPersistence::stopBatchWriter()
837 stopBatchWriter_.store(
true, std::memory_order_release);
838 batchCondition_.notify_all();
840 if (batchWriterThread_.joinable())
842 batchWriterThread_.join();
846 flushBatchInternal(2);
850 DiskPersistence::batchWriterThread()
854 while (!stopBatchWriter_.load(std::memory_order_acquire))
856 bool shouldFlush =
false;
859 std::unique_lock<std::mutex> lock(batchMutex_);
862 auto timeout = std::chrono::milliseconds(batchTimeThresholdMs_);
863 batchCondition_.wait_for(lock, timeout, [
this]() {
864 return stopBatchWriter_.load(std::memory_order_acquire) ||
865 batchBuffer_.size() >= batchSizeThreshold_;
869 if (!batchBuffer_.empty())
871 auto now = std::chrono::steady_clock::now();
872 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
873 now - batchStartTime_).count();
875 if (elapsed >=
static_cast<long long>(batchTimeThresholdMs_) ||
876 batchBuffer_.size() >= batchSizeThreshold_)
885 flushBatchInternal(1);