DiskPersistence.cpp
Go to the documentation of this file.
2
3#include <fcntl.h>
4#include <unistd.h>
5
6#include <set>
7
8#include <SimoxUtility/algorithm/string/string_tools.h>
9
11
13{
14
15 bool
16 DiskPersistence::isCompressibleKey(const std::string& key)
17 {
18 // Only the Aron JSON payloads. Extracted members (.png / .exr) are already compressed.
19 return simox::alg::ends_with(key, ".json");
20 }
21
22 std::string
23 DiskPersistence::toLogicalKey(const std::string& key)
24 {
25 if (simox::alg::ends_with(key, GZIP_SUFFIX))
26 {
27 return simox::alg::remove_suffix(key, GZIP_SUFFIX);
28 }
29 return key;
30 }
31
32 void
33 DiskPersistence::removeStaleVariant(const armarx::armem::MemoryID& id,
34 const std::string& writtenKey)
35 {
36 const std::string staleKey = simox::alg::ends_with(writtenKey, GZIP_SUFFIX)
37 ? toLogicalKey(writtenKey)
38 : writtenKey + GZIP_SUFFIX;
39
40 // remove() reports a missing file as "nothing removed", not as an error, so the common
41 // case costs one unlink attempt and no branching.
42 std::error_code ec;
43 std::filesystem::remove(getFullPath(id) / staleKey, ec);
44
45 if (ec)
46 {
47 // The payload itself is already stored; failing to tidy up must not fail the write.
48 ARMARX_WARNING << deactivateSpam(60) << "Could not remove superseded " << staleKey
49 << " in " << getFullPath(id) << ": " << ec.message()
50 << ". Reads of this item may return stale content.";
51 }
52 }
53
54 std::vector<unsigned char>
55 DiskPersistence::readCompressedFile(const armarx::armem::MemoryID& id,
56 const std::string& compressedKey)
57 {
58 try
59 {
60 return util::fs::gzipDecompress(readDataFromFile(id, compressedKey));
61 }
62 catch (const std::exception& e)
63 {
64 ARMARX_ERROR << "Could not decompress " << id.str() << "/" << compressedKey << ": "
65 << e.what();
66 return std::vector<unsigned char>();
67 }
68 }
69
70 std::vector<std::string>
72 {
73 std::vector<std::string> containers;
74
75 if (!enabled_)
76 {
77 return containers;
78 }
79
80 // If it has a nice form
81 if (!id.hasEntityName() || id.hasTimestamp())
82 {
83 std::vector<std::filesystem::path> dirs = getAllDirectories(id);
84
85
86 for (auto& path : dirs)
87 {
88 std::string container = path.filename().string();
89
90 if (!container.empty())
91 {
92 containers.emplace_back(container);
93 }
94 }
95 }
96 // If it has not a nice form
97 else
98 {
99 std::vector<std::filesystem::path> dayDirs = getAllDirectories(id);
100
101 for (std::filesystem::path& dayDir : dayDirs)
102 {
103 if (!util::fs::detail::isDateString(dayDir.filename()))
104 {
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.";
108 continue;
109 }
110
111 std::vector<std::filesystem::path> secondDirs = util::fs::getAllDirectories(dayDir);
112
113 for (std::filesystem::path& secondDir : secondDirs)
114 {
115 if (!util::fs::detail::isNumberString(secondDir.filename()))
116 {
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.";
121 continue;
122 }
123
124 std::vector<std::filesystem::path> timestampDirs =
126
127 for (std::filesystem::path& timestampDir : timestampDirs)
128 {
129 if (!util::fs::detail::isNumberString(timestampDir.filename()))
130 {
132 << "Found a non-timestamp folder inside an entity '" << id.str()
133 << "' seconds folder with name '" << timestampDir.filename()
134 << "'. "
135 << "Ignoring this folder, however this is a bad situation.";
136 continue;
137 }
138
139 std::string container = timestampDir.filename().string();
140
141 if (!container.empty())
142 {
143 containers.emplace_back(container);
144 }
145 }
146 }
147 }
148 }
149
150 return containers;
151 }
152
153 std::vector<std::string>
155 {
156 if (!enabled_)
157 {
158 return std::vector<std::string>();
159 }
160
161 // THREAD-SAFETY: Acquire per-directory mutex to get consistent file listing
162 std::string directoryPath = getFullPath(id).string();
163
164 std::mutex* dirMutex = nullptr;
165 {
166 std::lock_guard mapLock(directoryMutexMapLock_);
167 auto& mutexPtr = directoryMutexes_[directoryPath];
168 if (!mutexPtr)
169 {
170 mutexPtr = std::make_unique<std::mutex>();
171 }
172 dirMutex = mutexPtr.get();
173 }
174
175 std::lock_guard dirLock(*dirMutex);
176
177 std::vector<std::filesystem::path> files = getAllFiles(id);
178 std::vector<std::string> filesStr;
179 std::set<std::string> seen;
180
181 for (auto& path : files)
182 {
183 // Report the logical key, i.e. without the ".gz" suffix a compressed item carries on
184 // disk. Callers (e.g. the extracted-member scan in EntityInstance::_implResolve) match
185 // keys by prefix and suffix and should not have to know about compression;
186 // retrieveItem() resolves the logical key back to whichever file actually exists.
187 std::string item = toLogicalKey(path.filename().string());
188
189 // Both spellings collapse onto the same logical key, so an export still holding a
190 // stale sibling would otherwise report the item twice and have it resolved twice.
191 if (!item.empty() && seen.insert(item).second)
192 {
193 filesStr.emplace_back(item);
194 }
195 }
196
197 return filesStr;
198 }
199
200 bool
202 {
203 if (!enabled_)
204 {
205 return false;
206 }
207
208 //from id get the directory for this memory item and append the key to it, then check if the directory exists
209 auto path_to_id = getFullPath(id);
210 auto correct_container_path = path_to_id / key;
211 bool contains_container = util::fs::directoryExists(correct_container_path);
212
213 return contains_container;
214 }
215
216 bool
218 {
219 if (!enabled_)
220 {
221 return false;
222 }
223
224 // Accept both the plain file (older exports, or payloads below the compression threshold)
225 // and the compressed variant.
226 return fileExists(id, key) or fileExists(id, key + GZIP_SUFFIX);
227 }
228
229 void
231 std::string key,
232 std::vector<unsigned char>& data)
233 {
234 if (!enabled_)
235 {
236 return;
237 }
238
239 // Compress before dispatching, so that the immediate and the batched write path both
240 // benefit and neither needs to know about compression. The key gains a ".gz" suffix;
241 // retrieveItem()/containsItem()/getItemKeys() hide that again from callers.
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))
247 {
248 try
249 {
250 compressed = util::fs::gzipCompress(
251 data, compressionLevel_.load(std::memory_order_relaxed));
252 key += GZIP_SUFFIX;
253 didCompress = true;
254 }
255 catch (const std::exception& e)
256 {
257 // Storing the data uncompressed is always safe, so never lose a snapshot over this.
258 ARMARX_WARNING << "Could not compress " << id.str() << "/" << key << ": " << e.what()
259 << ". Storing uncompressed.";
260 compressed.clear();
261 }
262 }
263 std::vector<unsigned char>& payload = didCompress ? compressed : data;
264
265 // If batch writing is enabled, add to batch buffer instead of writing immediately
266 if (batchWriteEnabled_.load(std::memory_order_acquire))
267 {
268 enqueueBatchItem(id, key, std::move(payload));
269 return;
270 }
271
272 // Original immediate write path
273 // THREAD-SAFETY: Get per-directory mutex for fine-grained locking
274 // This allows parallel writes to different directories (different entities)
275 // while preventing filesystem corruption within the same directory
276 std::string directoryPath = getFullPath(id).string();
277
278 // Get or create mutex for this directory
279 std::mutex* dirMutex = nullptr;
280 {
281 std::lock_guard mapLock(directoryMutexMapLock_);
282 auto& mutexPtr = directoryMutexes_[directoryPath];
283 if (!mutexPtr)
284 {
285 mutexPtr = std::make_unique<std::mutex>();
286 }
287 dirMutex = mutexPtr.get();
288 }
289
290 // Lock this specific directory for the duration of the write
291 // Other threads can write to different directories in parallel
292 std::lock_guard dirLock(*dirMutex);
293
294 try
295 {
296 auto dir = getFullPath(id);
297 auto parentDir = dir.parent_path();
298
299 // Check permissions before attempting write
300 if (util::fs::directoryExists(parentDir) && !util::fs::hasWritePermission(parentDir))
301 {
302 ARMARX_ERROR << "No write permission for directory: " << parentDir
303 << ". Cannot store " << id.str() << "/" << key;
304 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
305 return;
306 }
307
308 ensureFullPathExists(id, true);
309
310 if (enoughDiskSpaceLeft())
311 {
312 writeDataToFile(id, key, payload);
313 removeStaleVariant(id, key);
314 }
315 else
316 {
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);
320 }
321 }
322 catch (const armarx::LocalException& e)
323 {
324 ARMARX_ERROR << "ArmarX exception while storing " << id.str() << "/" << key
325 << ": " << e.what();
326 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
327 }
328 catch (const std::filesystem::filesystem_error& e)
329 {
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);
333 }
334 catch (const std::system_error& e)
335 {
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);
339 }
340 catch (const std::exception& e)
341 {
342 ARMARX_ERROR << "Unexpected exception while storing " << id.str() << "/" << key
343 << ": " << e.what();
344 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
345 }
346 catch (...)
347 {
348 ARMARX_ERROR << "Unknown exception while storing " << id.str() << "/" << key;
349 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
350 }
351 }
352
353 std::vector<unsigned char>
355 {
356 if (!enabled_)
357 {
358 return std::vector<unsigned char>();
359 }
360
361 // THREAD-SAFETY: Acquire per-directory mutex to prevent reading during writes
362 // This ensures we don't read partial/empty files while another thread is writing
363 std::string directoryPath = getFullPath(id).string();
364
365 std::mutex* dirMutex = nullptr;
366 {
367 std::lock_guard mapLock(directoryMutexMapLock_);
368 auto& mutexPtr = directoryMutexes_[directoryPath];
369 if (!mutexPtr)
370 {
371 mutexPtr = std::make_unique<std::mutex>();
372 }
373 dirMutex = mutexPtr.get();
374 }
375
376 std::lock_guard dirLock(*dirMutex);
377
378 const std::string compressedKey = key + GZIP_SUFFIX;
379 const bool hasPlain = fileExists(id, key);
380 const bool hasCompressed = fileExists(id, compressedKey);
381
382 if (hasPlain and hasCompressed)
383 {
384 // Both spellings of one logical key. storeItem() deletes the form it supersedes, so
385 // this is either an export written before that was done, or a crash in the window
386 // between the write and that cleanup. The names say nothing about which is current,
387 // so go by mtime -- silently preferring one is how an export reads stale for weeks.
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;
395
396 ARMARX_WARNING << deactivateSpam(60) << "Both " << key << " and " << compressedKey
397 << " exist in " << getFullPath(id) << ". Reading the newer one ("
398 << (preferCompressed ? compressedKey : key)
399 << "); the other is stale and should be deleted.";
400
401 return preferCompressed ? readCompressedFile(id, compressedKey)
402 : readDataFromFile(id, key);
403 }
404
405 if (hasPlain)
406 {
407 return readDataFromFile(id, key);
408 }
409 if (hasCompressed)
410 {
411 return readCompressedFile(id, compressedKey);
412 }
413
414 return std::vector<unsigned char>();
415 }
416
417 std::filesystem::path
418 DiskPersistence::getMemoryParentPath()
419 {
420 std::string p = memoryParentPath_.string();
421
423
424 return p;
425 }
426
427 std::filesystem::path
428 DiskPersistence::getFullPath(const armarx::armem::MemoryID& id)
429 {
430 auto p = getMemoryParentPath() / getExportName();
431
432 auto cleanID =
433 id.cleanID(); //somehow, the iDs are jumbled when loading the LTM from disk, this solves it for now
434
435 auto fullPath = util::fs::toPath(p, cleanID);
436
437 return fullPath;
438 }
439
440 bool
441 DiskPersistence::fullPathExists(const armarx::armem::MemoryID& id)
442 {
443 auto p = getFullPath(id);
445 }
446
447 bool
448 DiskPersistence::fileExists(const armarx::armem::MemoryID& id, const std::string& filename)
449 {
450 auto p = getFullPath(id) / filename;
451 return util::fs::fileExists(p);
452 }
453
454 void
455 DiskPersistence::ensureFullPathExists(const armarx::armem::MemoryID& id,
456 bool createIfNotExistent)
457 {
458 auto p = getFullPath(id);
459 util::fs::ensureDirectoryExists(p, createIfNotExistent);
460 }
461
462 void
463 DiskPersistence::ensureFileExists(const armarx::armem::MemoryID& id,
464 const std::string& filename,
465 bool createIfNotExistent)
466 {
467 auto p = getFullPath(id) / filename;
468 util::fs::ensureFileExists(p, createIfNotExistent);
469 }
470
471 void
472 DiskPersistence::writeDataToFile(const armarx::armem::MemoryID& id,
473 const std::string& filename,
474 const std::vector<unsigned char>& data)
475 {
476 auto p = getFullPath(id) / filename;
478 }
479
480 std::vector<unsigned char>
481 DiskPersistence::readDataFromFile(const armarx::armem::MemoryID& id,
482 const std::string& filename)
483 {
484 auto p = getFullPath(id) / filename;
486 }
487
488 std::vector<std::filesystem::path>
489 DiskPersistence::getAllFiles(const armarx::armem::MemoryID& id)
490 {
491 if (fullPathExists(id))
492 {
493 auto p = getFullPath(id);
494 return util::fs::getAllFiles(p);
495 }
496
497 return std::vector<std::filesystem::path>();
498 }
499
500 std::vector<std::filesystem::path>
501 DiskPersistence::getAllDirectories(const armarx::armem::MemoryID& id)
502 {
503 if (fullPathExists(id))
504 {
505 auto p = getFullPath(id);
507 }
508
509 return std::vector<std::filesystem::path>();
510 }
511
512 bool
513 DiskPersistence::enoughDiskSpaceLeft()
514 {
515 const std::filesystem::path configured_path = this->getMemoryParentPath();
516 bool debug_info_output_enabled = false;
517
518 // The memory directory need not exist yet on the first write, so walk up to the
519 // nearest existing ancestor: it lives on the same disk and gives a usable reading.
520 std::error_code ec;
521 std::filesystem::path path_to_disk = configured_path;
522 while (!path_to_disk.empty() && !std::filesystem::exists(path_to_disk, ec))
523 {
524 const std::filesystem::path parent = path_to_disk.parent_path();
525 if (parent == path_to_disk)
526 {
527 break;
528 }
529 path_to_disk = parent;
530 }
531
532 if (!path_to_disk.empty() && std::filesystem::exists(path_to_disk, ec))
533 {
534 try
535 {
536 auto space_info = std::filesystem::space(path_to_disk);
537 int const conversion_factor = 1024;
538
539 auto available_space = space_info.available /
540 (conversion_factor * conversion_factor * conversion_factor);
541
542 if (debug_info_output_enabled)
543 {
544 ARMARX_DEBUG << "Capacity: "
545 << space_info.capacity /
546 (conversion_factor * conversion_factor * conversion_factor)
547 << " GB\n";
548 ARMARX_DEBUG << "Free space: "
549 << space_info.free /
550 (conversion_factor * conversion_factor * conversion_factor)
551 << " GB\n";
552 ARMARX_DEBUG << "Available space: "
553 << space_info.available /
554 (conversion_factor * conversion_factor * conversion_factor)
555 << " GB\n";
556
557 ARMARX_DEBUG << "Min disk space: " << this->minDiskSpace << " GB\n";
558 }
559 // minDiskSpace is a signed int; compare only after ruling out negative
560 // values, which would otherwise convert to a huge unsigned bound.
561 return this->minDiskSpace <= 0 ||
562 available_space >= static_cast<std::uintmax_t>(this->minDiskSpace);
563 }
564 catch (const std::filesystem::filesystem_error& e)
565 {
566 ARMARX_WARNING << "Error: " << e.what() << '\n';
567 return false;
568 }
569 catch (...)
570 {
571 ARMARX_DEBUG << "Error while trying to get info on available disk space";
572 return false;
573 }
574 }
575 else
576 {
577 // Nothing on the configured path resolves to an existing directory, so free
578 // space cannot be determined. Refuse the write rather than proceeding blindly,
579 // matching how the failure paths above behave.
580 ARMARX_WARNING << "Cannot resolve any existing directory for '" << configured_path
581 << "' and thus cannot check the available disk space. "
582 << "Refusing to write.";
583 return false;
584 }
585 }
586
587 void
589 {
590 auto basePath = getMemoryParentPath();
591
592 if (!util::fs::canCreateFiles(basePath))
593 {
594 ARMARX_ERROR << "LTM storage path '" << basePath
595 << "' is not writable! Data will not be persisted.";
596 ARMARX_ERROR << "Please check:\n"
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";
601 }
602 else
603 {
604 ARMARX_INFO << "LTM storage path validated: " << basePath;
605 }
606 }
607
608 // ==================== Batch Write Implementation ====================
609
610 void
612 {
613 bool wasEnabled = batchWriteEnabled_.exchange(enable, std::memory_order_acq_rel);
614
615 if (enable && !wasEnabled)
616 {
617 // Starting batch mode - start the background flush thread
618 startBatchWriter();
619 ARMARX_INFO << "Batch write mode ENABLED (threshold: " << batchSizeThreshold_
620 << " items or " << batchTimeThresholdMs_ << "ms)";
621 }
622 else if (!enable && wasEnabled)
623 {
624 // Stopping batch mode - flush remaining items and stop thread
625 flushBatch();
626 stopBatchWriter();
627 ARMARX_INFO << "Batch write mode DISABLED";
628 }
629 }
630
631 void
632 DiskPersistence::enqueueBatchItem(const armarx::armem::MemoryID& id,
633 const std::string& key,
634 std::vector<unsigned char> data)
635 {
636 bool shouldFlush = false;
637 {
638 std::lock_guard<std::mutex> lock(batchMutex_);
639
640 // Set batch start time on first item
641 if (batchBuffer_.empty())
642 {
643 batchStartTime_ = std::chrono::steady_clock::now();
644 }
645
646 // Add item to batch
647 batchBuffer_.push_back({id, key, std::move(data), std::chrono::steady_clock::now()});
648
649 // Check if we should flush due to size threshold
650 if (batchBuffer_.size() >= batchSizeThreshold_)
651 {
652 shouldFlush = true;
653 }
654 }
655
656 // Flush outside the lock if needed
657 if (shouldFlush)
658 {
659 flushBatchInternal(0); // 0 = flush by size
660 }
661 else
662 {
663 // Notify the background thread that there's work
664 batchCondition_.notify_one();
665 }
666 }
667
668 void
670 {
671 flushBatchInternal(2); // 2 = explicit flush
672 }
673
674 size_t
676 {
677 std::lock_guard<std::mutex> lock(batchMutex_);
678 return batchBuffer_.size();
679 }
680
681 void
682 DiskPersistence::flushBatchInternal(int reason)
683 {
684 std::vector<BatchWriteItem> itemsToWrite;
685
686 {
687 std::lock_guard<std::mutex> lock(batchMutex_);
688 if (batchBuffer_.empty())
689 {
690 return;
691 }
692 itemsToWrite = std::move(batchBuffer_);
693 batchBuffer_.clear();
694 }
695
696 // Update statistics for flush reason
697 switch (reason)
698 {
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;
702 }
703
704 // Write the batch
705 auto startTime = std::chrono::steady_clock::now();
706 writeBatch(itemsToWrite);
707 auto endTime = std::chrono::steady_clock::now();
708
709 // Update statistics
710 uint64_t durationNs = std::chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime).count();
711 batchStats_.totalBatchesWritten.fetch_add(1, std::memory_order_relaxed);
712 batchStats_.totalItemsBatched.fetch_add(itemsToWrite.size(), std::memory_order_relaxed);
713 batchStats_.totalFlushTimeNs.fetch_add(durationNs, std::memory_order_relaxed);
714
715 // Update max batch size
716 uint64_t currentMax = batchStats_.maxBatchSize.load(std::memory_order_relaxed);
717 while (itemsToWrite.size() > currentMax)
718 {
719 if (batchStats_.maxBatchSize.compare_exchange_weak(currentMax, itemsToWrite.size(), std::memory_order_relaxed))
720 {
721 break;
722 }
723 }
724
725 ARMARX_DEBUG << "Batch flush: " << itemsToWrite.size() << " items in "
726 << (durationNs / 1e6) << "ms (reason=" << reason << ")";
727 }
728
729 void
730 DiskPersistence::writeBatch(std::vector<BatchWriteItem>& items)
731 {
732 if (items.empty())
733 {
734 return;
735 }
736
737 // Check disk space once for the whole batch
738 if (!enoughDiskSpaceLeft())
739 {
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);
742 return;
743 }
744
745 // Group items by directory path for efficient directory creation
746 std::unordered_map<std::string, std::vector<BatchWriteItem*>> itemsByDirectory;
747 for (auto& item : items)
748 {
749 std::string dirPath = getFullPath(item.id).string();
750 itemsByDirectory[dirPath].push_back(&item);
751 }
752
753 // Process each directory group
754 std::set<std::string> createdDirectories; // Track directories we've created
755 std::vector<std::string> directoriesToSync; // Directories that need fsync
756
757 for (auto& [dirPath, dirItems] : itemsByDirectory)
758 {
759 // Get or create directory mutex
760 std::mutex* dirMutex = nullptr;
761 {
762 std::lock_guard mapLock(directoryMutexMapLock_);
763 auto& mutexPtr = directoryMutexes_[dirPath];
764 if (!mutexPtr)
765 {
766 mutexPtr = std::make_unique<std::mutex>();
767 }
768 dirMutex = mutexPtr.get();
769 }
770
771 // Lock this directory for the batch write
772 std::lock_guard dirLock(*dirMutex);
773
774 try
775 {
776 // Create directory once for all items in this group
777 if (createdDirectories.find(dirPath) == createdDirectories.end())
778 {
779 // Use the first item's ID to create the directory
780 ensureFullPathExists(dirItems[0]->id, true);
781 createdDirectories.insert(dirPath);
782 }
783
784 // Write all files in this directory without individual fsync
785 for (auto* item : dirItems)
786 {
787 auto filePath = getFullPath(item->id) / item->key;
788
789 // Atomic but not synced: the rename keeps a crash from leaving a truncated
790 // file behind -- which EntityInstance::_implResolve() then has to cope with --
791 // while the single fsync per directory at the end of this function, rather
792 // than one per file, is what makes batching worth doing.
794 filePath, item->data, util::fs::WriteMode::Atomic);
795 removeStaleVariant(item->id, item->key);
796
797 batchStats_.totalBytesWritten.fetch_add(item->data.size(), std::memory_order_relaxed);
798 }
799
800 // Track this directory for final sync
801 directoriesToSync.push_back(dirPath);
802 }
803 catch (const std::exception& e)
804 {
805 ARMARX_ERROR << "Error writing batch to directory " << dirPath << ": " << e.what();
806 storageErrorCount_.fetch_add(dirItems.size(), std::memory_order_relaxed);
807 }
808 }
809
810 // Single fsync for all directories at the end (deferred sync)
811 for (const auto& dirPath : directoriesToSync)
812 {
813 int dfd = ::open(dirPath.c_str(), O_DIRECTORY | O_RDONLY);
814 if (dfd >= 0)
815 {
816 ::fsync(dfd);
817 ::close(dfd);
818 }
819 }
820 }
821
822 void
823 DiskPersistence::startBatchWriter()
824 {
825 if (batchWriterThread_.joinable())
826 {
827 return; // Already running
828 }
829
830 stopBatchWriter_.store(false, std::memory_order_release);
831 batchWriterThread_ = std::thread(&DiskPersistence::batchWriterThread, this);
832 }
833
834 void
835 DiskPersistence::stopBatchWriter()
836 {
837 stopBatchWriter_.store(true, std::memory_order_release);
838 batchCondition_.notify_all();
839
840 if (batchWriterThread_.joinable())
841 {
842 batchWriterThread_.join();
843 }
844
845 // Flush any remaining items
846 flushBatchInternal(2);
847 }
848
849 void
850 DiskPersistence::batchWriterThread()
851 {
852 ARMARX_DEBUG << "Batch writer thread started";
853
854 while (!stopBatchWriter_.load(std::memory_order_acquire))
855 {
856 bool shouldFlush = false;
857
858 {
859 std::unique_lock<std::mutex> lock(batchMutex_);
860
861 // Wait for items or timeout
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_;
866 });
867
868 // Check if we should flush due to time threshold
869 if (!batchBuffer_.empty())
870 {
871 auto now = std::chrono::steady_clock::now();
872 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
873 now - batchStartTime_).count();
874
875 if (elapsed >= static_cast<long long>(batchTimeThresholdMs_) ||
876 batchBuffer_.size() >= batchSizeThreshold_)
877 {
878 shouldFlush = true;
879 }
880 }
881 }
882
883 if (shouldFlush)
884 {
885 flushBatchInternal(1); // 1 = flush by time
886 }
887 }
888
889 ARMARX_DEBUG << "Batch writer thread stopped";
890 }
891
892} // namespace armarx::armem::server::ltm::persistence
SpamFilterDataPtr deactivateSpam(SpamFilterDataPtr const &spamFilter, float deactivationDurationSec, const std::string &identifier, bool deactivate)
Definition Logging.cpp:75
static bool ReplaceEnvVars(std::string &string)
ReplaceEnvVars replaces environment variables in a string with their values, if the env.
static constexpr const char * GZIP_SUFFIX
Suffix appended to the logical key when an item is stored compressed.
bool containsContainer(const armarx::armem::MemoryID &id, std::string key) override
Checks if the container is available for the current memory id.
void setBatchWriteEnabled(bool enable)
Enable or disable batch writing.
std::vector< std::string > getItemKeys(const armarx::armem::MemoryID &id) override
Returns all items for the current id.
void storeItem(const armarx::armem::MemoryID &id, std::string key, std::vector< unsigned char > &data) override
Create a new file with name 'key' and stores the data inside it.
bool containsItem(const armarx::armem::MemoryID &id, std::string key) override
Checks if current container contains the item defined by its key.
std::vector< std::string > getContainerKeys(const armarx::armem::MemoryID &id) override
Returns all containers for the current id.
void flushBatch()
Explicitly flush all pending batch writes to disk.
size_t getBatchPendingCount() const
Get the current number of items pending in the batch buffer.
std::vector< unsigned char > retrieveItem(const armarx::armem::MemoryID &id, std::string key) override
Reads the data of the file with name 'key' at the current location.
bool enabled_
If false, the strategy is not writing or reading anything.
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:194
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
bool isDateString(const std::string &s)
bool isNumberString(const std::string &s)
std::vector< unsigned char > readDataFromFile(const std::filesystem::path &p)
void ensureFileExists(const std::filesystem::path &p, bool createIfNotExistent=false)
void writeDataToFile(const std::filesystem::path &p, const std::vector< unsigned char > &data, WriteMode mode=WriteMode::AtomicDurable)
bool directoryExists(const std::filesystem::path &p)
std::vector< std::filesystem::path > getAllFiles(const std::filesystem::path &p)
bool canCreateFiles(const std::filesystem::path &dir)
Check if we can create files in this directory.
std::filesystem::path toPath(const std::filesystem::path &base, const armem::MemoryID &id)
@ Atomic
Write a temporary file and rename it over the destination.
Definition filesystem.h:57
bool hasWritePermission(const std::filesystem::path &p)
Check if directory has write permission.
std::vector< unsigned char > gzipCompress(const std::vector< unsigned char > &data, int level=1)
Compress data into a gzip container (i.e.
std::vector< unsigned char > gzipDecompress(const std::vector< unsigned char > &data)
Inverse of gzipCompress. Throws if the data is not a valid gzip stream.
bool fileExists(const std::filesystem::path &p)
void ensureDirectoryExists(const std::filesystem::path &p, bool createIfNotExistent=false)
std::vector< std::filesystem::path > getAllDirectories(const std::filesystem::path &p)