use crate::communication_replay::{CommunicationConsumptionArtifact, CommunicationReplayMode};
use crate::determinism::EffectDeterminismTier;
use crate::effect::{CorruptionType, EffectTraceEntry};
use crate::trace::normalize_trace;
use crate::verification::Hash;
use crate::vm::ObsEvent;
use serde::{Deserialize, Serialize};
pub const SERIALIZATION_SCHEMA_VERSION: &str = "vm.serialization.v1";
fn default_serialization_schema_version() -> String {
SERIALIZATION_SCHEMA_VERSION.to_string()
}
fn normalize_serialization_schema_version(raw: &str) -> String {
if raw == "1" {
SERIALIZATION_SCHEMA_VERSION.to_string()
} else {
raw.to_string()
}
}
fn deserialize_serialization_schema_version<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum SchemaVersionValue {
String(String),
Integer(u64),
}
let parsed = SchemaVersionValue::deserialize(deserializer)?;
Ok(match parsed {
SchemaVersionValue::String(version) => normalize_serialization_schema_version(&version),
SchemaVersionValue::Integer(version) => {
normalize_serialization_schema_version(&version.to_string())
}
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CanonicalTraceV1 {
#[serde(
default = "default_serialization_schema_version",
deserialize_with = "deserialize_serialization_schema_version"
)]
pub schema_version: String,
pub events: Vec<ObsEvent>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CanonicalReplayFragmentV1 {
#[serde(
default = "default_serialization_schema_version",
deserialize_with = "deserialize_serialization_schema_version"
)]
pub schema_version: String,
pub obs_trace: Vec<ObsEvent>,
pub effect_trace: Vec<EffectTraceEntry>,
pub crashed_sites: Vec<String>,
pub partitioned_edges: Vec<(String, String)>,
pub corrupted_edges: Vec<((String, String), CorruptionType)>,
pub timed_out_sites: Vec<(String, u64)>,
#[serde(default)]
pub effect_determinism_tier: EffectDeterminismTier,
#[serde(default)]
pub communication_replay_mode: CommunicationReplayMode,
#[serde(default)]
pub communication_replay_root: Option<Hash>,
#[serde(default)]
pub communication_consumption_artifacts: Vec<CommunicationConsumptionArtifact>,
}
#[must_use]
pub fn canonical_trace_v1(trace: &[ObsEvent]) -> CanonicalTraceV1 {
CanonicalTraceV1 {
schema_version: default_serialization_schema_version(),
events: normalize_trace(trace),
}
}
#[must_use]
pub fn canonical_effect_trace(trace: &[EffectTraceEntry]) -> Vec<EffectTraceEntry> {
let mut out = trace.to_vec();
out.sort_by(|lhs, rhs| {
(lhs.ordering_key, lhs.effect_id, &lhs.effect_kind).cmp(&(
rhs.ordering_key,
rhs.effect_id,
&rhs.effect_kind,
))
});
out
}
#[must_use]
pub fn canonical_replay_fragment_v1(
obs_trace: &[ObsEvent],
effect_trace: &[EffectTraceEntry],
mut crashed_sites: Vec<String>,
mut partitioned_edges: Vec<(String, String)>,
mut corrupted_edges: Vec<((String, String), CorruptionType)>,
mut timed_out_sites: Vec<(String, u64)>,
effect_determinism_tier: EffectDeterminismTier,
communication_replay_mode: CommunicationReplayMode,
communication_replay_root: Option<Hash>,
communication_consumption_artifacts: Vec<CommunicationConsumptionArtifact>,
) -> CanonicalReplayFragmentV1 {
crashed_sites.sort_unstable();
crashed_sites.dedup();
partitioned_edges.sort_unstable();
partitioned_edges.dedup();
corrupted_edges.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0));
corrupted_edges.dedup();
timed_out_sites.sort_unstable();
CanonicalReplayFragmentV1 {
schema_version: default_serialization_schema_version(),
obs_trace: canonical_trace_v1(obs_trace).events,
effect_trace: canonical_effect_trace(effect_trace),
crashed_sites,
partitioned_edges,
corrupted_edges,
timed_out_sites,
effect_determinism_tier,
communication_replay_mode,
communication_replay_root,
communication_consumption_artifacts,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::Edge;
#[test]
fn canonical_effect_trace_is_stably_sorted() {
let trace = vec![
EffectTraceEntry {
effect_id: 2,
effect_kind: "b".to_string(),
inputs: serde_json::json!({}),
outputs: serde_json::json!({}),
handler_identity: "h".to_string(),
ordering_key: 3,
topology: None,
},
EffectTraceEntry {
effect_id: 1,
effect_kind: "a".to_string(),
inputs: serde_json::json!({}),
outputs: serde_json::json!({}),
handler_identity: "h".to_string(),
ordering_key: 2,
topology: None,
},
];
let sorted = canonical_effect_trace(&trace);
assert_eq!(sorted[0].effect_id, 1);
assert_eq!(sorted[1].effect_id, 2);
}
#[test]
fn canonical_trace_payload_has_version() {
let trace = vec![ObsEvent::Sent {
tick: 1,
edge: Edge::new(1, "A", "B"),
session: 1,
from: "A".to_string(),
to: "B".to_string(),
label: "m".to_string(),
}];
let payload = canonical_trace_v1(&trace);
assert_eq!(payload.schema_version, SERIALIZATION_SCHEMA_VERSION);
assert_eq!(payload.events.len(), 1);
}
#[test]
fn legacy_numeric_schema_version_deserializes_to_string_identifier() {
let payload = serde_json::json!({
"schema_version": 1,
"events": []
});
let decoded: CanonicalTraceV1 =
serde_json::from_value(payload).expect("legacy schema version should deserialize");
assert_eq!(decoded.schema_version, SERIALIZATION_SCHEMA_VERSION);
}
}