1use std::collections::{HashMap, HashSet};
50use std::fs::{self, File, OpenOptions};
51use std::io::Write;
52use std::num::NonZeroU64;
53use std::path::{Path, PathBuf};
54use std::sync::{Arc, Mutex, MutexGuard};
55
56use fs2::FileExt;
57use serde::{Deserialize, Serialize};
58use serde_json::{Map, Value};
59
60use crate::configuration::TaskParameters;
61use crate::system_state::{SystemState, SystemStateSchema};
62
63mod error;
64mod json_payload_decoder;
65mod json_state_record_encoder;
66mod jsonl_format;
67mod queued_state_writer;
68mod stored_state_series_reader;
69
70pub use error::StorageError;
71pub use json_payload_decoder::{
72 JsonPayloadDecoder, JsonPayloadDecoderRegistry, JsonStringDecoder, JsonVecF64Decoder,
73};
74pub use stored_state_series_reader::StoredStateSeriesReader;
75
76use json_state_record_encoder::JsonStateRecordEncoder;
77use jsonl_format::{
78 RecordingMetadata, RecordingStatus, StateFieldMetadata, StateStreamMetadata,
79 TimeAxisMetadata as StoredTimeAxis,
80};
81use queued_state_writer::{RecoveredStateStream, StateStreamStorageConfig, StateWriterWorker};
82
83const METADATA_FILE: &str = "metadata.json";
85
86const METADATA_TEMP_FILE: &str = ".metadata.json.tmp";
88
89#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct TimeAxisMetadata {
97 iteration_name: String,
98 iteration_unit: Option<String>,
99 physical_time_name: Option<String>,
100 physical_time_unit: Option<String>,
101}
102
103impl TimeAxisMetadata {
104 pub fn new(iteration_name: impl Into<String>) -> Self {
110 Self {
111 iteration_name: iteration_name.into(),
112 iteration_unit: None,
113 physical_time_name: None,
114 physical_time_unit: None,
115 }
116 }
117
118 #[must_use]
120 pub fn with_iteration_unit(mut self, unit: impl Into<String>) -> Self {
121 self.iteration_unit = Some(unit.into());
122 self
123 }
124
125 #[must_use]
127 pub fn with_physical_time_name(mut self, name: impl Into<String>) -> Self {
128 self.physical_time_name = Some(name.into());
129 self
130 }
131
132 #[must_use]
137 pub fn with_physical_time_unit(mut self, unit: impl Into<String>) -> Self {
138 self.physical_time_unit = Some(unit.into());
139 self
140 }
141
142 #[must_use]
144 pub fn with_physical_axis(mut self, name: impl Into<String>, unit: impl Into<String>) -> Self {
145 self.physical_time_name = Some(name.into());
146 self.physical_time_unit = Some(unit.into());
147 self
148 }
149
150 fn into_stored(self) -> StoredTimeAxis {
152 StoredTimeAxis {
153 iteration_name: self.iteration_name,
154 iteration_unit: self.iteration_unit,
155 physical_time_name: self.physical_time_name,
156 physical_time_unit: self.physical_time_unit,
157 }
158 }
159}
160
161impl Default for TimeAxisMetadata {
162 fn default() -> Self {
165 Self::new("iteration")
166 }
167}
168
169#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
176#[serde(rename_all = "snake_case")]
177pub enum SamplingInterval {
178 Iterations(NonZeroU64),
180}
181
182impl SamplingInterval {
183 pub const fn iterations(interval: u64) -> Option<Self> {
185 match NonZeroU64::new(interval) {
186 Some(interval) => Some(Self::Iterations(interval)),
187 None => None,
188 }
189 }
190
191 const fn includes(self, iteration: u64) -> bool {
193 match self {
194 Self::Iterations(interval) => iteration % interval.get() == 0,
195 }
196 }
197}
198
199#[derive(Clone, Debug, Eq, PartialEq)]
208pub struct StateStreamConfig {
209 name: String,
210 directory: String,
211 sampling_interval: SamplingInterval,
212 fields: Vec<String>,
213 storage_limits: Option<(NonZeroU64, NonZeroU64)>,
214}
215
216impl StateStreamConfig {
217 pub fn new<I, K>(
225 name: impl Into<String>,
226 fields: I,
227 sampling_interval: SamplingInterval,
228 max_chunk_bytes: NonZeroU64,
229 queue_bytes: NonZeroU64,
230 ) -> Self
231 where
232 I: IntoIterator<Item = K>,
233 K: Into<String>,
234 {
235 let name = name.into();
236 Self {
237 directory: name.clone(),
238 name,
239 sampling_interval,
240 fields: fields.into_iter().map(Into::into).collect(),
241 storage_limits: Some((max_chunk_bytes, queue_bytes)),
242 }
243 }
244
245 fn sampled<I, K>(
247 name: impl Into<String>,
248 fields: I,
249 sampling_interval: SamplingInterval,
250 ) -> Self
251 where
252 I: IntoIterator<Item = K>,
253 K: Into<String>,
254 {
255 let name = name.into();
256 Self {
257 directory: name.clone(),
258 name,
259 sampling_interval,
260 fields: fields.into_iter().map(Into::into).collect(),
261 storage_limits: None,
262 }
263 }
264
265 #[must_use]
270 pub fn with_relative_directory(mut self, directory: impl Into<String>) -> Self {
271 self.directory = directory.into();
272 self
273 }
274}
275
276#[derive(Debug)]
283pub struct SystemStateWriterBuilder {
284 root: PathBuf,
285 spec: SystemStateSchema,
286 time: TimeAxisMetadata,
287 user_metadata: Map<String, Value>,
288 shared_stream_limits: Option<(NonZeroU64, NonZeroU64)>,
289 streams: Vec<StateStreamConfig>,
290}
291
292impl SystemStateWriterBuilder {
293 pub fn new(root: impl Into<PathBuf>, spec: &SystemStateSchema) -> Self {
298 Self {
299 root: root.into(),
300 spec: spec.clone(),
301 time: TimeAxisMetadata::default(),
302 user_metadata: Map::new(),
303 shared_stream_limits: None,
304 streams: Vec::new(),
305 }
306 }
307
308 #[must_use]
310 pub fn with_time_axis_metadata(mut self, time: TimeAxisMetadata) -> Self {
311 self.time = time;
312 self
313 }
314
315 #[must_use]
321 pub fn with_user_metadata(mut self, metadata: Map<String, Value>) -> Self {
322 self.user_metadata = metadata;
323 self
324 }
325
326 #[must_use]
333 pub fn with_shared_stream_limits(
334 mut self,
335 max_chunk_bytes: NonZeroU64,
336 queue_bytes: NonZeroU64,
337 ) -> Self {
338 self.shared_stream_limits = Some((max_chunk_bytes, queue_bytes));
339 self
340 }
341
342 #[must_use]
348 pub fn with_task_parameters(mut self, parameters: &TaskParameters) -> Self {
349 self.user_metadata = parameters
350 .iter()
351 .map(|(key, value)| (key.to_owned(), value.clone()))
352 .collect();
353 self.user_metadata.insert(
354 "task_index".to_owned(),
355 Value::from(parameters.task_index()),
356 );
357 self
358 }
359
360 #[must_use]
365 pub fn add_state_stream(mut self, stream: StateStreamConfig) -> Self {
366 self.streams.push(stream);
367 self
368 }
369
370 #[must_use]
377 pub fn add_sampled_state_stream<I, K>(
378 mut self,
379 name: impl Into<String>,
380 fields: I,
381 sampling_interval: SamplingInterval,
382 ) -> Self
383 where
384 I: IntoIterator<Item = K>,
385 K: Into<String>,
386 {
387 self.streams
388 .push(StateStreamConfig::sampled(name, fields, sampling_interval));
389 self
390 }
391
392 pub fn create_new_recording(self) -> Result<SystemStateWriter, StorageError> {
404 SystemStateWriter::create_new_recording(self)
405 }
406
407 pub fn continue_existing_recording(self) -> Result<SystemStateWriter, StorageError> {
413 SystemStateWriter::continue_recording(self, None).map(|(writer, _)| writer)
414 }
415
416 pub fn continue_recording_from_latest_checkpoint(
422 self,
423 stream: &str,
424 decoders: JsonPayloadDecoderRegistry,
425 ) -> Result<(SystemStateWriter, SystemState), StorageError> {
426 let (writer, state) =
427 SystemStateWriter::continue_recording(self, Some((stream, decoders)))?;
428 Ok((
429 writer,
430 state.expect("checkpoint-aware resume always reconstructs one state"),
431 ))
432 }
433}
434
435pub struct SystemStateWriter {
442 root: PathBuf,
443 stream_order: Vec<String>,
444 manifest: Arc<RecordingManifest>,
445 streams: HashMap<String, ScheduledStateStream>,
446 writer: Option<StateWriterWorker>,
447 _lease: RecordingLease,
450}
451
452impl SystemStateWriter {
453 pub fn builder(root: impl Into<PathBuf>, spec: &SystemStateSchema) -> SystemStateWriterBuilder {
455 SystemStateWriterBuilder::new(root, spec)
456 }
457
458 pub fn recording_directory(&self) -> &Path {
460 &self.root
461 }
462
463 pub fn stream_names(&self) -> impl ExactSizeIterator<Item = &str> {
465 self.stream_order.iter().map(String::as_str)
466 }
467
468 pub fn observe_state(&mut self, state: &SystemState) -> Result<(), StorageError> {
482 let iteration = state.simulation_time().iteration();
483 let writer = self
484 .writer
485 .as_ref()
486 .expect("an active recording owns its writer worker");
487 for name in &self.stream_order {
488 let stream = self
489 .streams
490 .get_mut(name)
491 .expect("stream order contains every configured stream");
492 if !stream.sampling_interval.includes(iteration)
493 || stream.last_recorded_iteration == Some(iteration)
494 {
495 continue;
496 }
497 let record = stream.encoder.encode(state)?;
498 writer.submit_record(name, record)?;
499 stream.last_recorded_iteration = Some(iteration);
500 }
501 Ok(())
502 }
503
504 pub fn flush_stream_to_storage(&self, stream: &str) -> Result<(), StorageError> {
511 if !self.streams.contains_key(stream) {
512 return Err(StorageError::UnknownStateStream {
513 stream: stream.to_owned(),
514 });
515 }
516 self.writer
517 .as_ref()
518 .expect("an active recording owns its writer worker")
519 .flush_state_stream(stream)
520 }
521
522 pub fn complete_recording(mut self) -> Result<(), StorageError> {
530 if let Err(error) = self.finish_writer() {
531 let _ = self.manifest.transition(RecordingStatus::Failed {
532 message: error.to_string(),
533 });
534 return Err(error);
535 }
536 self.manifest.transition(RecordingStatus::Complete)
537 }
538
539 pub fn complete_recording_with_final_state(
546 mut self,
547 state: &SystemState,
548 ) -> Result<(), StorageError> {
549 self.record_final_state(state)?;
550 self.complete_recording()
551 }
552
553 fn record_final_state(&mut self, state: &SystemState) -> Result<(), StorageError> {
555 let iteration = state.simulation_time().iteration();
556 let writer = self
557 .writer
558 .as_ref()
559 .expect("an active recording owns its writer worker");
560 for name in &self.stream_order {
561 let stream = self
562 .streams
563 .get_mut(name)
564 .expect("stream order contains every configured stream");
565 if stream.last_recorded_iteration == Some(iteration) {
566 continue;
567 }
568 let record = stream.encoder.encode(state)?;
569 writer.submit_record(name, record)?;
570 stream.last_recorded_iteration = Some(iteration);
571 }
572 Ok(())
573 }
574
575 pub fn mark_recording_failed(mut self, message: impl Into<String>) -> Result<(), StorageError> {
587 let message = message.into();
588 if message.trim().is_empty() {
589 return Err(StorageError::InvalidConfiguration {
590 setting: "failure_message",
591 reason: "failed run message must not be empty".to_owned(),
592 });
593 }
594
595 if let Err(error) = self.finish_writer() {
596 let _ = self.manifest.transition(RecordingStatus::Failed {
597 message: error.to_string(),
598 });
599 return Err(error);
600 }
601 self.manifest
602 .transition(RecordingStatus::Failed { message })
603 }
604
605 fn create_new_recording(builder: SystemStateWriterBuilder) -> Result<Self, StorageError> {
607 ensure_absent(&builder.root)?;
608 let prepared = PreparedRecording::from_builder(builder)?;
609 create_root(&prepared.root)?;
610 let lease = RecordingLease::acquire(&prepared.root)?;
611 for stream in &prepared.streams {
612 stream.writer.create_directory()?;
613 }
614 commit_metadata(&prepared.root, &prepared.metadata_path, &prepared.metadata)?;
615 let manifest = Arc::new(RecordingManifest::new(
616 prepared.root.clone(),
617 prepared.metadata_path.clone(),
618 prepared.metadata,
619 ));
620 Self::start_new_prepared(prepared.root, prepared.streams, manifest, lease)
621 }
622
623 fn continue_recording(
625 builder: SystemStateWriterBuilder,
626 checkpoint: Option<(&str, JsonPayloadDecoderRegistry)>,
627 ) -> Result<(Self, Option<SystemState>), StorageError> {
628 let prepared = PreparedRecording::from_builder(builder)?;
629 let lease = RecordingLease::acquire(&prepared.root)?;
630 remove_stale_metadata_temp(&prepared.root)?;
631 let existing = load_metadata(&prepared.metadata_path)?;
632 if !matches!(existing.status, RecordingStatus::Running) {
633 return Err(StorageError::RecordingNotContinuable {
634 path: prepared.metadata_path,
635 });
636 }
637 ensure_resume_match(&prepared.metadata_path, &prepared.metadata, &existing)?;
638
639 let manifest = Arc::new(RecordingManifest::new(
640 prepared.root.clone(),
641 prepared.metadata_path.clone(),
642 existing.clone(),
643 ));
644 let mut recovered = Vec::with_capacity(prepared.streams.len());
645 for stream in prepared.streams {
646 let declaration = existing
647 .stream(&stream.name)
648 .expect("matched metadata contains every prepared stream");
649 let seed = StateWriterWorker::recover_state_stream(&stream.writer, declaration)?;
650 recovered.push((stream, seed));
651 }
652
653 let state = if let Some((checkpoint_stream, decoders)) = checkpoint {
654 let declaration = existing.stream(checkpoint_stream).ok_or_else(|| {
655 StorageError::UnknownStateStream {
656 stream: checkpoint_stream.to_owned(),
657 }
658 })?;
659 let seed = recovered
660 .iter()
661 .find(|(stream, _)| stream.name == checkpoint_stream)
662 .map(|(_, seed)| seed)
663 .expect("matched stream has one recovered seed");
664 Some(stored_state_series_reader::decode_resume_state(
665 &prepared.root,
666 &prepared.metadata_path,
667 declaration,
668 &prepared.spec,
669 &decoders,
670 seed.latest_open_record(),
671 )?)
672 } else {
673 None
674 };
675
676 let output = Self::start_resumed_prepared(prepared.root, recovered, manifest, lease)?;
677 Ok((output, state))
678 }
679
680 fn start_new_prepared(
682 root: PathBuf,
683 streams: Vec<PreparedStateStream>,
684 manifest: Arc<RecordingManifest>,
685 lease: RecordingLease,
686 ) -> Result<Self, StorageError> {
687 let mut scheduled = HashMap::with_capacity(streams.len());
688 let mut configs = Vec::with_capacity(streams.len());
689 let mut stream_order = Vec::with_capacity(streams.len());
690 for prepared in streams {
691 let name = prepared.name;
692 stream_order.push(name.clone());
693 scheduled.insert(
694 name,
695 ScheduledStateStream {
696 encoder: prepared.encoder,
697 sampling_interval: prepared.sampling_interval,
698 last_recorded_iteration: None,
699 },
700 );
701 configs.push(prepared.writer);
702 }
703 let writer = StateWriterWorker::start_new_recording(configs, Arc::clone(&manifest))?;
704 Ok(Self {
705 root,
706 stream_order,
707 manifest,
708 streams: scheduled,
709 writer: Some(writer),
710 _lease: lease,
711 })
712 }
713
714 fn start_resumed_prepared(
716 root: PathBuf,
717 streams: Vec<(PreparedStateStream, RecoveredStateStream)>,
718 manifest: Arc<RecordingManifest>,
719 lease: RecordingLease,
720 ) -> Result<Self, StorageError> {
721 let mut scheduled = HashMap::with_capacity(streams.len());
722 let mut recovered_streams = Vec::with_capacity(streams.len());
723 let mut stream_order = Vec::with_capacity(streams.len());
724 for (prepared, seed) in streams {
725 let name = prepared.name;
726 stream_order.push(name.clone());
727 scheduled.insert(
728 name,
729 ScheduledStateStream {
730 encoder: prepared.encoder,
731 sampling_interval: prepared.sampling_interval,
732 last_recorded_iteration: seed.last_iteration(),
733 },
734 );
735 recovered_streams.push((prepared.writer, seed));
736 }
737 let writer = StateWriterWorker::continue_recovered_recording(
738 recovered_streams,
739 Arc::clone(&manifest),
740 )?;
741 Ok(Self {
742 root,
743 stream_order,
744 manifest,
745 streams: scheduled,
746 writer: Some(writer),
747 _lease: lease,
748 })
749 }
750
751 fn finish_writer(&mut self) -> Result<(), StorageError> {
753 let Some(writer) = self.writer.take() else {
754 return Ok(());
755 };
756 writer.finish_recording()
757 }
758}
759
760struct PreparedRecording {
762 root: PathBuf,
763 metadata_path: PathBuf,
764 spec: SystemStateSchema,
765 metadata: RecordingMetadata,
766 streams: Vec<PreparedStateStream>,
767}
768
769impl PreparedRecording {
770 fn from_builder(builder: SystemStateWriterBuilder) -> Result<Self, StorageError> {
772 let metadata_path = builder.root.join(METADATA_FILE);
773 let stored_time = builder.time.into_stored();
774 let mut names = HashSet::with_capacity(builder.streams.len());
775 let mut directories = HashSet::with_capacity(builder.streams.len());
776 let mut streams = Vec::with_capacity(builder.streams.len());
777 let mut declarations = Vec::with_capacity(builder.streams.len());
778
779 for config in builder.streams {
780 if !names.insert(config.name.clone()) {
781 return Err(StorageError::DuplicateStateStream {
782 stream: config.name,
783 });
784 }
785 if !directories.insert(config.directory.clone()) {
786 return Err(StorageError::InvalidConfiguration {
787 setting: "stream.directory",
788 reason: format!(
789 "multiple streams use relative directory `{}`",
790 config.directory
791 ),
792 });
793 }
794
795 let (max_chunk_bytes, queue_bytes) = config
796 .storage_limits
797 .or(builder.shared_stream_limits)
798 .ok_or_else(|| StorageError::InvalidConfiguration {
799 setting: "stream.storage_limits",
800 reason: format!(
801 "stream `{}` has no explicit limits and the writer has no shared limits",
802 config.name
803 ),
804 })?;
805 let encoder = JsonStateRecordEncoder::new(&config.name, &builder.spec, &config.fields)?;
806 let fields = encoder
807 .fields()
808 .map(|name| {
809 let field = builder
810 .spec
811 .field_schema(name)
812 .expect("encoder fields were validated against this specification");
813 StateFieldMetadata {
814 name: name.to_owned(),
815 description: field.description().map(str::to_owned),
816 }
817 })
818 .collect::<Vec<_>>();
819 declarations.push(StateStreamMetadata {
820 name: config.name.clone(),
821 directory: config.directory.clone(),
822 sampling_interval: config.sampling_interval,
823 fields,
824 max_chunk_bytes: max_chunk_bytes.get(),
825 queue_bytes: queue_bytes.get(),
826 chunks: Vec::new(),
827 });
828 streams.push(PreparedStateStream {
829 name: config.name.clone(),
830 encoder,
831 sampling_interval: config.sampling_interval,
832 writer: StateStreamStorageConfig::new(
833 &config.name,
834 builder.root.join(&config.directory),
835 max_chunk_bytes,
836 queue_bytes,
837 )?,
838 });
839 }
840
841 let metadata = RecordingMetadata::running(stored_time, builder.user_metadata, declarations);
842 metadata.validate(&metadata_path)?;
843 Ok(Self {
844 root: builder.root,
845 metadata_path,
846 spec: builder.spec,
847 metadata,
848 streams,
849 })
850 }
851}
852
853struct PreparedStateStream {
855 name: String,
856 encoder: JsonStateRecordEncoder,
857 sampling_interval: SamplingInterval,
858 writer: StateStreamStorageConfig,
859}
860
861struct ScheduledStateStream {
863 encoder: JsonStateRecordEncoder,
864 sampling_interval: SamplingInterval,
865 last_recorded_iteration: Option<u64>,
866}
867
868pub(crate) struct RecordingManifest {
874 root: PathBuf,
875 path: PathBuf,
876 metadata: Mutex<RecordingMetadata>,
877}
878
879impl RecordingManifest {
880 fn new(root: PathBuf, path: PathBuf, metadata: RecordingMetadata) -> Self {
882 Self {
883 root,
884 path,
885 metadata: Mutex::new(metadata),
886 }
887 }
888
889 pub(crate) fn prepare_chunk(
891 &self,
892 stream: &str,
893 descriptor: jsonl_format::ChunkMetadata,
894 ) -> Result<(), StorageError> {
895 let mut current = lock_metadata(&self.metadata);
896 if !matches!(current.status, RecordingStatus::Running) {
897 return Err(StorageError::RecordingFinished);
898 }
899 let mut candidate = current.clone();
900 let declaration =
901 candidate
902 .stream_mut(stream)
903 .ok_or_else(|| StorageError::UnknownStateStream {
904 stream: stream.to_owned(),
905 })?;
906 let expected = u64::try_from(declaration.chunks.len()).map_err(|_| {
907 StorageError::ByteCountOverflow {
908 stream: stream.to_owned(),
909 }
910 })?;
911 if descriptor.ordinal != expected {
912 return Err(StorageError::InvalidMetadata {
913 path: self.path.clone(),
914 reason: format!(
915 "stream `{stream}` prepared chunk ordinal {}, expected {expected}",
916 descriptor.ordinal
917 ),
918 });
919 }
920 declaration.chunks.push(descriptor);
921 commit_metadata(&self.root, &self.path, &candidate)?;
922 *current = candidate;
923 Ok(())
924 }
925
926 fn transition(&self, status: RecordingStatus) -> Result<(), StorageError> {
928 let mut current = lock_metadata(&self.metadata);
929 let mut candidate = current.clone();
930 candidate.status = status;
931 commit_metadata(&self.root, &self.path, &candidate)?;
932 *current = candidate;
933 Ok(())
934 }
935}
936
937struct RecordingLease {
942 _directory: File,
943}
944
945impl RecordingLease {
946 fn acquire(root: &Path) -> Result<Self, StorageError> {
948 let directory = File::open(root).map_err(|source| StorageError::Io {
949 operation: "open output root for exclusive ownership",
950 path: root.to_path_buf(),
951 source,
952 })?;
953 match FileExt::try_lock_exclusive(&directory) {
954 Ok(()) => Ok(Self {
955 _directory: directory,
956 }),
957 Err(source) if source.kind() == std::io::ErrorKind::WouldBlock => {
958 Err(StorageError::RecordingDirectoryInUse {
959 path: root.to_path_buf(),
960 })
961 }
962 Err(source) => Err(StorageError::Io {
963 operation: "acquire exclusive output ownership",
964 path: root.to_path_buf(),
965 source,
966 }),
967 }
968 }
969}
970
971fn load_metadata(path: &Path) -> Result<RecordingMetadata, StorageError> {
973 let bytes = fs::read(path).map_err(|source| StorageError::Io {
974 operation: "read metadata for resume",
975 path: path.to_path_buf(),
976 source,
977 })?;
978 let metadata: RecordingMetadata =
979 serde_json::from_slice(&bytes).map_err(|source| StorageError::Json {
980 operation: "parse metadata for resume",
981 path: path.to_path_buf(),
982 source,
983 })?;
984 metadata.validate(path)?;
985 Ok(metadata)
986}
987
988fn ensure_resume_match(
990 path: &Path,
991 expected: &RecordingMetadata,
992 existing: &RecordingMetadata,
993) -> Result<(), StorageError> {
994 let mut configuration = existing.clone();
995 for stream in &mut configuration.streams {
996 stream.chunks.clear();
997 }
998 configuration.status = RecordingStatus::Running;
999 if &configuration != expected {
1000 return Err(StorageError::RecordingConfigurationMismatch {
1001 path: path.to_path_buf(),
1002 reason: "builder time axis, user metadata, or stream declarations differ".to_owned(),
1003 });
1004 }
1005 Ok(())
1006}
1007
1008fn remove_stale_metadata_temp(root: &Path) -> Result<(), StorageError> {
1010 let path = root.join(METADATA_TEMP_FILE);
1011 match fs::remove_file(&path) {
1012 Ok(()) => File::open(root)
1013 .and_then(|directory| directory.sync_all())
1014 .map_err(|source| StorageError::Io {
1015 operation: "synchronize stale metadata cleanup",
1016 path: root.to_path_buf(),
1017 source,
1018 }),
1019 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
1020 Err(source) => Err(StorageError::Io {
1021 operation: "remove stale temporary metadata",
1022 path,
1023 source,
1024 }),
1025 }
1026}
1027
1028fn lock_metadata(metadata: &Mutex<RecordingMetadata>) -> MutexGuard<'_, RecordingMetadata> {
1030 metadata
1031 .lock()
1032 .unwrap_or_else(|poisoned| poisoned.into_inner())
1033}
1034
1035fn ensure_absent(root: &Path) -> Result<(), StorageError> {
1037 match root.try_exists() {
1038 Ok(false) => Ok(()),
1039 Ok(true) => Err(StorageError::RecordingDirectoryExists {
1040 path: root.to_path_buf(),
1041 }),
1042 Err(source) => Err(StorageError::Io {
1043 operation: "inspect output root",
1044 path: root.to_path_buf(),
1045 source,
1046 }),
1047 }
1048}
1049
1050fn create_root(root: &Path) -> Result<(), StorageError> {
1052 match fs::create_dir(root) {
1053 Ok(()) => Ok(()),
1054 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
1055 Err(StorageError::RecordingDirectoryExists {
1056 path: root.to_path_buf(),
1057 })
1058 }
1059 Err(source) => Err(StorageError::Io {
1060 operation: "create output root",
1061 path: root.to_path_buf(),
1062 source,
1063 }),
1064 }
1065}
1066
1067fn commit_metadata(
1075 root: &Path,
1076 metadata_path: &Path,
1077 metadata: &RecordingMetadata,
1078) -> Result<(), StorageError> {
1079 metadata.validate(metadata_path)?;
1080 let mut bytes = serde_json::to_vec_pretty(metadata).map_err(|source| StorageError::Json {
1081 operation: "serialize metadata",
1082 path: metadata_path.to_path_buf(),
1083 source,
1084 })?;
1085 bytes.push(b'\n');
1086
1087 let temporary_path = root.join(METADATA_TEMP_FILE);
1088 let result = write_and_replace_metadata(root, metadata_path, &temporary_path, &bytes);
1089 if result.is_err() {
1090 let _ = fs::remove_file(&temporary_path);
1091 }
1092 result
1093}
1094
1095fn write_and_replace_metadata(
1097 root: &Path,
1098 metadata_path: &Path,
1099 temporary_path: &Path,
1100 bytes: &[u8],
1101) -> Result<(), StorageError> {
1102 let mut temporary = OpenOptions::new()
1103 .write(true)
1104 .create_new(true)
1105 .open(temporary_path)
1106 .map_err(|source| StorageError::Io {
1107 operation: "create temporary metadata",
1108 path: temporary_path.to_path_buf(),
1109 source,
1110 })?;
1111 temporary
1112 .write_all(bytes)
1113 .map_err(|source| StorageError::Io {
1114 operation: "write temporary metadata",
1115 path: temporary_path.to_path_buf(),
1116 source,
1117 })?;
1118 temporary.sync_all().map_err(|source| StorageError::Io {
1119 operation: "sync temporary metadata",
1120 path: temporary_path.to_path_buf(),
1121 source,
1122 })?;
1123 drop(temporary);
1124
1125 fs::rename(temporary_path, metadata_path).map_err(|source| StorageError::Io {
1126 operation: "publish metadata",
1127 path: metadata_path.to_path_buf(),
1128 source,
1129 })?;
1130
1131 File::open(root)
1132 .and_then(|directory| directory.sync_all())
1133 .map_err(|source| StorageError::Io {
1134 operation: "sync output root",
1135 path: root.to_path_buf(),
1136 source,
1137 })
1138}