1use std::collections::BTreeMap;
12use std::fmt;
13
14use chrono::DateTime;
15use chrono::Utc;
16use detcore_model::host_input::HostInputSet;
17use serde::Deserialize;
18use serde::Serialize;
19use serde::de::DeserializeSeed;
20use serde::de::MapAccess;
21use serde::de::SeqAccess;
22use serde::de::Visitor;
23use serde::de::value::MapAccessDeserializer;
24use serde::de::value::SeqAccessDeserializer;
25use serde::de::value::StringDeserializer;
26use serde::ser::SerializeMap;
27
28use super::HostObservedCount;
29use super::PreemptionRecord;
30use super::ReservedInputMetadata;
31use super::ThreadHistory;
32use super::TimeSkip;
33use super::callback_history;
34use crate::types::DetTid;
35use crate::types::SchedEvent;
36
37const ENVELOPE: &str = "incomplete_preemption_record";
38const DEPARTURE: &str = "replay_departed_at";
39const VERSION: u32 = 1;
40
41#[derive(Serialize, Deserialize)]
44#[serde(remote = "PreemptionRecord")]
45struct RawRecord {
46 per_thread: BTreeMap<DetTid, ThreadHistory>,
47 global: Vec<SchedEvent>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 epoch: Option<DateTime<Utc>>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pipe_wake_latency: Option<u64>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 signal_state_copy: Option<bool>,
54 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
55 cap_preemption_charge: bool,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 chaos_timeslice: Option<u64>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 guest_ids: Option<detcore_model::pid::GuestIds>,
60 #[serde(default, skip_serializing_if = "ReservedInputMetadata::is_missing")]
61 reserved_input: ReservedInputMetadata,
62 #[serde(skip)]
63 replay_departed_at: Option<u64>,
64 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
65 callback_checkpoints: BTreeMap<DetTid, Vec<callback_history::Checkpoint>>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 rng_seed: Option<u64>,
68 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
69 expired_wait_runs_charged_once: bool,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 host_inputs: Option<HostInputSet>,
72 #[serde(default, skip_serializing_if = "HostObservedCount::is_zero")]
73 store_exclusive_failures: HostObservedCount,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 format_version: Option<u32>,
76 #[serde(default, skip_serializing_if = "super::lists_no_time_skips")]
77 time_skips: Option<Vec<TimeSkip>>,
78}
79
80struct LegacyFields<'a>(&'a PreemptionRecord);
81
82impl Serialize for LegacyFields<'_> {
83 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
84 RawRecord::serialize(self.0, serializer)
85 }
86}
87
88#[derive(Serialize)]
89struct MarkedRef<'a> {
90 version: u32,
91 replay_departed_at: u64,
92 record: LegacyFields<'a>,
93}
94
95#[derive(Deserialize)]
96#[serde(deny_unknown_fields)]
97struct Marked {
98 version: u32,
99 replay_departed_at: u64,
100 #[serde(deserialize_with = "deserialize_payload")]
101 record: PreemptionRecord,
102}
103
104impl Serialize for PreemptionRecord {
105 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
106 let Some(replay_departed_at) = self.replay_departed_at else {
107 return RawRecord::serialize(self, serializer);
108 };
109 if !serializer.is_human_readable() {
112 return Err(serde::ser::Error::custom(
113 "marked preemption records require the incomplete_preemption_record JSON envelope",
114 ));
115 }
116 let mut map = serializer.serialize_map(Some(1))?;
117 map.serialize_entry(
118 ENVELOPE,
119 &MarkedRef {
120 version: VERSION,
121 replay_departed_at,
122 record: LegacyFields(self),
123 },
124 )?;
125 map.end()
126 }
127}
128
129impl<'de> Deserialize<'de> for PreemptionRecord {
130 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
131 if !deserializer.is_human_readable() {
132 return RawRecord::deserialize(deserializer);
135 }
136 deserializer.deserialize_any(RecordVisitor)
137 }
138}
139
140struct RecordVisitor;
141
142impl<'de> Visitor<'de> for RecordVisitor {
143 type Value = PreemptionRecord;
144
145 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 formatter.write_str("a legacy preemption record or an incomplete-preemption envelope")
147 }
148
149 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
150 let first = map.next_key::<String>()?;
151 if first.as_deref() == Some(ENVELOPE) {
152 let marked: Marked = map.next_value()?;
153 if map.next_key::<serde::de::IgnoredAny>()?.is_some() {
154 return Err(serde::de::Error::custom(
155 "an incomplete-preemption envelope cannot contain other top-level fields",
156 ));
157 }
158 if marked.version != VERSION {
159 return Err(serde::de::Error::custom(format!(
160 "unsupported incomplete_preemption_record version {}",
161 marked.version
162 )));
163 }
164 let mut record = marked.record;
165 record.replay_departed_at = Some(marked.replay_departed_at);
166 Ok(record)
167 } else {
168 legacy_map(map, first, true)
169 }
170 }
171
172 fn visit_seq<A: SeqAccess<'de>>(self, sequence: A) -> Result<Self::Value, A::Error> {
173 RawRecord::deserialize(SeqAccessDeserializer::new(sequence))
175 }
176}
177
178fn deserialize_payload<'de, D: serde::Deserializer<'de>>(
181 deserializer: D,
182) -> Result<PreemptionRecord, D::Error> {
183 struct Payload;
184 impl<'de> Visitor<'de> for Payload {
185 type Value = PreemptionRecord;
186 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
187 formatter.write_str("unmarked legacy fields inside the envelope")
188 }
189 fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
190 legacy_map(map, None, false)
191 }
192 }
193 deserializer.deserialize_map(Payload)
194}
195
196fn legacy_map<'de, A: MapAccess<'de>>(
197 map: A,
198 first: Option<String>,
199 allow_flat_marker: bool,
200) -> Result<PreemptionRecord, A::Error> {
201 let mut departure = None;
202 let mut seen = false;
203 let mut record = RawRecord::deserialize(MapAccessDeserializer::new(LegacyMap {
204 map,
205 first,
206 allow_flat_marker,
207 departure: &mut departure,
208 seen: &mut seen,
209 }))?;
210 record.replay_departed_at = departure;
211 Ok(record)
212}
213
214struct LegacyMap<'a, A> {
215 map: A,
216 first: Option<String>,
217 allow_flat_marker: bool,
218 departure: &'a mut Option<u64>,
219 seen: &'a mut bool,
220}
221
222impl<'de, A: MapAccess<'de>> MapAccess<'de> for LegacyMap<'_, A> {
223 type Error = A::Error;
224
225 fn next_key_seed<K: DeserializeSeed<'de>>(
226 &mut self,
227 seed: K,
228 ) -> Result<Option<K::Value>, Self::Error> {
229 loop {
230 let key = match self.first.take() {
231 Some(key) => Some(key),
232 None => self.map.next_key::<String>()?,
233 };
234 let Some(key) = key else { return Ok(None) };
235 if key == ENVELOPE {
236 return Err(serde::de::Error::custom(
237 "an incomplete-preemption envelope cannot be mixed with legacy fields",
238 ));
239 }
240 if key != DEPARTURE {
241 return seed.deserialize(StringDeserializer::new(key)).map(Some);
242 }
243 if !self.allow_flat_marker {
244 return Err(serde::de::Error::custom(
245 "an envelope payload cannot contain replay_departed_at",
246 ));
247 }
248 if *self.seen {
249 return Err(serde::de::Error::duplicate_field("replay_departed_at"));
250 }
251 *self.seen = true;
252 *self.departure = self.map.next_value()?;
256 }
257 }
258
259 fn next_value_seed<V: DeserializeSeed<'de>>(
260 &mut self,
261 seed: V,
262 ) -> Result<V::Value, Self::Error> {
263 self.map.next_value_seed(seed)
264 }
265}
266
267pub(super) fn settings_value(record: &PreemptionRecord) -> serde_json::Result<serde_json::Value> {
270 let mut value = serde_json::to_value(LegacyFields(record))?;
271 if let Some(event) = record.replay_departed_at {
272 value[DEPARTURE] = event.into();
273 }
274 Ok(value)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::preemptions::PreemptionWriter;
281 use crate::preemptions::boundary_recording::BoundaryRecording;
282 use crate::preemptions::boundary_recording::Recording;
283 use crate::preemptions::read_recorded_replay_departure;
284
285 #[test]
286 fn marked_envelopes_never_fall_back_to_legacy_fields() {
287 let raw = r#"{"per_thread":{},"global":[]}"#;
288 let valid = format!(r#"{{"version":1,"replay_departed_at":7,"record":{raw}}}"#);
289 let envelope = |body: &str| format!(r#"{{"incomplete_preemption_record":{body}}}"#);
290 let marked = envelope(&valid);
291 assert_eq!(
292 serde_json::from_str::<PreemptionRecord>(&marked)
293 .unwrap()
294 .replay_departed_at(),
295 Some(7)
296 );
297 let bad = [
298 envelope("null"),
299 envelope(&valid.replace("\"version\":1", "\"version\":2")),
300 envelope(&valid.replace("\"version\":1,", "")),
301 envelope(&valid.replace("\"replay_departed_at\":7,", "")),
302 envelope(&valid.replace("\"replay_departed_at\":7", "\"replay_departed_at\":null")),
303 envelope(&valid.replace("\"version\":1", "\"version\":1,\"version\":1")),
304 envelope(&valid.replace(
305 "\"replay_departed_at\":7",
306 "\"replay_departed_at\":7,\"replay_departed_at\":9",
307 )),
308 format!(
309 r#"{{"incomplete_preemption_record":{valid},"incomplete_preemption_record":{valid}}}"#
310 ),
311 format!(r#"{{"incomplete_preemption_record":{valid},"per_thread":{{}},"global":[]}}"#),
312 format!(r#"{{"per_thread":{{}},"global":[],"incomplete_preemption_record":{valid}}}"#),
313 r#"{"per_thread":{},"global":[],"incomplete_preemption_record":null}"#.to_owned(),
314 envelope(&valid.replace(raw, &marked)),
315 envelope(&valid.replace(
316 raw,
317 r#"{"per_thread":{},"global":[],"replay_departed_at":7}"#,
318 )),
319 envelope(&valid.replace(
320 raw,
321 r#"{"per_thread":{},"global":[],"replay_departed_at":null}"#,
322 )),
323 r#"{"per_thread":{},"global":[],"replay_departed_at":7,"replay_departed_at":9}"#
324 .to_owned(),
325 ];
326 let dir = tempfile::tempdir().unwrap();
327 let path = dir.path().join("bad.json");
328 for input in bad {
329 assert!(
330 serde_json::from_str::<PreemptionRecord>(&input).is_err(),
331 "{input}"
332 );
333 assert!(Recording::decode(input.as_bytes()).is_err(), "{input}");
334 if input.contains(ENVELOPE) {
337 std::fs::write(&path, &input).unwrap();
338 assert!(
339 crate::preemptions::read_recorded_epoch(&path).is_err(),
340 "{input}"
341 );
342 assert!(read_recorded_replay_departure(&path).is_err(), "{input}");
343 }
344 }
345 }
346
347 #[test]
348 fn historical_flat_departures_are_retained_without_restricting_ordinary_unknown_fields() {
349 let ordinary = r#"{"per_thread":{},"global":[],"future_setting":{"x":1}}"#;
350 let ordinary: PreemptionRecord = serde_json::from_str(ordinary).unwrap();
351 ordinary.require_faithful_replay().unwrap();
352 assert_eq!(
353 serde_json::to_string(&ordinary).unwrap(),
354 r#"{"per_thread":{},"global":[]}"#
355 );
356 for input in [
357 r#"{"replay_departed_at":0,"per_thread":{},"global":[]}"#,
358 r#"{"per_thread":{},"global":[],"replay_departed_at":0}"#,
359 ] {
360 let record: PreemptionRecord = serde_json::from_str(input).unwrap();
361 assert_eq!(record.replay_departed_at(), Some(0));
362 assert!(record.require_faithful_replay().is_err());
363 let rewritten = serde_json::to_string(&record).unwrap();
364 assert_eq!(
365 rewritten,
366 r#"{"incomplete_preemption_record":{"version":1,"replay_departed_at":0,"record":{"per_thread":{},"global":[]}}}"#
367 );
368 assert_eq!(
369 serde_json::from_str::<PreemptionRecord>(&rewritten).unwrap(),
370 record
371 );
372 }
373 }
374
375 #[test]
376 fn wrapped_settings_and_file_transforms_retain_the_authoritative_departure() {
377 let directory = tempfile::tempdir().unwrap();
378 for marker in [None, Some(7)] {
379 let mut writer = PreemptionWriter::new(None)
380 .with_epoch("2001-02-03T04:05:06Z".parse().unwrap())
381 .with_pipe_wake_latency(Some(0))
382 .with_signal_state_copy(false)
383 .with_rng_seed(31)
384 .with_expired_wait_runs_charged_once(true)
385 .with_cap_preemption_charge(true)
386 .with_chaos_timeslice(std::num::NonZeroU64::new(1000));
387 if let Some(event) = marker {
388 writer.mark_replay_departed(event);
389 }
390 let record: PreemptionRecord = serde_json::from_str(&writer.into_string()).unwrap();
391 for boundaries in [false, true] {
392 let recording = if boundaries {
393 Recording::Boundaries(
394 BoundaryRecording::new(
395 record.clone(),
396 detcore_model::schedule::boundary::Schedule::new(vec![]).unwrap(),
397 )
398 .unwrap(),
399 )
400 } else {
401 Recording::Legacy(record.clone())
402 };
403 let path = directory
404 .path()
405 .join(format!("{boundaries}-{marker:?}.json"));
406 recording.write(&path).unwrap();
407 assert_eq!(read_recorded_replay_departure(&path).unwrap(), marker);
408 assert_eq!(
409 crate::preemptions::read_recorded_epoch(&path).unwrap(),
410 record.epoch()
411 );
412 assert_eq!(
413 crate::preemptions::read_recorded_pipe_wake_latency(&path).unwrap(),
414 Some(0)
415 );
416 assert_eq!(
417 crate::preemptions::read_recorded_signal_state_copy(&path).unwrap(),
418 Some(false)
419 );
420 assert!(
421 crate::preemptions::read_recorded_expired_wait_runs_charged_once(&path)
422 .unwrap()
423 );
424 assert!(crate::preemptions::read_recorded_cap_preemption_charge(&path).unwrap());
425 assert_eq!(
426 crate::preemptions::read_recorded_chaos_timeslice(&path).unwrap(),
427 Some(1000)
428 );
429 let stripped =
430 crate::preemptions::strip_times_from_events_file(&path, None).unwrap();
431 assert_eq!(Recording::read(&stripped).unwrap(), recording);
432 assert_eq!(read_recorded_replay_departure(&stripped).unwrap(), marker);
433 assert_eq!(
434 Recording::read(&path)
435 .unwrap()
436 .require_faithful_replay()
437 .is_err(),
438 marker.is_some()
439 );
440 }
441 for copy in [
442 record.clone(),
443 record.clone_preemptions_only(),
444 record.schedule_prefix(0),
445 record.normalize(),
446 record.with_schedule(vec![]).unwrap(),
447 ] {
448 assert_eq!(copy.replay_departed_at(), marker);
449 assert_eq!(
450 copy.retain_replay_departure(None).replay_departed_at(),
451 marker
452 );
453 }
454 }
455 }
456}