DiskPersistence.h
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <chrono>
5#include <condition_variable>
6#include <filesystem>
7#include <memory>
8#include <mutex>
9#include <string>
10#include <thread>
11#include <unordered_map>
12#include <vector>
13
16
18{
19 /**
20 * @brief Statistics for batch write operations.
21 */
23 {
24 std::atomic<uint64_t> totalBatchesWritten{0};
25 std::atomic<uint64_t> totalItemsBatched{0};
26 std::atomic<uint64_t> totalBytesWritten{0};
27 std::atomic<uint64_t> totalFlushTimeNs{0};
28 std::atomic<uint64_t> maxBatchSize{0};
29 std::atomic<uint64_t> flushBySize{0}; // Flushes triggered by size threshold
30 std::atomic<uint64_t> flushByTime{0}; // Flushes triggered by time threshold
31 std::atomic<uint64_t> flushByExplicit{0}; // Explicit flush calls
32
33 void reset()
34 {
39 maxBatchSize = 0;
40 flushBySize = 0;
41 flushByTime = 0;
43 }
44
45 double getAvgFlushTimeMs() const
46 {
47 uint64_t count = totalBatchesWritten.load(std::memory_order_relaxed);
48 if (count == 0) return 0.0;
49 return totalFlushTimeNs.load(std::memory_order_relaxed) / (count * 1e6);
50 }
51
52 double getAvgBatchSize() const
53 {
54 uint64_t count = totalBatchesWritten.load(std::memory_order_relaxed);
55 if (count == 0) return 0.0;
56 return static_cast<double>(totalItemsBatched.load(std::memory_order_relaxed)) / count;
57 }
58 };
59
60 /**
61 * @brief A single item pending batch write.
62 */
64 {
66 std::string key;
67 std::vector<unsigned char> data;
68 std::chrono::steady_clock::time_point enqueuedTime;
69 };
70
72 {
73 public:
74 FileIdentifier(std::string& filename, std::string& fileType) :
75 filename_(filename),
76 fileType_(fileType){
77
78 };
79
81 {
82 }
83
84 std::string
85 getKey() override
86 {
87 return filename_ + fileType_;
88 }
89
90 private:
91 std::string filename_;
92 std::string fileType_;
93 };
94
95 /**
96 * @brief Persistence strategy that writes items (e.g. json files) to a specific container (a directory)
97 * Use it to write the data of a WM to disk.
98 *
99 * Where are the items written (=:location)?
100 * /memoryParentPath/exportName/path(id)/
101 *
102 * How is the file of an item named?
103 * -> Just they key (e.g. filename = key = "data.aron.json")
104 * If you might want a more sophisticated solution fell free to implement your custom ItemIdentifier today!
105 */
107 {
108 public:
109 DiskPersistence() : DiskPersistence(std::filesystem::path("."))
110 {
111 }
112
113 DiskPersistence(const std::filesystem::path& memoryParentPath) :
114 DiskPersistence("Disk", "DefaultExport", memoryParentPath)
115 {
116 }
117
118 /**
119 * @param identifier basically a unique name for the strategy (important if you use different strategies @see RedundantPersistenceStrategy)
120 * @param exportName identifier for the exported memory. A new directory with name is created beneath the memoryParentPath. Everything is stored inside it.
121 * @param memoryParentPath path where the memory should be exported to
122 */
123 DiskPersistence(const std::string& identifier,
124 const std::string& exportName,
125 const std::filesystem::path& memoryParentPath) :
126 MemoryPersistenceStrategy(identifier, exportName), memoryParentPath_(memoryParentPath)
127 {
128 }
129
130 /**
131 * @brief Destructor - flushes pending batch and stops batch thread.
132 */
134 {
135 stopBatchWriter();
136 }
137
138 /**
139 * Returns all containers for the current id.
140 * @return containers <=> directories at current location (=/memoryParentPath/exportName/path(id)/)
141 */
142 std::vector<std::string> getContainerKeys(const armarx::armem::MemoryID& id) override;
143
144 /**
145 * Returns all items for the current id.
146 * @return items <=> files at the current location (=/memoryParentPath/exportName/path(id)/)
147 */
148 std::vector<std::string> getItemKeys(const armarx::armem::MemoryID& id) override;
149
150 /**
151 * Checks if the container is available for the current memory id.
152 * @return true if the current location contains the directory with name 'key'
153 */
154 bool containsContainer(const armarx::armem::MemoryID& id, std::string key) override;
155
156 /**
157 * Checks if current container contains the item defined by its key.
158 * @return true if the current location contains a file with the name 'key'
159 */
160 bool containsItem(const armarx::armem::MemoryID& id, std::string key) override;
161
162 /**
163 * Create a new file with name 'key' and stores the data inside it.
164 */
165 void storeItem(const armarx::armem::MemoryID& id,
166 std::string key,
167 std::vector<unsigned char>& data) override;
168
169 /**
170 * Reads the data of the file with name 'key' at the current location.
171 * @return data if a file was found, an empty vector if the file is empty or was not found
172 */
173 std::vector<unsigned char> retrieveItem(const armarx::armem::MemoryID& id,
174 std::string key) override;
175
176 void
177 createPropertyDefinitions(PropertyDefinitionsPtr& defs, const std::string& prefix) override
178 {
179 // Nothing to do
180 }
181
182 void
184 {
185 this->minDiskSpace = minDiskSpace;
186 if (!enoughDiskSpaceLeft())
187 {
188 ARMARX_WARNING << "Not enough available disk space for DiskPersistance Strategy. "
189 "You need at least "
190 << this->minDiskSpace
191 << " GB available disk space to record into LTM using this strategy";
192 }
193 }
194
195 int minDiskSpace = 50; // in GB
196
197 // ==================== Compression Configuration ====================
198
199 /**
200 * @brief Enable transparent gzip compression of exported JSON files.
201 *
202 * When enabled, items whose key ends in ".json" and whose payload is at least
203 * getCompressionMinBytes() large are gzipped and stored under "<key>.gz". Reading is
204 * unaffected: containsItem()/retrieveItem() accept the plain (logical) key and transparently
205 * fall back to the ".gz" variant, and getItemKeys() reports logical keys. Exports written
206 * before this feature existed therefore keep working, and an export may freely mix both.
207 *
208 * The compressed file deliberately gets a ".gz" suffix rather than being compressed in
209 * place under the original name: a reader that does not know about compression then simply
210 * does not find the file, instead of choking on unparsable content.
211 */
212 void setCompressionEnabled(bool enable) { compressionEnabled_ = enable; }
213
214 bool isCompressionEnabled() const { return compressionEnabled_; }
215
216 /// zlib level, 1 (fastest) to 9 (smallest). Default 1 - see gzipCompress().
217 void setCompressionLevel(int level) { compressionLevel_ = level; }
218
219 int getCompressionLevel() const { return compressionLevel_; }
220
221 /**
222 * @brief Only compress payloads of at least this many bytes.
223 *
224 * Defaults to one filesystem block (4096). Compressing anything smaller cannot free a
225 * single block, so it would cost compatibility (older readers no longer find the file) for
226 * no disk saving at all. In practice this leaves metadata.aron.json and type.aron.json as
227 * plain JSON and only compresses the instance data, which is where the size actually is.
228 */
229 void setCompressionMinBytes(std::size_t bytes) { compressionMinBytes_ = bytes; }
230
231 std::size_t getCompressionMinBytes() const { return compressionMinBytes_; }
232
233 /// Suffix appended to the logical key when an item is stored compressed.
234 static constexpr const char* GZIP_SUFFIX = ".gz";
235
236 // ==================== Batch Write Configuration ====================
237
238 /**
239 * @brief Enable or disable batch writing.
240 * When enabled, writes are accumulated and flushed in batches for better I/O performance.
241 * @param enable True to enable batching, false for immediate writes (default behavior)
242 */
243 void setBatchWriteEnabled(bool enable);
244
245 /**
246 * @brief Check if batch writing is enabled.
247 */
248 bool isBatchWriteEnabled() const { return batchWriteEnabled_.load(std::memory_order_acquire); }
249
250 /**
251 * @brief Set the maximum number of items to accumulate before auto-flushing.
252 * @param size Maximum batch size (default: 100)
253 */
254 void setBatchSizeThreshold(size_t size) { batchSizeThreshold_ = size; }
255
256 /**
257 * @brief Set the maximum time to hold items before auto-flushing.
258 * @param ms Maximum time in milliseconds (default: 100ms)
259 */
260 void setBatchTimeThresholdMs(size_t ms) { batchTimeThresholdMs_ = ms; }
261
262 /**
263 * @brief Explicitly flush all pending batch writes to disk.
264 * This is automatically called when batch thresholds are reached.
265 */
266 void flushBatch();
267
268 /**
269 * @brief Get the current number of items pending in the batch buffer.
270 */
271 size_t getBatchPendingCount() const;
272
273 /**
274 * @brief Get batch write statistics.
275 */
276 const BatchWriteStatistics& getBatchStatistics() const { return batchStats_; }
277
278 /**
279 * @brief Reset batch write statistics.
280 */
281 void resetBatchStatistics() { batchStats_.reset(); }
282
283
284 public:
285 size_t getStorageErrorCount() const
286 {
287 return storageErrorCount_.load(std::memory_order_acquire);
288 }
289
291 {
292 storageErrorCount_.store(0, std::memory_order_release);
293 }
294
296
297 private:
298 std::filesystem::path memoryParentPath_;
299
300 // THREAD-SAFETY: Per-directory mutexes for parallel writes
301 // Allows concurrent writes to different directories (different entities)
302 // while preventing corruption within the same directory
303 mutable std::mutex directoryMutexMapLock_;
304 mutable std::unordered_map<std::string, std::unique_ptr<std::mutex>> directoryMutexes_;
305
306 // Error tracking
307 std::atomic<size_t> storageErrorCount_{0};
308
309 // Compression state (see the setters above)
310 std::atomic<bool> compressionEnabled_{false};
311 std::atomic<int> compressionLevel_{1};
312 std::atomic<std::size_t> compressionMinBytes_{4096};
313
314 /* Internal compression helpers */
315
316 /// Whether an item with this key is worth compressing (JSON payloads only; .png/.exr are
317 /// already compressed and would only grow).
318 static bool isCompressibleKey(const std::string& key);
319
320 /// Drop a trailing ".gz" if present, yielding the logical key callers use.
321 static std::string toLogicalKey(const std::string& key);
322
323 /**
324 * @brief Delete the other on-disk spelling of the item just written.
325 *
326 * One logical key must map to exactly one file: retrieveItem() and getItemKeys() both
327 * assume the plain and the ".gz" form are mutually exclusive. Toggling compression (or
328 * crossing the size threshold) on an export that already holds the other form would
329 * otherwise leave a sibling behind that shadows the fresh write.
330 *
331 * Called after the payload is safely on disk, never before: a crash in the window leaves
332 * both files, which is only the state this repairs, whereas deleting first could lose the
333 * item outright.
334 */
335 void removeStaleVariant(const armarx::armem::MemoryID& id, const std::string& writtenKey);
336
337 /// Read and gunzip a ".gz" item; returns empty and logs if the stream is unusable.
338 std::vector<unsigned char> readCompressedFile(const armarx::armem::MemoryID& id,
339 const std::string& compressedKey);
340
341 /* Internal disk logic */
342
343 bool fullPathExists(const armarx::armem::MemoryID& id);
344
345 std::vector<std::filesystem::path> getAllFiles(const armarx::armem::MemoryID& id);
346
347 std::vector<std::filesystem::path> getAllDirectories(const armarx::armem::MemoryID& id);
348
349 bool fileExists(const armarx::armem::MemoryID& id, const std::string& filename);
350
351 std::filesystem::path getFullPath(const armarx::armem::MemoryID& id);
352
353 void ensureFullPathExists(const armarx::armem::MemoryID& id,
354 bool createIfNotExistent = false);
355
356 void ensureFileExists(const armarx::armem::MemoryID& id,
357 const std::string& filename,
358 bool createIfNotExistent = false);
359
360 void writeDataToFile(const armarx::armem::MemoryID& id,
361 const std::string& filename,
362 const std::vector<unsigned char>& data);
363
364 std::vector<unsigned char> readDataFromFile(const armarx::armem::MemoryID& id,
365 const std::string& filename);
366
367 std::filesystem::path getMemoryParentPath();
368
369 bool enoughDiskSpaceLeft();
370
371 // ==================== Batch Write Implementation ====================
372
373 /**
374 * @brief Add an item to the batch buffer (called by storeItem when batching is enabled).
375 */
376 void enqueueBatchItem(const armarx::armem::MemoryID& id,
377 const std::string& key,
378 std::vector<unsigned char> data);
379
380 /**
381 * @brief Internal method to flush the batch buffer.
382 * @param reason Statistics tracking reason (size/time/explicit)
383 */
384 void flushBatchInternal(int reason);
385
386 /**
387 * @brief Write a batch of items to disk efficiently.
388 * Groups items by directory, creates directories once, writes files, syncs once.
389 */
390 void writeBatch(std::vector<BatchWriteItem>& items);
391
392 /**
393 * @brief Start the background batch flush thread.
394 */
395 void startBatchWriter();
396
397 /**
398 * @brief Stop the background batch flush thread.
399 */
400 void stopBatchWriter();
401
402 /**
403 * @brief Background thread function for time-based batch flushing.
404 */
405 void batchWriterThread();
406
407 // Batch write state
408 std::atomic<bool> batchWriteEnabled_{false};
409 size_t batchSizeThreshold_ = 100; // Flush when batch reaches this size
410 size_t batchTimeThresholdMs_ = 100; // Flush after this many ms
411
412 // Batch buffer
413 mutable std::mutex batchMutex_;
414 std::vector<BatchWriteItem> batchBuffer_;
415 std::chrono::steady_clock::time_point batchStartTime_;
416
417 // Background flush thread
418 std::thread batchWriterThread_;
419 std::atomic<bool> stopBatchWriter_{false};
420 std::condition_variable batchCondition_;
421
422 // Statistics
423 mutable BatchWriteStatistics batchStats_;
424 };
425} // namespace armarx::armem::server::ltm::persistence
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.
virtual ~DiskPersistence()
Destructor - flushes pending batch and stops batch thread.
void setBatchSizeThreshold(size_t size)
Set the maximum number of items to accumulate before auto-flushing.
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 setCompressionMinBytes(std::size_t bytes)
Only compress payloads of at least this many bytes.
void resetBatchStatistics()
Reset batch write statistics.
void flushBatch()
Explicitly flush all pending batch writes to disk.
const BatchWriteStatistics & getBatchStatistics() const
Get batch write statistics.
void setBatchTimeThresholdMs(size_t ms)
Set the maximum time to hold items before auto-flushing.
DiskPersistence(const std::filesystem::path &memoryParentPath)
void setCompressionEnabled(bool enable)
Enable transparent gzip compression of exported JSON files.
bool isBatchWriteEnabled() const
Check if batch writing is enabled.
void setCompressionLevel(int level)
zlib level, 1 (fastest) to 9 (smallest). Default 1 - see gzipCompress().
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.
void createPropertyDefinitions(PropertyDefinitionsPtr &defs, const std::string &prefix) override
DiskPersistence(const std::string &identifier, const std::string &exportName, const std::filesystem::path &memoryParentPath)
FileIdentifier(std::string &filename, std::string &fileType)
For usage if you might want to create the key using some logic defined with your strategy rather than...
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
std::chrono::steady_clock::time_point enqueuedTime