somatize_store/recorder.rs
1//! The engine's [`Watcher`], filled in by a [`Store`]: what happened, kept.
2//!
3//! What arrives is a stream of facts; what is written is **one record per
4//! `forward`**. Five nodes trained ten thousand steps are fifty thousand node
5//! executions — a record each is a scan nobody can afford — and one record for
6//! the whole run has no step 500 in it. The `forward` is the unit the engine
7//! has: a [`Plan`] is walked once per one.
8//!
9//! ```text
10//! run/<id>/<n>
11//! ```
12//!
13//! In the **record**, which a scan already carries: `run`, `forward`, `took_us`,
14//! `state = ok | broke`, `nodes`, and `<kind>.<field>` for whatever was asked
15//! for with [`summarising`](Recorder::summarising). In the **blob**: every fact,
16//! flattened, in the order it arrived. So *how is it going* costs one scan and
17//! no fetches, and only the detail is paid for — the last row is what keeps a
18//! training curve on the cheap side of that line.
19//!
20//! Two ways in, and they come through different doors so nothing has to be
21//! guessed: [`saw`](Watcher::saw) is the engine's and a terminal fact closes a
22//! record, while [`said`](Recorder::said) is for a vocabulary that is not — a
23//! loss, which arrives **after** the `forward` it belongs to and is written into
24//! the one that closed last.
25//!
26//! It does not judge. Whether 400 ms is slow is an opinion about this, and it
27//! has to be reachable from what is written here without running again.
28
29use crate::{Meta, Store, StoreError};
30use somatize_core::{Fact, Watcher};
31use std::sync::{Arc, Mutex};
32
33/// One fact as it is written: a name and text-to-text fields.
34type Written = (String, Vec<(String, String)>);
35
36/// What is being accumulated for the `forward` in flight.
37#[derive(Default)]
38struct Pending {
39 /// Which `forward` this is, from zero.
40 which: usize,
41 /// Its facts, in the order they arrived — which for a wave is not the order
42 /// they happened in, and nothing here pretends otherwise.
43 facts: Vec<Written>,
44 /// Whether a terminal fact has already closed it.
45 closed: bool,
46}
47
48/// Writes down what happened, one record per `forward`. It **owns** its store
49/// where a [`Cache`](crate::Cache) borrows one: a cache is made for one
50/// `forward` and a recorder counts them.
51pub struct Recorder {
52 store: Arc<dyn Store>,
53 run: String,
54 /// Which kinds of fact are worth having in the record itself and not only
55 /// in the blob. See [`summarising`](Self::summarising).
56 summarising: Vec<String>,
57 pending: Mutex<Pending>,
58}
59
60impl Recorder {
61 /// A recorder over this store, under a name of its own — made here and
62 /// readable with [`run`](Self::run), because a `forward` in a notebook has
63 /// no reason to invent one and still has to be findable.
64 pub fn over(store: Arc<dyn Store>) -> Self {
65 Self::named(store, made_up())
66 }
67
68 /// The same, under a name you chose: a training run that wants to be found
69 /// again by the name it already has.
70 pub fn named(store: Arc<dyn Store>, run: impl Into<String>) -> Self {
71 Self {
72 store,
73 run: run.into(),
74 summarising: Vec::new(),
75 pending: Mutex::new(Pending::default()),
76 }
77 }
78
79 /// The same recorder, with these kinds of fact carried **in the record** and
80 /// not only in the blob, as `<kind>.<field>`.
81 ///
82 /// Ten thousand losses read one blob at a time is ten thousand round trips
83 /// and the number wanted from each is one. Which kinds those are is the
84 /// caller's, so this crate does not learn what a loss is. The **last** fact
85 /// of each kind in a `forward` is the one carried.
86 pub fn summarising(mut self, kinds: impl IntoIterator<Item = impl Into<String>>) -> Self {
87 self.summarising = kinds.into_iter().map(Into::into).collect();
88 self
89 }
90
91 /// What this run is called, which is the first half of every name it writes.
92 pub fn run(&self) -> &str {
93 &self.run
94 }
95
96 /// One fact from a vocabulary that is not the engine's. It lands in the
97 /// `forward` in flight, or — the normal case for a loss — in the one that
98 /// ended last, whose record is rewritten. The fields are text to text
99 /// because that is what a record is; the vocabulary is the caller's.
100 pub fn said(&self, kind: &str, fields: Vec<(String, String)>) {
101 let mut pending = self.pending.lock().expect("nobody poisons this mutex");
102 pending.facts.push((kind.to_string(), fields));
103 if pending.closed {
104 // Already written once. A name is a question and the answer can be
105 // refreshed, so this is a rebind of the same record and not a new
106 // one — the same thing a trial's record does on every report.
107 self.write(&pending);
108 }
109 }
110
111 /// The name of one `forward`'s record. `run/<id>/<n>`, which is
112 /// `<study>/trial/<n>/<attempt>` with a different noun in it: the level
113 /// above, and a number.
114 fn name(&self, which: usize) -> String {
115 format!("run/{}/{which}", self.run)
116 }
117
118 /// Writes what is pending. Failing is reported and not returned: there is no
119 /// useful answer to "the record could not be written" in the middle of a
120 /// run, and stopping one because its log could not be kept would be the
121 /// observability layer breaking the thing it observes.
122 fn write(&self, pending: &Pending) {
123 let blob = match blob(&pending.facts) {
124 Ok(blob) => blob,
125 Err(why) => return eprintln!("what happened could not be written down: {why}"),
126 };
127 let written = self.store.put(&blob).and_then(|digest| {
128 self.store.bind(
129 &self.name(pending.which),
130 &digest,
131 meta(&self.run, &self.summarising, pending),
132 )
133 });
134 if let Err(why) = written {
135 eprintln!("what happened could not be kept: {why}");
136 }
137 }
138}
139
140impl Watcher for Recorder {
141 fn saw(&self, fact: &Fact) {
142 let (kind, fields) = fact.flattened();
143 let mut pending = self.pending.lock().expect("nobody poisons this mutex");
144 // A fact after a closed record is the next `forward` beginning. Only the
145 // engine's door does this: level 2's belongs to the one that ended.
146 if pending.closed {
147 let next = pending.which + 1;
148 *pending = Pending {
149 which: next,
150 ..Pending::default()
151 };
152 }
153 pending.facts.push((kind.to_string(), fields));
154 if fact.ends_a_run() {
155 pending.closed = true;
156 self.write(&pending);
157 }
158 }
159}
160
161/// What a scan carries, so that *how is it going* costs no fetches. Read back
162/// off the facts rather than counted as they arrive: a rewritten record has to
163/// say the same thing about the same facts, and a counter that only goes up
164/// would not.
165fn meta(run: &str, summarising: &[String], pending: &Pending) -> Meta {
166 let how_many = |kind: &str| pending.facts.iter().filter(|(one, _)| one == kind).count();
167 let mut meta = vec![
168 ("run".to_string(), run.to_string()),
169 ("forward".to_string(), pending.which.to_string()),
170 (
171 "state".to_string(),
172 match how_many("broke") {
173 0 => "ok".to_string(),
174 _ => "broke".to_string(),
175 },
176 ),
177 ("nodes".to_string(), how_many("ran").to_string()),
178 ];
179 if let Some(took) = field_of(&pending.facts, "finished", "took_us") {
180 meta.push(("took_us".to_string(), took.to_string()));
181 }
182 for kind in summarising {
183 let Some((_, fields)) = pending.facts.iter().rev().find(|(one, _)| one == kind) else {
184 continue;
185 };
186 for (name, what) in fields {
187 meta.push((format!("{kind}.{name}"), what.clone()));
188 }
189 }
190 meta
191}
192
193/// One field of the last fact of that kind, if it is there.
194fn field_of<'f>(facts: &'f [Written], kind: &str, field: &str) -> Option<&'f str> {
195 facts
196 .iter()
197 .rev()
198 .find(|(one, _)| one == kind)?
199 .1
200 .iter()
201 .find(|(name, _)| name == field)
202 .map(|(_, what)| what.as_str())
203}
204
205/// The detail, as JSON: whoever reads a record is another process, often on
206/// another machine and sometimes a notebook, and none of them should need this
207/// library's version of anything to look at it.
208fn blob(facts: &[Written]) -> Result<Vec<u8>, StoreError> {
209 let said: Vec<_> = facts
210 .iter()
211 .map(|(kind, fields)| {
212 let mut one = serde_json::Map::new();
213 one.insert("fact".to_string(), serde_json::Value::String(kind.clone()));
214 for (name, what) in fields {
215 one.insert(name.clone(), serde_json::Value::String(what.clone()));
216 }
217 serde_json::Value::Object(one)
218 })
219 .collect();
220 serde_json::to_vec_pretty(&said)
221 .map_err(|e| StoreError::Corrupt(format!("that record cannot be written: {e}")))
222}
223
224/// A name for a run nobody named. The pid and a counter, which is enough:
225/// two runs in one process are two numbers, and two processes are two pids.
226fn made_up() -> String {
227 use std::sync::atomic::{AtomicU64, Ordering};
228 static RUNS: AtomicU64 = AtomicU64::new(0);
229 format!(
230 "{}-{}",
231 std::process::id(),
232 RUNS.fetch_add(1, Ordering::Relaxed)
233 )
234}