Mads
Multi-Agent Distributed System
Loading...
Searching...
No Matches
bag.hpp
Go to the documentation of this file.
1/*
2 ____
3 | __ ) __ _ __ _
4 | _ \ / _` |/ _` |
5 | |_) | (_| | (_| |
6 |____/ \__,_|\__, |
7 |___/
8
9Bespoke binary bag file format for MADS message recording/replay (P3).
10
11Stores the exact multi-part ZMQ wire frame MADS agents already exchange
12(topic + N parts, e.g. [topic][header][payload] or [topic][meta][blob
13bytes]) with no interpretation of the parts -- a bag round-trips whatever
14was on the wire byte-identical, whether it is JSON or a binary blob.
15
16BagWriter/BagReader are pure file I/O: no sockets, no Agent dependency, no
17ZMQ. mads-record/mads-play (src/main/record.cpp, src/main/play.cpp) are the
18only callers that combine this with Agent::receive_raw_message()/
19publish_raw_message().
20
21File layout
22-----------
23All multi-byte integers are little-endian, written/read byte-by-byte (never
24via a raw struct memcpy) so the format is identical across x86/ARM and
2532/64-bit builds regardless of host endianness or struct padding.
26
27 Header (12 bytes):
28 magic[4] = 'M','B','A','G'
29 format_ver: u32 = BAG_FORMAT_VERSION
30 flags: u32 bit 0: per-record CRC32 enabled
31
32 Record (repeated):
33 record_len: u32 number of bytes following this field for this record
34 (timestamp + topic + parts + optional crc32); lets a
35 linear scan detect a truncated trailing record with a
36 single bounds check instead of parsing into the void.
37 timestamp: i64 nanoseconds
38 topic_len: u32
39 topic: bytes[topic_len]
40 n_parts: u32
41 parts: n_parts * { part_len: u32; bytes[part_len] }
42 crc32: u32 present only if the header's CRC flag is set; CRC-32
43 (IEEE 802.3 / zlib polynomial) over every byte of this
44 record from `timestamp` through the last part, i.e.
45 everything `record_len` counts except the crc32 field
46 itself.
47
48 Footer (optional, written by BagWriter::close()):
49 footer_magic[4] = 'M','B','F','T'
50 record_count: u64
51 first_timestamp: i64
52 last_timestamp: i64
53 index: record_count * u64 byte offset of each record's `record_len`
54 field, for O(1) random access / seeking.
55
56 Trailer (fixed 16 bytes, always the last bytes of a cleanly-closed file):
57 trailer_magic[4] = 'M','B','T','R'
58 footer_offset: u64 absolute offset where footer_magic begins
59 trailer_format_ver: u32 duplicated here so a reader can sanity-check
60 the trailer without re-reading the header
61
62A reader first seeks to (file_size - 16) and checks for the trailer magic.
63If found (and consistent), the footer is read directly: O(1) `mads bag
64info` and O(1) seeking to any record via the index, with no need to scan
65every record. If the trailer is missing or inconsistent (e.g. the recording
66process was killed before close()), the reader falls back to a linear scan
67from just after the header, recovering the complete valid prefix and
68reporting truncated() -- never undefined behaviour, and never a hard
69failure just because the footer never got written.
70
71A bad magic or a format_ver newer than BAG_FORMAT_VERSION is a clean
72BagError, thrown from the BagReader constructor -- never garbage reads.
73
74Author(s): Paolo Bosetti
75*/
76
77#ifndef MADS_BAG_HPP
78#define MADS_BAG_HPP
79
80#include <cstdint>
81#include <filesystem>
82#include <fstream>
83#include <optional>
84#include <stdexcept>
85#include <string>
86#include <vector>
87
88namespace Mads {
89
92inline constexpr uint32_t BAG_FORMAT_VERSION = 1;
93
103class BagError : public std::runtime_error {
104public:
105 explicit BagError(const std::string &msg) : std::runtime_error(msg) {}
106};
107
110struct BagRecord {
111 int64_t timestamp_ns = 0;
112 std::string topic;
113 std::vector<std::string> parts;
114};
115
127public:
136 explicit BagWriter(const std::filesystem::path &path, bool enable_crc = true);
137
139
140 BagWriter(const BagWriter &) = delete;
141 BagWriter &operator=(const BagWriter &) = delete;
142
153 void write(int64_t timestamp_ns, const std::string &topic,
154 const std::vector<std::string> &parts);
155
164 void close();
165
167 size_t record_count() const { return _offsets.size(); }
168
169 bool crc_enabled() const { return _crc_enabled; }
170
171private:
172 std::ofstream _out;
173 std::filesystem::path _path;
174 bool _crc_enabled;
175 bool _closed = false;
176 std::vector<uint64_t> _offsets; // start of each record's record_len field
177 std::optional<int64_t> _first_ts, _last_ts;
178};
179
186public:
193 explicit BagReader(const std::filesystem::path &path);
194
196 size_t record_count() const { return _offsets.size(); }
197
199 std::optional<int64_t> first_timestamp() const { return _first_ts; }
200
202 std::optional<int64_t> last_timestamp() const { return _last_ts; }
203
210 bool truncated() const { return _truncated; }
211
214 bool has_index() const { return _has_index; }
215
217 bool crc_enabled() const { return _crc_enabled; }
218
225 BagRecord read(size_t index) const;
226
228 void rewind() { _cursor = 0; }
229
232 std::optional<BagRecord> next();
233
234private:
235 // Attempts to load the trailer+footer at the end of the file. Returns
236 // true (and populates _offsets/_first_ts/_last_ts) only if a complete,
237 // internally-consistent footer was found.
238 bool try_load_footer(uint64_t file_size);
239
240 // Recovers as many complete records as possible starting right after the
241 // header, stopping at the first incomplete/corrupt record. Sets
242 // _truncated accordingly.
243 void linear_scan(uint64_t file_size);
244
245 // Reads and decodes the record whose `record_len` field starts at
246 // `offset`. If `strict` is true, any problem (short file, bad internal
247 // length, CRC mismatch) throws BagError -- used by read()/next() on
248 // offsets already trusted (from the footer index or a completed linear
249 // scan), where corruption is a real error to surface. If `strict` is
250 // false, the same problems are reported by returning std::nullopt
251 // instead -- used by linear_scan() while probing the (possibly corrupt
252 // or truncated) tail record, so recovery of the valid prefix can stop
253 // cleanly instead of throwing out of the constructor.
254 std::optional<BagRecord> read_record_at(uint64_t offset, uint64_t file_size,
255 bool strict) const;
256
257 mutable std::ifstream _in;
258 std::filesystem::path _path;
259 uint32_t _format_ver = 0;
260 bool _crc_enabled = false;
261 bool _has_index = false;
262 bool _truncated = false;
263 std::vector<uint64_t> _offsets;
264 std::optional<int64_t> _first_ts, _last_ts;
265 size_t _cursor = 0;
266};
267
268} // namespace Mads
269
270#endif // MADS_BAG_HPP
Error thrown by BagWriter/BagReader for any I/O or format problem (bad magic, unsupported format_ver,...
Definition bag.hpp:103
BagError(const std::string &msg)
Definition bag.hpp:105
Reads a bag file written by BagWriter (or a compatible/truncated one), transparently using the traili...
Definition bag.hpp:185
BagRecord read(size_t index) const
Random access to record index (0-based).
std::optional< BagRecord > next()
BagReader(const std::filesystem::path &path)
Opens and parses path.
bool has_index() const
Definition bag.hpp:214
void rewind()
Resets sequential iteration (next()) to the first record.
Definition bag.hpp:228
bool truncated() const
True if the file was missing a valid footer/trailer and some trailing bytes could not be recovered as...
Definition bag.hpp:210
std::optional< int64_t > first_timestamp() const
Timestamp of the first record, if any.
Definition bag.hpp:199
bool crc_enabled() const
Whether per-record CRC32 is present (mirrors the header flag).
Definition bag.hpp:217
size_t record_count() const
Number of complete, valid records found (indexed or recovered).
Definition bag.hpp:196
std::optional< int64_t > last_timestamp() const
Timestamp of the last record, if any.
Definition bag.hpp:202
Sequentially appends BagRecord entries to a bag file.
Definition bag.hpp:126
void write(int64_t timestamp_ns, const std::string &topic, const std::vector< std::string > &parts)
Appends one record.
size_t record_count() const
Number of records written so far.
Definition bag.hpp:167
BagWriter(const std::filesystem::path &path, bool enable_crc=true)
Creates (truncating) path and writes the bag header.
BagWriter & operator=(const BagWriter &)=delete
bool crc_enabled() const
Definition bag.hpp:169
void close()
Finalizes the file: writes the index/footer/trailer and flushes.
BagWriter(const BagWriter &)=delete
Definition agent.hpp:67
constexpr uint32_t BAG_FORMAT_VERSION
Definition bag.hpp:92
std::vector< std::string > parts
Definition bag.hpp:113
std::string topic
Definition bag.hpp:112
int64_t timestamp_ns
Definition bag.hpp:111