nautilus_event_store/manifest.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! The per-run manifest stored alongside captured entries.
17
18use indexmap::IndexMap;
19use nautilus_core::UnixNanos;
20pub use nautilus_system::{RegisteredComponents, event_store::RunId};
21use serde::{Deserialize, Serialize};
22
23use crate::wire;
24
25/// Lifecycle state of a captured run.
26#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum RunStatus {
28 /// The run is open and accepting writes.
29 Running,
30 /// The run was sealed by graceful shutdown after a `RunEnded` entry.
31 Ended,
32 /// The run was sealed on boot after the writer found no `RunEnded` entry.
33 CrashedRecovered,
34 /// The run failed an integrity check and is unsafe to replay.
35 Quarantined,
36}
37
38/// Per-run manifest persisted alongside captured entries.
39///
40/// Every field is recorded at run start; `end_ts_init`, `high_watermark`, and `status` are
41/// updated when the run is sealed.
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
43pub struct RunManifest {
44 /// The id of this run.
45 pub run_id: RunId,
46 /// The id of the predecessor run that this run resumes from, if any.
47 pub parent_run_id: Option<RunId>,
48 /// The id of the trading instance owning this run.
49 pub instance_id: String,
50 /// A hex-encoded hash of the trader binary.
51 pub binary_hash: String,
52 /// Bumps when the entry payload schema changes.
53 pub schema_version: u32,
54 /// A hex-encoded hash of `Cargo.lock` or an equivalent crate version manifest.
55 pub crate_versions: String,
56 /// The active Cargo features for the trader binary.
57 pub feature_flags: Vec<String>,
58 /// Per-adapter version stamp keyed by adapter name.
59 pub adapter_versions: IndexMap<String, String>,
60 /// A hex-encoded hash of the kernel configuration.
61 pub config_hash: String,
62 /// Registered actor, strategy, and algorithm identities along with subscription bindings.
63 pub registered_components: RegisteredComponents,
64 /// The deterministic seed, populated when the run executes under a seeded mode.
65 pub seed: Option<u64>,
66 /// The first `ts_init` observed by the writer for this run.
67 #[serde(with = "wire::nanos_as_u64")]
68 pub start_ts_init: UnixNanos,
69 /// The last `ts_init` observed by the writer for this run, populated on seal.
70 #[serde(with = "wire::opt_nanos_as_u64")]
71 pub end_ts_init: Option<UnixNanos>,
72 /// The largest `seq` durably acknowledged by the backend at end of run.
73 pub high_watermark: u64,
74 /// The lifecycle state of this run.
75 pub status: RunStatus,
76}
77
78impl RunManifest {
79 /// Returns `true` once `status` is anything other than [`RunStatus::Running`].
80 #[must_use]
81 pub const fn is_sealed(&self) -> bool {
82 !matches!(self.status, RunStatus::Running)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use rstest::rstest;
89
90 use super::*;
91
92 fn manifest_with(status: RunStatus) -> RunManifest {
93 RunManifest {
94 run_id: "1700000000-abcd1234".to_string(),
95 parent_run_id: None,
96 instance_id: "trader-001".to_string(),
97 binary_hash: "deadbeef".to_string(),
98 schema_version: 1,
99 crate_versions: "feedface".to_string(),
100 feature_flags: vec!["live".to_string()],
101 adapter_versions: IndexMap::new(),
102 config_hash: "cafebabe".to_string(),
103 registered_components: RegisteredComponents::default(),
104 seed: None,
105 start_ts_init: UnixNanos::from(0),
106 end_ts_init: None,
107 high_watermark: 0,
108 status,
109 }
110 }
111
112 #[rstest]
113 #[case(RunStatus::Running, false)]
114 #[case(RunStatus::Ended, true)]
115 #[case(RunStatus::CrashedRecovered, true)]
116 #[case(RunStatus::Quarantined, true)]
117 fn is_sealed_matches_status(#[case] status: RunStatus, #[case] expected: bool) {
118 assert_eq!(manifest_with(status).is_sealed(), expected);
119 }
120}