feat(core): Dest type replaces to + lifetime_ms
This commit is contained in:
+18
-11
@@ -145,7 +145,7 @@ mod crc32c {
|
||||
/// Magic header: the first 8 bytes of every v1 log.
|
||||
const MAGIC: [u8; 8] = *b"TINESLOG";
|
||||
/// The `format_version` field of the header. Bump for an incompatible encoding.
|
||||
const FORMAT_VERSION: u16 = 1;
|
||||
const FORMAT_VERSION: u16 = 2;
|
||||
/// 4-byte record sync marker (`b"TING"`), lets a torn/corrupt record be
|
||||
/// located and skipped by scanning forward. Made `pub` so hosts/tests can emit
|
||||
/// (and corrupt) framing directly.
|
||||
@@ -502,7 +502,7 @@ impl LogStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the v1 header: `magic ‖ format_version ‖ reserved`.
|
||||
/// Write the log header: `magic ‖ format_version ‖ reserved`.
|
||||
fn write_header(file: &mut File) -> Result<(), StoreError> {
|
||||
file.write_all(&MAGIC).map_err(|_| StoreError::Backend)?;
|
||||
file.write_all(&FORMAT_VERSION.to_le_bytes())
|
||||
@@ -516,9 +516,10 @@ fn write_header(file: &mut File) -> Result<(), StoreError> {
|
||||
/// naming the fix (delete the log) in the message.
|
||||
fn reject_bad_header(path: &Path, reason: &str) -> Result<(), StoreError> {
|
||||
eprintln!(
|
||||
"tines-store: log header invalid ({reason}): {} is not a v1 \
|
||||
"tines-store: log header invalid ({reason}): {} is not a v{} \
|
||||
'TINESLOG' log — delete it and start fresh (fleet-fixes F5).",
|
||||
path.display()
|
||||
path.display(),
|
||||
FORMAT_VERSION,
|
||||
);
|
||||
Err(StoreError::Backend)
|
||||
}
|
||||
@@ -533,7 +534,14 @@ fn validate_header(data: &[u8], path: &Path) -> Result<(), StoreError> {
|
||||
}
|
||||
let version = u16::from_le_bytes([data[8], data[9]]);
|
||||
if version != FORMAT_VERSION {
|
||||
return reject_bad_header(path, &format!("version {version} != {FORMAT_VERSION}"));
|
||||
return reject_bad_header(
|
||||
path,
|
||||
&format!(
|
||||
"on-disk format_version {version} != {FORMAT_VERSION}: the \
|
||||
Observation encoding changed in 1.0, so this log can no longer \
|
||||
be decoded"
|
||||
),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -735,22 +743,22 @@ impl Store for LogStore {
|
||||
|
||||
fn for_each_broadcast_id(&self, f: &mut dyn FnMut(&ObservationId)) {
|
||||
for (id, entry) in self.index.iter() {
|
||||
if entry.obs.to.is_none() {
|
||||
if entry.obs.dest.is_none() {
|
||||
f(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Visit the ids of observations addressed to someone (`to == Some`),
|
||||
/// Visit the ids of observations addressed to someone (`dest` is `Some`),
|
||||
/// excluding those already released.
|
||||
///
|
||||
/// This is stricter than the [`Store`] trait's documented contract
|
||||
/// (`addressed == to == Some`): a released addressed observation has
|
||||
/// (`addressed` == `dest` is `Some`): a released addressed observation has
|
||||
/// already been sent to its recipient, so it is intentionally skipped and
|
||||
/// not offered for exchange again until (and if) it is re-inserted.
|
||||
fn for_each_addressed_id(&self, f: &mut dyn FnMut(&ObservationId)) {
|
||||
for (id, entry) in self.index.iter() {
|
||||
if entry.obs.to.is_some() && !entry.released {
|
||||
if entry.obs.dest.is_some() && !entry.released {
|
||||
f(id);
|
||||
}
|
||||
}
|
||||
@@ -1015,8 +1023,7 @@ mod tests {
|
||||
observed_at: at,
|
||||
topic: tines_core::Topic::from_slice(b"door/3").unwrap(),
|
||||
payload: tines_core::Payload::from_slice(b"open").unwrap(),
|
||||
to: None,
|
||||
lifetime_ms: None,
|
||||
dest: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ fn obs(observer: [u8; 16], seq: u64, age: u64, at: Option<u64>) -> Observation {
|
||||
observed_at: at,
|
||||
topic: Topic::from_slice(b"door/3").unwrap(),
|
||||
payload: Payload::from_slice(b"open").unwrap(),
|
||||
to: None,
|
||||
lifetime_ms: None,
|
||||
dest: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +41,28 @@ fn store_path(tag: &str) -> PathBuf {
|
||||
p
|
||||
}
|
||||
|
||||
/// A log whose header declares an older `format_version` must be refused on
|
||||
/// open. The 1.0 work changed the Observation encoding (the `dest` field
|
||||
/// replaces the old `to`/`lifetime_ms` pair), so a v1 log's records can no
|
||||
/// longer be decoded: `open` fails and names the fix (delete the log).
|
||||
#[test]
|
||||
fn old_format_is_refused() {
|
||||
let path = store_path("oldfmt");
|
||||
{
|
||||
let mut f = std::fs::File::create(&path).unwrap();
|
||||
// magic ‖ format_version (v1, little-endian) ‖ reserved: a genuine v1
|
||||
// header that this build (v2) no longer accepts.
|
||||
f.write_all(b"TINESLOG").unwrap();
|
||||
f.write_all(&1u16.to_le_bytes()).unwrap();
|
||||
f.write_all(&0u16.to_le_bytes()).unwrap();
|
||||
f.flush().unwrap();
|
||||
}
|
||||
assert!(
|
||||
LogStore::open(&path, 0).is_err(),
|
||||
"open must refuse a log whose format_version is older than this build"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reopen_preserves_store() {
|
||||
let path = store_path("reopen");
|
||||
@@ -324,8 +345,7 @@ mod fleet_fixes_f5 {
|
||||
observed_at: Some(10),
|
||||
topic: Topic::from_slice(b"door/3").unwrap(),
|
||||
payload: Payload::from_slice(b"open").unwrap(),
|
||||
to: None,
|
||||
lifetime_ms: None,
|
||||
dest: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +353,7 @@ mod fleet_fixes_f5 {
|
||||
/// mirroring on-disk corruption of a complete record.
|
||||
fn corrupt_record(path: &Path, n: usize) {
|
||||
let mut data = std::fs::read(path).expect("read log");
|
||||
let mut pos = 12usize; // past the v1 header
|
||||
let mut pos = 12usize; // past the header
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
assert!(pos + 12 <= data.len(), "record {i} out of range");
|
||||
|
||||
Reference in New Issue
Block a user