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