BufferedMemoryMixin.h
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <chrono>
5#include <condition_variable>
6#include <iomanip>
7#include <limits>
8#include <map>
9#include <mutex>
10#include <queue>
11#include <thread>
12#include <variant>
13
14#include <boost/lockfree/queue.hpp>
15
16#include <SimoxUtility/json.h>
17
19#include <ArmarXCore/interface/observers/ObserverInterface.h>
21
26
27// Include PendingConversion definition (shared with MemoryBase)
29
31{
32 /**
33 * @brief Statistics for async storage queue operations.
34 *
35 * Thread-safe statistics for tracking queue performance and throughput.
36 */
38 {
39 // Queue statistics
40 std::atomic<uint64_t> totalItemsEnqueued{0};
41 std::atomic<uint64_t> totalItemsProcessed{0};
42 std::atomic<uint64_t> totalSnapshotsStored{0};
43
44 // Timing statistics (in nanoseconds)
45 std::atomic<uint64_t> totalStorageTimeNs{0};
46 std::atomic<uint64_t> maxStorageTimeNs{0};
47 std::atomic<uint64_t> totalConversionTimeNs{0};
48
49 // Backpressure statistics
50 std::atomic<uint64_t> backpressureEvents{0};
51
52 // Dropped snapshots due to queue being full (non-blocking drop policy)
53 std::atomic<uint64_t> snapshotsDroppedBackpressure{0};
54
55 // Dropped snapshots (other reasons, e.g., errors)
56 std::atomic<uint64_t> snapshotsDropped{0};
57
58 // Pre-filter statistics (snapshots filtered before enqueue)
59 std::atomic<uint64_t> snapshotsPreFiltered{0};
60 std::atomic<uint64_t> snapshotsPassedPreFilter{0};
61 std::atomic<uint64_t> totalPreFilterTimeNs{0};
62
78
79 /// Every snapshot the LTM discarded, whatever the reason. The two counters below are
80 /// disjoint -- backpressure drops and error drops -- so a consumer that watches only one
81 /// of them under-reports data loss.
82 uint64_t totalSnapshotsDropped() const
83 {
84 return snapshotsDroppedBackpressure.load(std::memory_order_relaxed) +
85 snapshotsDropped.load(std::memory_order_relaxed);
86 }
87
88 double getAvgStorageTimeMs() const
89 {
90 uint64_t count = totalItemsProcessed.load(std::memory_order_relaxed);
91 if (count == 0) return 0.0;
92 return totalStorageTimeNs.load(std::memory_order_relaxed) / (count * 1e6);
93 }
94
95 double getMaxStorageTimeMs() const
96 {
97 return maxStorageTimeNs.load(std::memory_order_relaxed) / 1e6;
98 }
99
101 {
102 uint64_t count = totalItemsProcessed.load(std::memory_order_relaxed);
103 if (count == 0) return 0.0;
104 return totalConversionTimeNs.load(std::memory_order_relaxed) / (count * 1e6);
105 }
106
108 {
109 uint64_t total = snapshotsPreFiltered.load(std::memory_order_relaxed) +
110 snapshotsPassedPreFilter.load(std::memory_order_relaxed);
111 if (total == 0) return 0.0;
112 return totalPreFilterTimeNs.load(std::memory_order_relaxed) / (total * 1e6);
113 }
114
116 {
117 uint64_t filtered = snapshotsPreFiltered.load(std::memory_order_relaxed);
118 uint64_t passed = snapshotsPassedPreFilter.load(std::memory_order_relaxed);
119 uint64_t total = filtered + passed;
120 if (total == 0) return 0.0;
121 return static_cast<double>(filtered) / static_cast<double>(total);
122 }
123 };
124
125
126 template <class _CoreSegmentT>
128 {
129 public:
130 /// Can hold either pre-converted Memory or pending conversion data
131 using StorageItem = std::variant<std::shared_ptr<const armem::wm::Memory>, PendingConversion>;
132
134 buffer(std::make_unique<armem::wm::Memory>(id)),
135 to_store(std::make_unique<armem::wm::Memory>(id)),
136 storageQueue(1000), // Initialize lock-free queue with capacity
137 queueSize(0),
138 stopWorkerThread(false)
139 {
140 }
141
143 {
144 stopAsyncStorageWorker();
145
146 // Clean up any remaining items in the lock-free queue
147 StorageItem* item;
148 while (storageQueue.pop(item))
149 {
150 delete item;
151 }
152 }
153
154 void
155 directlyStore(const armem::wm::Memory& memory, bool simulatedVersion = false)
156 {
157 // DEADLOCK FIX: Removed storeMutex to prevent lock order inversion
158 // Previously: storeMutex → ltm_mutex (in _directlyStore)
159 // This could deadlock with code that holds ltm_mutex → storeMutex
160 //
161 // The _directlyStore() implementations are responsible for their own
162 // thread safety (e.g., Memory::_directlyStore uses ltm_mutex)
163
164 TIMING_START(LTM_Memory_DirectlyStore);
165 _directlyStore(memory, simulatedVersion);
166 TIMING_END_STREAM(LTM_Memory_DirectlyStore, ARMARX_DEBUG);
167 }
168
169 void
170 directlyStore(const armem::server::wm::Memory& serverMemory, bool simulatedVersion = false)
171 {
173 memory.setName(serverMemory.name());
174 auto ids = memory.update(armem::toCommit(serverMemory), true);
175 ARMARX_DEBUG << "Amount of ids in update: " << ids.size();
176 this->directlyStore(memory, simulatedVersion);
177 }
178
179 void
181 {
182 //use this to count how much work is still left
183 }
184
185 /**
186 * @brief Flush the async storage queue and wait for all pending items to be stored.
187 * This blocks until the queue is empty and all storage operations are complete.
188 * @param timeoutMs Maximum time to wait in milliseconds (0 = wait indefinitely)
189 * @return true if queue was flushed successfully, false if timeout occurred
190 */
191 bool
192 flushAsyncStorage(int timeoutMs = 0)
193 {
194 ARMARX_DEBUG << "Flushing async storage queue...";
195
196 auto startTime = std::chrono::steady_clock::now();
197
198 while (true)
199 {
200 // Lock-free check: no mutex needed for atomic operations
201 if (queueSize.load(std::memory_order_acquire) == 0 &&
202 numThreadsProcessing.load(std::memory_order_acquire) == 0)
203 {
204 ARMARX_DEBUG << "Async storage queue flushed successfully";
205 return true;
206 }
207
208 // Check timeout
209 if (timeoutMs > 0)
210 {
211 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
212 std::chrono::steady_clock::now() - startTime).count();
213 if (elapsed >= timeoutMs)
214 {
215 ARMARX_WARNING << "Flush timeout after " << elapsed << "ms with "
216 << getQueueSize() << " items still in queue";
217 return false;
218 }
219 }
220
221 std::this_thread::sleep_for(std::chrono::milliseconds(10));
222 }
223 }
224
225 /**
226 * @brief Get the current size of the async storage queue
227 */
228 size_t
230 {
231 return queueSize.load(std::memory_order_acquire);
232 }
233
234 /**
235 * @brief Get the async storage statistics
236 */
239 {
240 return asyncStats;
241 }
242
243 /**
244 * @brief Reset the async storage statistics
245 */
246 void
248 {
249 asyncStats.reset();
250 }
251
252 /**
253 * @brief Get the number of threads currently processing items
254 */
255 size_t
257 {
258 return numThreadsProcessing.load(std::memory_order_acquire);
259 }
260
261 protected:
262 void
264 {
265 ARMARX_CHECK_NOT_EMPTY(id.memoryName) << " The full id was: " << id.str();
266
267 buffer->id() = id.getMemoryID();
268 to_store->id() = id.getMemoryID();
269 }
270
271 void
273 {
274 // Start the async storage worker thread
275 startAsyncStorageWorker();
276
277 // create task if not already exists
278 if (!task)
279 {
280 int waitingTimeMs = 1000.f / storeFrequency;
282 this, &BufferedMemoryMixin::storeBuffer, waitingTimeMs);
283 task->start();
284 task->setDelayWarningTolerance(
285 waitingTimeMs); //a warning will be issued if the task takes longer than the waitingTime
286 }
287 }
288
289 void
291 {
292 if (task)
293 {
294 task->stop();
295 task = nullptr;
296 }
297
298 // Stop the async storage worker thread (but don't flush yet)
299 // The caller should call flushAsyncStorage() if they want to wait for completion
300 }
301
303 getBuffer() const
304 {
305 std::lock_guard l(bufferMutex);
306 return *buffer;
307 }
308
309 void
311 {
312 std::shared_ptr<const armem::wm::Memory> memoryToStore;
313 {
314 std::lock_guard l(bufferMutex);
315 to_store = std::move(buffer);
316 buffer = std::make_unique<armem::wm::Memory>(to_store->id());
317 // Convert unique_ptr to shared_ptr<const>
318 memoryToStore = std::shared_ptr<const armem::wm::Memory>(std::move(to_store));
319 }
320
321 if (memoryToStore->empty())
322 {
323 ARMARX_DEBUG << deactivateSpam(120) << "Cannot store an empty buffer. Ignoring.";
324 return;
325 }
326
327 // Pre-filter the memory before enqueuing to reduce queue pressure
328 auto preFilterStart = std::chrono::steady_clock::now();
329 uint64_t filteredCount = 0;
330 uint64_t passedCount = 0;
331 auto filteredMemory = _preFilterMemory(*memoryToStore, filteredCount, passedCount);
332 auto preFilterEnd = std::chrono::steady_clock::now();
333
334 // Update pre-filter statistics
335 auto preFilterTimeNs = std::chrono::duration_cast<std::chrono::nanoseconds>(
336 preFilterEnd - preFilterStart).count();
337 asyncStats.snapshotsPreFiltered.fetch_add(filteredCount, std::memory_order_relaxed);
338 asyncStats.snapshotsPassedPreFilter.fetch_add(passedCount, std::memory_order_relaxed);
339 asyncStats.totalPreFilterTimeNs.fetch_add(preFilterTimeNs, std::memory_order_relaxed);
340
341 // If all snapshots were filtered, skip enqueuing
342 if (!filteredMemory || filteredMemory->empty())
343 {
344 ARMARX_DEBUG << deactivateSpam(10) << "All " << filteredCount
345 << " snapshots were pre-filtered, skipping enqueue";
346 return;
347 }
348
349 // Push filtered memory to async storage queue
350 enqueueForAsyncStorage(std::move(filteredMemory));
351 }
352
353 /// configuration
354 void
355 configureMixin(const nlohmann::json& json)
356 {
357 if (json.find("BufferedMemory.storeFrequency") != json.end())
358 {
359 storeFrequency = json.at("BufferedMemory.storeFrequency");
360 ARMARX_INFO << "Setting store frequency from configuration json to "
362 }
363 if (json.find("BufferedMemory.maxAsyncQueueSize") != json.end())
364 {
365 maxAsyncQueueSize = json.at("BufferedMemory.maxAsyncQueueSize");
366 ARMARX_INFO << "Setting max async queue size from configuration json to "
368 }
369 if (json.find("BufferedMemory.numAsyncStorageThreads") != json.end())
370 {
371 numAsyncStorageThreads = json.at("BufferedMemory.numAsyncStorageThreads");
372 ARMARX_INFO << "Setting number of async storage threads from configuration json to "
374 }
375 if (json.find("BufferedMemory.workerShutdownTimeoutSeconds") != json.end())
376 {
377 workerShutdownTimeoutSeconds = json.at("BufferedMemory.workerShutdownTimeoutSeconds");
378 ARMARX_INFO << "Setting worker shutdown timeout from configuration json to "
379 << workerShutdownTimeoutSeconds << " seconds";
380 }
381 }
382
383 void
384 createPropertyDefinitions(PropertyDefinitionsPtr& defs, const std::string& prefix)
385 {
386 defs->optional(storeFrequency, prefix + "storeFrequency");
387 defs->optional(maxAsyncQueueSize, prefix + "maxAsyncQueueSize");
388 defs->optional(numAsyncStorageThreads, prefix + "numAsyncStorageThreads");
389 defs->optional(workerShutdownTimeoutSeconds, prefix + "workerShutdownTimeoutSeconds");
390 }
391
393 bool simulatedVersion = false) = 0;
394
395 /**
396 * @brief Pre-filter a memory object before enqueuing for async storage.
397 *
398 * This method applies snapshot filters BEFORE the memory enters the async queue,
399 * reducing queue pressure and avoiding work on the async worker threads.
400 *
401 * @param memory The memory to filter
402 * @param filteredCount Output: number of snapshots that were filtered out
403 * @param passedCount Output: number of snapshots that passed the filter
404 * @return A new memory containing only the snapshots that passed the filters,
405 * or nullptr if all snapshots were filtered out
406 */
407 virtual std::shared_ptr<armem::wm::Memory> _preFilterMemory(
409 uint64_t& filteredCount,
410 uint64_t& passedCount) = 0;
411
412 void
414 {
415 std::lock_guard l(bufferMutex);
416 buffer->append(memory);
417 }
418
419 /**
420 * @brief Public interface to enqueue memory for async storage
421 * This allows MemoryBase::store() to use the async thread pool
422 */
423 void
424 enqueueForAsyncStoragePublic(std::shared_ptr<const armem::wm::Memory> memory)
425 {
426 enqueueForAsyncStorage(std::move(memory));
427 }
428
429 protected:
430 /**
431 * @brief Enqueue a memory object for async storage
432 */
433 void
434 enqueueForAsyncStorage(std::shared_ptr<const armem::wm::Memory> memory)
435 {
437 }
438
439 /**
440 * @brief Enqueue snapshots for deferred conversion and async storage
441 * This defers the expensive toMemory() conversion to the async thread
442 */
443 void
445 {
446 enqueueStorageItem(StorageItem{std::move(pending)});
447 }
448
449 private:
450 /**
451 * @brief Start the async storage worker threads (thread pool)
452 */
453 void
454 startAsyncStorageWorker()
455 {
456 std::lock_guard<std::mutex> lock(workerMutex);
457 if (workerThreads.empty())
458 {
459 stopWorkerThread = false;
460 size_t numThreads = std::max(size_t(1), numAsyncStorageThreads);
461 workerThreads.reserve(numThreads);
462 for (size_t i = 0; i < numThreads; ++i)
463 {
464 workerThreads.emplace_back(&BufferedMemoryMixin::asyncStorageWorker, this, i);
465 }
466 ARMARX_INFO << "Async storage thread pool started with " << numThreads << " worker threads";
467 }
468 }
469
470 /**
471 * @brief Stop the async storage worker threads with configurable timeout
472 */
473 void
474 stopAsyncStorageWorker()
475 {
476 {
477 std::lock_guard<std::mutex> lock(workerMutex);
478 if (workerThreads.empty())
479 {
480 return;
481 }
482 stopWorkerThread = true;
483 }
484 queueCondition.notify_all();
485
486 // Wait for threads to finish with timeout
487 auto startTime = std::chrono::steady_clock::now();
488 auto timeout = std::chrono::seconds(workerShutdownTimeoutSeconds);
489 bool allThreadsFinished = false;
490
491 // Poll until all threads exit or timeout
492 while (true)
493 {
494 std::this_thread::sleep_for(std::chrono::milliseconds(100));
495
496 // Check if all threads have finished processing
497 if (numThreadsProcessing.load(std::memory_order_acquire) == 0 &&
498 queueSize.load(std::memory_order_acquire) == 0)
499 {
500 allThreadsFinished = true;
501 break;
502 }
503
504 // Check timeout
505 auto elapsed = std::chrono::steady_clock::now() - startTime;
506 if (elapsed >= timeout)
507 {
508 ARMARX_WARNING << "Worker threads did not finish within timeout of "
509 << workerShutdownTimeoutSeconds << " seconds. "
510 << "Still processing: " << numThreadsProcessing.load()
511 << " threads, queue size: " << queueSize.load();
512 break;
513 }
514 }
515
516 // RACE CONDITION FIX: Hold lock during thread join to protect workerThreads vector
517 {
518 std::lock_guard<std::mutex> lock(workerMutex);
519
520 if (allThreadsFinished)
521 {
522 // Threads finished gracefully, join them
523 for (auto& thread : workerThreads)
524 {
525 if (thread.joinable())
526 {
527 thread.join();
528 }
529 }
530 ARMARX_INFO << "Async storage thread pool stopped gracefully";
531 }
532 else
533 {
534 // Timeout occurred - detach threads to avoid blocking
535 ARMARX_WARNING << "Detaching worker threads that did not terminate in time. "
536 << "This may indicate slow disk I/O or stuck operations.";
537 for (auto& thread : workerThreads)
538 {
539 if (thread.joinable())
540 {
541 thread.detach();
542 }
543 }
544 ARMARX_WARNING << "Async storage thread pool stopped with timeout - threads detached";
545 }
546
547 workerThreads.clear();
548 }
549 }
550
551 protected:
552 /**
553 * @brief Internal method to enqueue any storage item (Memory or PendingConversion)
554 * PERFORMANCE: Uses lock-free queue to avoid mutex contention at 50Hz commit rate
555 * NON-BLOCKING: If the queue is full, the item is DROPPED immediately to prevent
556 * upstream queue (e.g., RobotWriterQueue) from backing up.
557 */
558 void
560 {
561 // Check queue size BEFORE allocating to avoid unnecessary allocation
562 size_t currentSize = queueSize.load(std::memory_order_acquire);
563 if (currentSize >= maxAsyncQueueSize)
564 {
565 // NON-BLOCKING DROP POLICY: Immediately drop new items when queue is full
566 // This prevents upstream queues (RobotWriterQueue) from backing up
567 asyncStats.backpressureEvents.fetch_add(1, std::memory_order_relaxed);
568
569 // Count snapshots in the item we're about to drop
570 size_t snapshotCount = countSnapshotsInItem(item);
571 asyncStats.snapshotsDroppedBackpressure.fetch_add(snapshotCount, std::memory_order_relaxed);
572
574 << "LTM async storage queue full (" << currentSize << "/" << maxAsyncQueueSize
575 << " items). DROPPING " << snapshotCount << " snapshots to prevent upstream backup. "
576 << "Consider increasing queue size, reducing commit rate, or enabling more aggressive filtering.";
577
578 return; // Non-blocking: return immediately without waiting
579 }
580
581 // Allocate on heap for lock-free queue (requires pointer type)
582 StorageItem* itemPtr = new StorageItem(std::move(item));
583
584 // Lock-free push - should succeed since we checked size above
585 // (there's a small race window but that's acceptable)
586 if (!storageQueue.push(itemPtr))
587 {
588 // Rare case: queue filled up between size check and push
589 size_t snapshotCount = countSnapshotsInItem(*itemPtr);
590 asyncStats.snapshotsDroppedBackpressure.fetch_add(snapshotCount, std::memory_order_relaxed);
591 ARMARX_WARNING << deactivateSpam(1) << "Lock-free queue race: dropping " << snapshotCount << " snapshots";
592 delete itemPtr;
593 return;
594 }
595
596 // Track enqueued item
597 asyncStats.totalItemsEnqueued.fetch_add(1, std::memory_order_relaxed);
598
599 // Atomically increment size counter
600 queueSize.fetch_add(1, std::memory_order_release);
601
602 // Signal waiting worker threads (use mutex only for condition variable)
603 {
604 std::lock_guard<std::mutex> lock(queueMutex);
605 }
606 queueCondition.notify_one();
607 }
608
609 /**
610 * @brief Count the number of snapshots in a storage item
611 */
612 size_t
614 {
615 size_t count = 0;
616 std::visit([&count](auto&& arg) {
617 using T = std::decay_t<decltype(arg)>;
618 if constexpr (std::is_same_v<T, std::shared_ptr<const armem::wm::Memory>>)
619 {
620 arg->forEachCoreSegment([&count](const auto& coreSegment) {
621 coreSegment.forEachProviderSegment([&count](const auto& providerSegment) {
622 providerSegment.forEachEntity([&count](const auto& entity) {
623 entity.forEachSnapshot([&count](const auto&) { count++; });
624 });
625 });
626 });
627 }
628 else if constexpr (std::is_same_v<T, PendingConversion>)
629 {
630 count = arg.snapshots.size();
631 }
632 }, item);
633 return count;
634 }
635
636 /**
637 * @brief Worker thread that processes the async storage queue
638 * @param threadId ID of this worker thread (for logging)
639 */
640 void
641 asyncStorageWorker(size_t threadId)
642 {
643 ARMARX_INFO << "Async storage worker thread #" << threadId << " started";
644
645 size_t itemsProcessed = 0;
646 double totalTimeMs = 0.0;
647 double minTimeMs = std::numeric_limits<double>::max();
648 double maxTimeMs = 0.0;
649
650 while (true)
651 {
652 StorageItem* itemPtr = nullptr;
653 size_t queueSizeBeforePop = 0;
654 bool hasItem = false;
655
656 // Try lock-free pop first (fast path - no mutex)
657 if (storageQueue.pop(itemPtr))
658 {
659 // Got an item from the queue
660 queueSizeBeforePop = queueSize.fetch_sub(1, std::memory_order_acq_rel);
661 numThreadsProcessing.fetch_add(1, std::memory_order_release);
662 hasItem = true;
663 }
664 else
665 {
666 // Queue is empty - wait for signal or stop
667 std::unique_lock<std::mutex> lock(queueMutex);
668
669 // Exit if stop requested and queue is still empty
670 if (stopWorkerThread.load(std::memory_order_acquire))
671 {
672 // Double-check queue is empty after acquiring lock
673 if (!storageQueue.pop(itemPtr))
674 {
675 break;
676 }
677 // Found an item, process it before exiting
678 queueSizeBeforePop = queueSize.fetch_sub(1, std::memory_order_acq_rel);
679 numThreadsProcessing.fetch_add(1, std::memory_order_release);
680 hasItem = true;
681 }
682 else
683 {
684 // Wait for notification
685 queueCondition.wait(lock, [this]() {
686 return queueSize.load(std::memory_order_acquire) > 0 ||
687 stopWorkerThread.load(std::memory_order_acquire);
688 });
689 // Loop back to try popping again
690 continue;
691 }
692 }
693
694 // Store the memory (outside the lock to avoid blocking enqueue operations)
695 if (hasItem)
696 {
697 auto startTime = std::chrono::high_resolution_clock::now();
698 size_t snapshotCount = 0;
699 double conversionTimeMs = 0.0;
700
701 try
702 {
703 TIMING_START(LTM_AsyncStorage);
704
705 // Handle variant: either pre-converted Memory or pending conversion
706 // Note: itemPtr is a pointer to StorageItem, dereference it
707 std::visit([&](auto&& item) {
708 using T = std::decay_t<decltype(item)>;
709 if constexpr (std::is_same_v<T, std::shared_ptr<const armem::wm::Memory>>)
710 {
711 // Already converted Memory - just store it
712 item->forEachCoreSegment(
713 [&snapshotCount](const auto& coreSegment)
714 {
715 coreSegment.forEachProviderSegment(
716 [&snapshotCount](const auto& providerSegment)
717 {
718 providerSegment.forEachEntity(
719 [&snapshotCount](const auto& entity)
720 {
721 entity.forEachSnapshot(
722 [&snapshotCount](const auto&) { snapshotCount++; });
723 });
724 });
725 });
726 this->directlyStore(*item);
727 }
728 else if constexpr (std::is_same_v<T, PendingConversion>)
729 {
730 // Pending conversion - do toMemory() here in async thread
731 auto conversionStart = std::chrono::high_resolution_clock::now();
732
733 snapshotCount = item.snapshots.size();
734 auto memory = std::make_shared<armem::wm::Memory>(item.memoryName);
735
736 // Perform the conversion using captured metadata
737 for (const auto& snapshot : item.snapshots)
738 {
739 const std::string& coreSegmentName = snapshot.id().coreSegmentName;
740 const std::string& providerSegmentName = snapshot.id().providerSegmentName;
741
742 // Find metadata for this core segment
743 auto coreMeta = std::find_if(item.segmentMetadata.begin(), item.segmentMetadata.end(),
744 [&coreSegmentName](const auto& meta) { return meta.coreSegmentName == coreSegmentName; });
745
746 if (coreMeta == item.segmentMetadata.end())
747 {
748 ARMARX_WARNING << "Missing metadata for core segment: " << coreSegmentName;
749 continue;
750 }
751
752 // Add core segment if needed
753 if (!memory->hasCoreSegment(coreSegmentName))
754 {
755 memory->addCoreSegment(coreSegmentName, coreMeta->coreSegmentAronType);
756 }
757 auto* coreSegment = memory->findCoreSegment(coreSegmentName);
758
759 // Add provider segment if needed
760 if (!coreSegment->hasProviderSegment(providerSegmentName))
761 {
762 auto providerTypeIt = coreMeta->providerSegmentAronTypes.find(providerSegmentName);
763 aron::type::ObjectPtr providerType = (providerTypeIt != coreMeta->providerSegmentAronTypes.end())
764 ? providerTypeIt->second : nullptr;
765 coreSegment->addProviderSegment(providerSegmentName, providerType);
766 }
767 auto* providerSegment = coreSegment->findProviderSegment(providerSegmentName);
768
769 // Add entity and snapshot
770 if (!providerSegment->hasEntity(snapshot.id().entityName))
771 {
772 providerSegment->addEntity(snapshot.id().entityName);
773 }
774 auto* entity = providerSegment->findEntity(snapshot.id().entityName);
775 entity->addSnapshot(snapshot);
776 }
777
778 auto conversionEnd = std::chrono::high_resolution_clock::now();
779 conversionTimeMs = std::chrono::duration<double, std::milli>(conversionEnd - conversionStart).count();
780
781 // Now store the converted memory
782 this->directlyStore(*memory);
783 }
784 }, *itemPtr); // Dereference the pointer to get the StorageItem
785
786 TIMING_END_STREAM(LTM_AsyncStorage, ARMARX_DEBUG);
787 }
788 catch (const std::exception& e)
789 {
790 // The item is dropped here just as surely as under backpressure, so it
791 // has to be counted; leaving it out made these losses invisible.
792 asyncStats.snapshotsDropped.fetch_add(countSnapshotsInItem(*itemPtr),
793 std::memory_order_relaxed);
794 ARMARX_ERROR << "Error during async storage: " << e.what();
795 }
796 catch (...)
797 {
798 asyncStats.snapshotsDropped.fetch_add(countSnapshotsInItem(*itemPtr),
799 std::memory_order_relaxed);
800 ARMARX_ERROR << "Unknown error during async storage";
801 }
802
803 // Clean up the heap-allocated item
804 delete itemPtr;
805
806 auto endTime = std::chrono::high_resolution_clock::now();
807 double durationMs = std::chrono::duration<double, std::milli>(endTime - startTime).count();
808 uint64_t durationNs = std::chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime).count();
809
810 // Update local statistics
811 itemsProcessed++;
812 totalTimeMs += durationMs;
813 minTimeMs = std::min(minTimeMs, durationMs);
814 maxTimeMs = std::max(maxTimeMs, durationMs);
815 double avgTimeMs = totalTimeMs / itemsProcessed;
816
817 // Update global async storage statistics
818 asyncStats.totalItemsProcessed.fetch_add(1, std::memory_order_relaxed);
819 asyncStats.totalSnapshotsStored.fetch_add(snapshotCount, std::memory_order_relaxed);
820 asyncStats.totalStorageTimeNs.fetch_add(durationNs, std::memory_order_relaxed);
821 if (conversionTimeMs > 0)
822 {
823 uint64_t conversionNs = static_cast<uint64_t>(conversionTimeMs * 1e6);
824 asyncStats.totalConversionTimeNs.fetch_add(conversionNs, std::memory_order_relaxed);
825 }
826
827 // Update max storage time (using compare-exchange)
828 uint64_t currentMax = asyncStats.maxStorageTimeNs.load(std::memory_order_relaxed);
829 while (durationNs > currentMax)
830 {
831 if (asyncStats.maxStorageTimeNs.compare_exchange_weak(currentMax, durationNs, std::memory_order_relaxed))
832 {
833 break;
834 }
835 }
836
837 // Decrement processing counter and get queue size (all lock-free)
838 numThreadsProcessing.fetch_sub(1, std::memory_order_release);
839 size_t remainingInQueue = queueSize.load(std::memory_order_acquire);
840
841 ARMARX_DEBUG << "Async storage completed: "
842 << "snapshots=" << snapshotCount
843 << ", time=" << std::fixed << std::setprecision(2) << durationMs << "ms"
844 << (conversionTimeMs > 0 ? " (conversion=" + std::to_string(static_cast<int>(conversionTimeMs)) + "ms)" : "")
845 << ", queue_before=" << queueSizeBeforePop
846 << ", queue_after=" << remainingInQueue
847 << " | Stats: items=" << itemsProcessed
848 << ", avg=" << std::fixed << std::setprecision(2) << avgTimeMs << "ms"
849 << ", min=" << std::fixed << std::setprecision(2) << minTimeMs << "ms"
850 << ", max=" << std::fixed << std::setprecision(2) << maxTimeMs << "ms";
851 }
852 }
853
854 ARMARX_DEBUG << "Async storage worker thread #" << threadId << " exiting. Final stats: "
855 << "total_items=" << itemsProcessed
856 << ", total_time=" << std::fixed << std::setprecision(2) << totalTimeMs << "ms"
857 << ", avg_time=" << std::fixed << std::setprecision(2) << (itemsProcessed > 0 ? totalTimeMs / itemsProcessed : 0.0) << "ms"
858 << ", min_time=" << std::fixed << std::setprecision(2) << (itemsProcessed > 0 ? minTimeMs : 0.0) << "ms"
859 << ", max_time=" << std::fixed << std::setprecision(2) << maxTimeMs << "ms";
860 }
861
862
863 protected:
864 /// Internal memory for data consolidated from wm to ltm (double-buffer)
865 /// The to-put-to-ltm buffer (contains data in plain text)
866 /// This buffer may still be filtered (e.g. snapshot filters).
867 /// This means that it is not guaranteed that all data in the buffer will be stored in the ltm
868 std::unique_ptr<armem::wm::Memory> buffer;
869 std::unique_ptr<armem::wm::Memory> to_store;
870 std::atomic_flag storeFlag = ATOMIC_FLAG_INIT;
871
872 /// The frequency (Hz) to store data to the ltm
873 float storeFrequency = 10;
874
875 /// Maximum size of the async storage queue (default 100 items)
876 size_t maxAsyncQueueSize = 1000;
877
878 /// Number of worker threads for async storage (default 4)
880
881 /// Timeout in seconds for worker thread shutdown (default 30 seconds)
883
884 private:
885 /// The periodic'task to store the content of the buffer to the ltm
887
888 /// A mutex to access the buffer object
889 /// PERFORMANCE: Using std::mutex instead of recursive_mutex since no recursion occurs
890 /// All buffer access methods (getBuffer, storeBuffer, addToBuffer) are non-recursive
891 mutable std::mutex bufferMutex;
892 // NOTE: storeMutex removed - was causing lock order inversion deadlock
893 // _directlyStore() implementations handle their own synchronization
894
895 /// Async storage queue infrastructure
896 /// PERFORMANCE: Using lock-free queue to eliminate mutex contention at 50Hz commit rate
897 boost::lockfree::queue<StorageItem*> storageQueue; // Lock-free queue (holds pointers)
898 std::atomic<size_t> queueSize{0}; // Atomic counter for queue size
899 mutable std::mutex queueMutex; // Only used for condition variable signaling, not queue access
900 std::condition_variable queueCondition;
901 std::vector<std::thread> workerThreads;
902 std::mutex workerMutex;
903 std::atomic<bool> stopWorkerThread;
904 std::atomic<size_t> numThreadsProcessing{0}; // Tracks how many threads are currently processing items
905
906 /// Statistics for async storage operations
907 mutable AsyncStorageStatistics asyncStats;
908 };
909} // namespace armarx::armem::server::ltm::detail::mixin
#define ARMARX_CHECK_NOT_EMPTY(c)
SpamFilterDataPtr deactivateSpam(SpamFilterDataPtr const &spamFilter, float deactivationDurationSec, const std::string &identifier, bool deactivate)
Definition Logging.cpp:75
The periodic task executes one thread method repeatedly using the time period specified in the constr...
IceUtil::Handle< PeriodicTask< T > > pointer_type
Shared pointer type for convenience.
A memory storing data on the hard drive and in mongodb (needs 'armarx memory start' to start the mong...
Definition Memory.h:24
virtual void _directlyStore(const armem::wm::Memory &memory, bool simulatedVersion=false)=0
virtual std::shared_ptr< armem::wm::Memory > _preFilterMemory(const armem::wm::Memory &memory, uint64_t &filteredCount, uint64_t &passedCount)=0
Pre-filter a memory object before enqueuing for async storage.
void resetAsyncStorageStatistics()
Reset the async storage statistics.
void createPropertyDefinitions(PropertyDefinitionsPtr &defs, const std::string &prefix)
size_t countSnapshotsInItem(const StorageItem &item) const
Count the number of snapshots in a storage item.
size_t getQueueSize() const
Get the current size of the async storage queue.
void directlyStore(const armem::server::wm::Memory &serverMemory, bool simulatedVersion=false)
void enqueueForAsyncStorage(std::shared_ptr< const armem::wm::Memory > memory)
Enqueue a memory object for async storage.
void configureMixin(const nlohmann::json &json)
configuration
const AsyncStorageStatistics & getAsyncStorageStatistics() const
Get the async storage statistics.
std::variant< std::shared_ptr< const armem::wm::Memory >, PendingConversion > StorageItem
Can hold either pre-converted Memory or pending conversion data.
size_t getNumThreadsProcessing() const
Get the number of threads currently processing items.
void enqueuePendingConversion(PendingConversion pending)
Enqueue snapshots for deferred conversion and async storage This defers the expensive toMemory() conv...
void enqueueForAsyncStoragePublic(std::shared_ptr< const armem::wm::Memory > memory)
Public interface to enqueue memory for async storage This allows MemoryBase::store() to use the async...
bool flushAsyncStorage(int timeoutMs=0)
Flush the async storage queue and wait for all pending items to be stored.
void directlyStore(const armem::wm::Memory &memory, bool simulatedVersion=false)
void asyncStorageWorker(size_t threadId)
Worker thread that processes the async storage queue.
void enqueueStorageItem(StorageItem item)
Internal method to enqueue any storage item (Memory or PendingConversion) PERFORMANCE: Uses lock-free...
Client-side working memory.
Brief description of class memory.
Definition memory.h:39
#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
#define TIMING_START(name)
Helper macro to do timing tests.
Definition TimeUtil.h:289
#define TIMING_END_STREAM(name, os)
Prints duration.
Definition TimeUtil.h:310
Commit toCommit(const ContainerT &container)
Definition operations.h:23
std::shared_ptr< Object > ObjectPtr
Definition Object.h:36
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
uint64_t totalSnapshotsDropped() const
Every snapshot the LTM discarded, whatever the reason.
Holds snapshots and metadata for deferred conversion in async thread This allows us to defer the expe...