somatize_store/store.rs
1//! Who keeps what is worth keeping. The hole.
2
3use crate::Digest;
4use std::fmt;
5
6/// What you want to remember about something you stored, in the order you say
7/// it. Text and not a closed type because the vocabulary is the caller's; what
8/// this crate does with it is write it down and hand it back.
9pub type Meta = Vec<(String, String)>;
10
11/// A name, what it points at, and what was said about it.
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13pub struct Bound {
14 /// What it is called: a cache key, an artifact's id.
15 pub name: String,
16 /// The bytes it points at.
17 pub digest: Digest,
18 /// What the caller wanted to remember.
19 pub meta: Meta,
20 /// When it was bound, in seconds since the epoch. Stamped here because a
21 /// store you cannot sort by time is one you cannot explore.
22 pub when: u64,
23}
24
25/// Keeps bytes by their content, and names that point at them.
26pub trait Store: Send + Sync {
27 /// Saves these bytes. Saving the same ones twice is the same as saving them
28 /// once — that is what content addressing is for.
29 fn put(&self, bytes: &[u8]) -> Result<Digest, StoreError>;
30
31 /// The bytes, if they are here.
32 fn get(&self, digest: &Digest) -> Result<Option<Vec<u8>>, StoreError>;
33
34 /// Points a name at some bytes, with what you want to remember about it.
35 ///
36 /// Binding the same name again replaces it: a name is the question, and the
37 /// answer can be refreshed — which is what `.overwrite()` will do.
38 fn bind(&self, name: &str, digest: &Digest, meta: Meta) -> Result<(), StoreError>;
39
40 /// Points a name at some bytes **only if nobody has**, and says whether it
41 /// did. This is how work gets handed out.
42 ///
43 /// Not `resolve` then `bind`: between the two somebody else does the same,
44 /// and two machines train the same round while nobody trains the next. Hence
45 /// on the trait with no default — one written out of the other two would be
46 /// a race with a doc comment on it.
47 fn claim(&self, name: &str, digest: &Digest, meta: Meta) -> Result<bool, StoreError>;
48
49 /// What that name points at, if anything.
50 fn resolve(&self, name: &str) -> Result<Option<Bound>, StoreError>;
51
52 /// The same for many at once, in the order they were asked. In the trait
53 /// from the first day: a cache that works item by item asks thousands at a
54 /// time, which against a remote store is thousands of round trips unless it
55 /// is one call. The default is the loop.
56 fn resolve_many(&self, names: &[&str]) -> Result<Vec<Option<Bound>>, StoreError> {
57 names.iter().map(|name| self.resolve(name)).collect()
58 }
59
60 /// The same for the bytes.
61 fn get_many(&self, digests: &[&Digest]) -> Result<Vec<Option<Vec<u8>>>, StoreError> {
62 digests.iter().map(|digest| self.get(digest)).collect()
63 }
64
65 /// Everything bound here. A scan, and that is the point: the records are the
66 /// truth, and an index that answers faster is built from them and thrown
67 /// away.
68 fn bound(&self) -> Result<Vec<Bound>, StoreError>;
69}
70
71/// One record, in the JSON it is kept as — readable with `cat`, which was a
72/// requirement before it was a format. Here and not in an implementor, because
73/// it is what makes a directory and a bucket the same store.
74pub(crate) fn record(name: &str, digest: &Digest, meta: Meta) -> Result<Vec<u8>, StoreError> {
75 let bound = Bound {
76 name: name.to_string(),
77 digest: digest.clone(),
78 meta,
79 when: std::time::SystemTime::now()
80 .duration_since(std::time::UNIX_EPOCH)
81 .map(|since| since.as_secs())
82 .unwrap_or(0),
83 };
84 serde_json::to_vec_pretty(&bound)
85 .map_err(|e| StoreError::Corrupt(format!("that record cannot be written: {e}")))
86}
87
88/// The other direction.
89pub(crate) fn read_record(bytes: &[u8]) -> Result<Bound, StoreError> {
90 serde_json::from_slice(bytes)
91 .map_err(|e| StoreError::Corrupt(format!("that record cannot be read: {e}")))
92}
93
94/// Why something could not be kept, or found.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum StoreError {
97 /// The system said no: no permission, no space, no such directory.
98 Io(String),
99 /// Something is there and is not what it should be.
100 Corrupt(String),
101}
102
103impl fmt::Display for StoreError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Io(why) => write!(f, "the store could not be reached: {why}"),
107 Self::Corrupt(why) => write!(f, "what the store has is not what it should be: {why}"),
108 }
109 }
110}
111
112impl std::error::Error for StoreError {}