NGen
Loading...
Searching...
No Matches
serialization_record.hpp
1/*
2Author: Nels Frazier
3Copyright (C) 2025 Lynker
4------------------------------------------------------------------------
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16------------------------------------------------------------------------
17Version 0.2
18Shared on-disk record format for ngen BMI state serialization.
19
20This header is the public API for producing and consuming
21serialization records. The on-disk layout is the responsibility of
22the internal `wire_format.hpp`, which lives under
23`src/utilities/bmi/` and is not exported. External code calls into
24this header's functions:
25
26 write_record(out, rec) — append a record
27 read_next_record(in, rec) — read the next record
28 read_record_length(in, body_out) — read prefix; report body length
29 read_record_metadata(in, prefix, id) — read prefix + id; skip payload
30
31The value type `SerializationRecord` itself lives in the engine-
32agnostic `ngen::serialization` library (see
33`include/utilities/serialization/record.hpp`) and is
34re-exported under `models::bmi::protocols` for source compatibility
35with existing call sites. Future storage backends share the same
36value type without depending on the BMI library.
37
38On-disk layout
39--------------
40A checkpoint file is a concatenation of records. Each record is:
41
42 [ fixed-size prefix ][ id_length bytes of id ][ payload_length bytes of payload ]
43
44The prefix layout is defined in `wire_format.hpp` and includes the
45magic bytes "NGSR" (which identify the file format), a wire-format
46version, simulation step / timestamp / wall-clock-epoch metadata,
47and the two body-length fields. Anyone with a hex editor can
48identify a checkpoint file by its "NGSR..." prefix.
49
50See `wire_format.hpp` for the rationale behind the chosen
51byte-level layout (cross-host portability, library independence,
52index-build performance).
53
54Truncation tolerance
55--------------------
56A torn final record (writer crashed mid-write, leaving fewer than
57the expected bytes on disk) is detected and treated as
58end-of-stream rather than as a corrupt file. The read functions
59return `false` on a short read at any field boundary — the prefix,
60the id bytes, or the payload bytes — which is what
61restart-from-crash workflows depend on.
62
63Uniqueness
64----------
65Each `(id, time_step)` pair is unique within a file. One entity may
66write multiple records at different time steps, but only one
67record per step.
68*/
69#pragma once
70
71#include "utilities/serialization/record.hpp"
72#include "wire_format.hpp"
73
74#include <nonstd/expected.hpp>
75
76#include <cstdint>
77#include <istream>
78#include <ostream>
79#include <string>
80#include <vector>
81
82namespace models {
83namespace bmi {
84namespace protocols {
85
86using nonstd::expected;
87using nonstd::make_unexpected;
88
89// Bring wire_format::Status into this namespace so call sites of
90// the read helpers below can write `Status::Ok` / `Status::Eof`
91// without the long `serialization::wire_format::` prefix.
93
94// Re-export the engine-agnostic value type and timestamp parser
95// from ngen::serialization so existing call sites continue to use
96// `models::bmi::protocols::SerializationRecord` / `parse_timestamp`
97// unchanged. `SerializationRecordView` is the non-owning sibling
98// (payload is `boost::span<const char>` rather than
99// `std::vector<char>`); use it on the write path when the caller
100// already holds the payload bytes in a buffer it owns for the
101// duration of the call.
104using ::ngen::serialization::parse_timestamp;
105
122#ifndef NGEN_BMI_MAX_RECORD_PAYLOAD_BYTES
123#define NGEN_BMI_MAX_RECORD_PAYLOAD_BYTES (1ULL << 30) // 1 GiB
124#endif
125constexpr uint64_t MAX_RECORD_PAYLOAD_BYTES = NGEN_BMI_MAX_RECORD_PAYLOAD_BYTES;
126
127// Internal helper shared by read_next_record, read_record_length,
128// and read_record_metadata. Reads the prefix and validates the
129// magic, the wire_version, and the payload_length cap. Returns
130// `Status::Ok` on a fully-read valid prefix, `Status::Eof` on a
131// clean short-read (EOF or torn prefix), or an error-arm string
132// on any validation failure of a fully-read prefix. Internal —
133// callers in this header always check the return value, so no
134// nodiscard.
135inline auto
137 -> expected<Status, std::string> {
138 if (const auto s = serialization::wire_format::read_record_prefix(in, prefix);
139 s == Status::Eof) {
140 return s; // EOF or torn prefix — clean stop, propagate
141 }
143 return make_unexpected(
144 "serialization_record: bad magic 0x" + std::to_string(prefix.magic)
145 + " — file is not a v0.2 record stream or is corrupted "
146 "at the record boundary"
147 );
148 }
149 if (prefix.wire_version != serialization::wire_format::RecordPrefix::VERSION) {
150 return make_unexpected(
151 "serialization_record: unsupported wire_version " + std::to_string(prefix.wire_version)
152 + " (this reader supports v"
154 );
155 }
156 if (prefix.payload_length > MAX_RECORD_PAYLOAD_BYTES) {
157 return make_unexpected(
158 "serialization_record: payload_length " + std::to_string(prefix.payload_length)
159 + " exceeds MAX_RECORD_PAYLOAD_BYTES sanity cap"
160 );
161 }
162 return Status::Ok;
163}
164
185nsel_NODISCARD inline auto write_record(std::ostream& out, const SerializationRecordView& rec)
186 -> expected<void, std::string> {
188 prefix.time_step = rec.time_step;
189 prefix.simulation_timestamp = rec.simulation_timestamp;
190 prefix.checkpoint_epoch = rec.checkpoint_epoch;
191 prefix.id_length = static_cast<uint16_t>(rec.id.size());
192 prefix.payload_length = static_cast<uint64_t>(rec.payload.size());
193 // (magic and wire_version come from the struct's default values.)
194
196 if (!rec.id.empty()) {
197 out.write(rec.id.data(), static_cast<std::streamsize>(rec.id.size()));
198 }
199 if (!rec.payload.empty()) {
200 out.write(rec.payload.data(), static_cast<std::streamsize>(rec.payload.size()));
201 }
202 if (!out) {
203 return make_unexpected(
204 std::string("serialization_record: stream failed during record write")
205 );
206 }
207 return {};
208}
209
221nsel_NODISCARD inline auto write_record(std::ostream& out, const SerializationRecord& rec)
222 -> expected<void, std::string> {
224 prefix.time_step = rec.time_step;
225 prefix.simulation_timestamp = rec.simulation_timestamp;
226 prefix.checkpoint_epoch = rec.checkpoint_epoch;
227 prefix.id_length = static_cast<uint16_t>(rec.id.size());
228 prefix.payload_length = static_cast<uint64_t>(rec.payload.size());
229
231 if (!rec.id.empty()) {
232 out.write(rec.id.data(), static_cast<std::streamsize>(rec.id.size()));
233 }
234 if (!rec.payload.empty()) {
235 out.write(rec.payload.data(), static_cast<std::streamsize>(rec.payload.size()));
236 }
237 if (!out) {
238 return make_unexpected(
239 std::string("serialization_record: stream failed during record write")
240 );
241 }
242 return {};
243}
244
256nsel_NODISCARD inline auto read_next_record(std::istream& in, SerializationRecord& rec)
257 -> expected<Status, std::string> {
259 auto p = read_validated_prefix(in, prefix);
260 // Propagate any non-Ok outcome unchanged — error arm or Eof.
261 if (!p || p.value() == Status::Eof) return p;
262
263 // id body
264 std::string id;
265 id.resize(prefix.id_length);
266 if (prefix.id_length > 0) {
267 in.read(&id[0], static_cast<std::streamsize>(prefix.id_length));
268 if (static_cast<uint64_t>(in.gcount()) != prefix.id_length) {
269 return Status::Eof; // torn id — clean stop
270 }
271 }
272
273 // payload body
274 std::vector<char> payload;
275 payload.resize(static_cast<size_t>(prefix.payload_length));
276 if (prefix.payload_length > 0) {
277 in.read(payload.data(), static_cast<std::streamsize>(prefix.payload_length));
278 if (static_cast<uint64_t>(in.gcount()) != prefix.payload_length) {
279 return Status::Eof; // torn payload — clean stop
280 }
281 }
282
283 rec.id = std::move(id);
284 rec.time_step = prefix.time_step;
285 rec.simulation_timestamp = prefix.simulation_timestamp;
286 rec.checkpoint_epoch = prefix.checkpoint_epoch;
287 rec.payload = std::move(payload);
288 return Status::Ok;
289}
290
308nsel_NODISCARD inline auto read_record_length(std::istream& in, uint64_t& body_out)
309 -> expected<Status, std::string> {
311 auto p = read_validated_prefix(in, prefix);
312 // Propagate any non-Ok outcome unchanged — error arm or Eof.
313 if (!p || p.value() == Status::Eof) return p;
314 body_out = static_cast<uint64_t>(prefix.id_length) + prefix.payload_length;
315 return Status::Ok;
316}
317
333nsel_NODISCARD inline auto read_record_metadata(
334 std::istream& in,
336 std::string& id_out
337) -> expected<Status, std::string> {
338 auto p = read_validated_prefix(in, prefix_out);
339 // Propagate any non-Ok outcome unchanged — error arm or Eof.
340 if (!p || p.value() == Status::Eof) return p;
341
342 id_out.resize(prefix_out.id_length);
343 if (prefix_out.id_length > 0) {
344 in.read(&id_out[0], static_cast<std::streamsize>(prefix_out.id_length));
345 if (static_cast<uint64_t>(in.gcount()) != prefix_out.id_length) {
346 return Status::Eof; // torn id — clean stop
347 }
348 }
349
350 if (prefix_out.payload_length > 0) {
351 in.seekg(static_cast<std::streamoff>(prefix_out.payload_length), std::ios::cur);
352 if (!in) return Status::Eof; // stream went bad while seeking
353 }
354 return Status::Ok;
355}
356
357} // namespace protocols
358} // namespace bmi
359} // namespace models
Status
Outcome of a wire-format read operation.
Definition wire_format.hpp:239
Status read_record_prefix(std::istream &in, RecordPrefix &p)
Read a RecordPrefix from in by reading each field through byte_io's stream adapters.
Definition wire_format.hpp:379
void write_record_prefix(std::ostream &out, const RecordPrefix &p)
Write a RecordPrefix to out by writing each field through byte_io's stream adapters.
Definition wire_format.hpp:359
Definition deserialization.hpp:40
constexpr uint64_t MAX_RECORD_PAYLOAD_BYTES
Definition serialization_record.hpp:125
nsel_NODISCARD auto read_record_length(std::istream &in, uint64_t &body_out) -> expected< Status, std::string >
Read the prefix at the current stream position and report the record's body length (id bytes + payloa...
Definition serialization_record.hpp:308
nsel_NODISCARD auto read_record_metadata(std::istream &in, serialization::wire_format::RecordPrefix &prefix_out, std::string &id_out) -> expected< Status, std::string >
Read the prefix and id at the current stream position and skip past the payload, leaving in positione...
Definition serialization_record.hpp:333
nsel_NODISCARD auto write_record(std::ostream &out, const SerializationRecordView &rec) -> expected< void, std::string >
Append a record to out using the v0.2 wire format (fixed-size prefix + id bytes + payload bytes).
Definition serialization_record.hpp:185
nsel_NODISCARD auto read_next_record(std::istream &in, SerializationRecord &rec) -> expected< Status, std::string >
Read the next record from in.
Definition serialization_record.hpp:256
auto read_validated_prefix(std::istream &in, serialization::wire_format::RecordPrefix &prefix) -> expected< Status, std::string >
Definition serialization_record.hpp:136
Definition AbstractCLibBmiAdapter.hpp:6
detail::RecordT< std::vector< char > > Record
A checkpoint record that owns its payload.
Definition record.hpp:182
detail::RecordT< boost::span< const char > > RecordView
A checkpoint record that views its payload (non-owning).
Definition record.hpp:192
Fixed-prefix wire header for a serialization record.
Definition wire_format.hpp:249
static constexpr uint32_t MAGIC
'NGSR' written byte-first.
Definition wire_format.hpp:257
int64_t simulation_timestamp
Definition wire_format.hpp:294
static constexpr uint8_t VERSION
The wire-format version this code writes.
Definition wire_format.hpp:273
Definition record.hpp:151