Skip to main content

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}