artifactize_core/monitor/
loader.rs1use super::{Node, Scope, catalog, model};
7use crate::{
8 store::{self, RequestView, RunSummary, RunView, reader::Reader},
9 types::RunId,
10};
11use std::{path::PathBuf, sync::Arc};
12use time::OffsetDateTime;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Page {
17 pub scope: Scope,
18 pub open: Option<RunId>,
20 pub offset: u32,
21 pub tail: bool,
22 pub limit: u32,
23}
24
25pub fn older_offset(offset: u32, limit: u32, shown: u32) -> Option<u32> {
27 (shown >= limit).then(|| offset.saturating_add(limit))
28}
29
30pub fn newer_offset(offset: u32, limit: u32) -> Option<u32> {
32 (offset > 0).then(|| offset.saturating_sub(limit))
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Selection {
38 pub state: PathBuf,
39 pub page: Page,
40 pub revision: u64,
41}
42
43pub struct Input {
45 pub state: PathBuf,
46 pub initial: Option<PathBuf>,
48 pub catalog: catalog::Catalog,
49 pub initialized: bool,
51 pub page: Page,
52 pub full: bool,
54 pub displayed: Option<RunId>,
56 pub runs: Option<Vec<RunSummary>>,
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
62pub struct Origin {
63 identity: Option<crate::types::StateId>,
64 database: Option<crate::platform::FileIdentity>,
65}
66impl Origin {
67 pub fn replaces(&self, previous: &Self) -> bool {
70 previous.database.is_some() && previous.database != self.database
71 || previous.identity.is_some() && previous.identity != self.identity
72 }
73 pub fn identity(&self) -> Option<&crate::types::StateId> {
74 self.identity.as_ref()
75 }
76 pub fn read(state: &std::path::Path) -> Result<Self, String> {
80 let path = state.join(store::DATABASE);
81 let database =
82 match crate::platform::open_no_follow(std::fs::File::options().read(true), &path) {
83 Ok(file) => Some(crate::platform::file_identity(&file).map_err(|e| e.to_string())?),
84 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
85 Err(error) => return Err(error.to_string()),
86 };
87 Ok(Self {
88 identity: store::read_state_id(state)?,
89 database,
90 })
91 }
92}
93
94pub struct Snapshot {
96 pub origin: Origin,
97 pub catalog: catalog::Catalog,
98 pub scope: Scope,
99 pub runs: Vec<RunSummary>,
100 pub selected_id: Option<RunId>,
101 pub selected: Option<(RunView, Vec<RequestView>, Arc<Vec<Node>>)>,
103}
104
105#[derive(Default)]
108pub struct Loader {
109 reader: Reader,
110}
111impl Loader {
112 #[cfg(any(test, feature = "test-support"))]
114 pub fn traced_statements(&self) -> Vec<String> {
115 self.reader.traced_statements()
116 }
117
118 pub fn load(&mut self, input: Input) -> Result<Snapshot, String> {
119 load(&mut self.reader, input)
120 }
121 pub fn reset(&mut self) {
123 self.reader.reset();
124 }
125}
126
127pub struct Completed {
128 pub selection: Selection,
129 pub loader: Loader,
130 pub result: Result<Snapshot, String>,
131 #[cfg(any(test, feature = "test-support"))]
133 pub queries: Vec<String>,
134}
135impl Completed {
136 pub fn run(selection: Selection, mut loader: Loader, input: Input) -> Self {
138 #[cfg(any(test, feature = "test-support"))]
139 loader.traced_statements();
140 let result = loader.load(input);
141 #[cfg(any(test, feature = "test-support"))]
142 let queries = loader.traced_statements();
143 Self {
144 selection,
145 loader,
146 result,
147 #[cfg(any(test, feature = "test-support"))]
148 queries,
149 }
150 }
151}
152
153fn load(reader: &mut Reader, input: Input) -> Result<Snapshot, String> {
154 let mut catalog = input.catalog;
155 let mut scope = input.page.scope.clone();
156 if !input
158 .state
159 .join(store::DATABASE)
160 .try_exists()
161 .map_err(|error| error.to_string())?
162 {
163 store::check_probe_files(&input.state)?;
164 catalog.update(&[], input.initial.as_deref());
165 if !input.initialized {
166 scope = catalog.initial(input.initial.as_deref());
167 }
168 reader.reset();
169 return Ok(Snapshot {
170 origin: Origin::default(),
171 catalog,
172 scope,
173 runs: Vec::new(),
174 selected_id: None,
175 selected: None,
176 });
177 }
178 let saved = reader.snapshot_optional(&input.state, |db, cache, state| {
179 let reuse_page = input.runs.is_some()
180 && !input.full
181 && input.initialized
182 && cache.has_catalog()
183 && !cache.changed();
184 if input.initialized {
185 catalog.selected = Some(scope.clone());
186 }
187 if input.full || !input.initialized || !cache.has_catalog() || cache.changed() {
188 let facts = cache.catalog(db, state)?;
189 if input.initialized {
190 catalog.selected = Some(scope.clone());
191 }
192 catalog.update(&facts, input.initial.as_deref());
193 if !input.initialized {
194 scope = catalog.initial(input.initial.as_deref());
195 catalog.selected = Some(scope.clone());
196 catalog.update(&facts, input.initial.as_deref());
197 }
198 }
199 if !input.full && input.initialized {
200 catalog.rebuild();
201 }
202 let paths = catalog.paths(&scope);
203 let runs = if reuse_page {
204 input.runs.clone().expect("accepted same-database page")
205 } else {
206 cache.page(
207 db,
208 state,
209 paths.as_deref(),
210 input.page.limit,
211 input.page.offset,
212 input.page.open.as_ref(),
213 )?
214 };
215 let selected = input
216 .page
217 .open
218 .as_ref()
219 .and_then(|id| runs.iter().find(|run| &run.id == id))
220 .or_else(|| {
221 if input.page.tail {
222 runs.iter().take(input.page.limit as usize).next_back()
223 } else {
224 runs.first()
225 }
226 });
227 let selected_id = selected.map(|run| run.id.clone());
228 let selected = match selected.filter(|run| run.readable()) {
229 Some(run) => cache.selected(
230 db,
231 state,
232 &run.id,
233 Some(run.unreadable.clone()),
234 input.displayed.as_ref() != Some(&run.id),
235 )?,
236 None => None,
237 };
238 Ok((cache.identity(db)?, runs, selected_id, selected))
239 })?;
240 let Some((identity, runs, selected_id, selected)) = saved else {
241 catalog.update(&[], input.initial.as_deref());
242 if !input.initialized {
243 scope = catalog.initial(input.initial.as_deref());
244 }
245 return Ok(Snapshot {
246 origin: Origin {
247 identity: None,
248 database: reader.database(),
249 },
250 catalog,
251 scope,
252 runs: Vec::new(),
253 selected_id: None,
254 selected: None,
255 });
256 };
257 let selected = selected.map(|(run, requests)| {
259 let nodes = Arc::new(model::tree(&run, &requests, OffsetDateTime::now_utc()));
260 (run, requests, nodes)
261 });
262 reader.require_current(&input.state)?;
263 Ok(Snapshot {
264 origin: Origin {
265 identity,
266 database: reader.database(),
267 },
268 catalog,
269 scope,
270 runs,
271 selected_id,
272 selected,
273 })
274}