OXYGENHUB
◆ v0.11.0 oxygen / crates / oxy-core / src / repo / checkpoint.rs
11.5 KiB · Rust · last changed A shallow clone fetches its checkpoint in parts (refs #137) 10 days ago source blame history raw
252e8cb2 Andy McCall ↶ 1
//! Checkpoints (spec §11): a serialized state kept per bookmark so loading
2
//! a state costs the changes since the checkpoint rather than all of them.
3
//!
4
//! The newest checkpoint of each bookmark is pointed at by
5
//! `.oxy/checkpoints/<bookmark>`; older ones become unreachable and are
6
//! garbage collected. A checkpoint applies to a requested head set when all
7
//! of its heads lie in that set's closure, which the loader establishes while
8
//! walking from the requested heads and stopping at checkpoint members.
9
10
use super::*;
11
use crate::objects::Checkpoint;
12
13
impl Repo {
14
    fn checkpoint_pointer(&self, bookmark: &str) -> PathBuf {
54cd08a1 Andy McCall ↶ 15
        self.oxy.join("checkpoints").join(name_file(bookmark))
252e8cb2 Andy McCall ↶ 16
    }
17
18
    /// The newest checkpoint recorded for a bookmark.
19
    pub fn latest_checkpoint(&self, bookmark: &str) -> Result<Option<(ObjectRef, Checkpoint)>> {
20
        match fs::read_to_string(self.checkpoint_pointer(bookmark)) {
21
            Ok(h) => {
75167672 Andy McCall 22
                let r = ObjectRef::from_hex(h.trim())
23
                    .ok_or_else(|| RepoError::Msg("corrupt checkpoint pointer".into()))?;
252e8cb2 Andy McCall ↶ 24
                match self.store.get_typed(&r, ObjectType::Checkpoint) {
25
                    Ok(bytes) => Ok(Some((r, Checkpoint::decode(&bytes)?))),
26
                    Err(StoreError::NotFound(_)) => Ok(None),
27
                    Err(e) => Err(e.into()),
28
                }
29
            }
30
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
31
            Err(e) => Err(e.into()),
32
        }
33
    }
34
35
    /// Every checkpoint pointer, for garbage collection roots.
36
    pub(crate) fn checkpoint_refs(&self) -> Result<Vec<ObjectRef>> {
37
        let dir = self.oxy.join("checkpoints");
38
        let mut out = Vec::new();
39
        if let Ok(rd) = fs::read_dir(&dir) {
40
            for e in rd.flatten() {
41
                if let Ok(h) = fs::read_to_string(e.path()) {
42
                    if let Some(r) = ObjectRef::from_hex(h.trim()) {
43
                        out.push(r);
44
                    }
45
                }
46
            }
47
        }
48
        Ok(out)
49
    }
50
51
    /// Write a checkpoint of `state` (whose heads are `heads`) for `bookmark`.
75167672 Andy McCall 52
    pub(crate) fn write_checkpoint(
53
        &self,
54
        bookmark: &str,
55
        state: &State,
56
        heads: &[ChangeHash],
57
    ) -> Result<(ObjectRef, usize)> {
8f10a07a Andy McCall ↶ 58
        let batch = self.store.begin_batch()?;
afaba5f1 Andy McCall ↶ 59
        let (blob, payload_len) = self.store_checkpoint_payload(state)?;
75167672 Andy McCall 60
        let cp = Checkpoint {
61
            heads: StateObject::new(heads.to_vec()).heads,
62
            count: state.len() as u64,
63
            payload: blob,
64
        };
252e8cb2 Andy McCall ↶ 65
        let r = self.store.put(ObjectType::Checkpoint, &cp.encode())?;
8f10a07a Andy McCall ↶ 66
        batch.finish()?;
252e8cb2 Andy McCall ↶ 67
        fs::create_dir_all(self.oxy.join("checkpoints"))?;
68
        write_atomic(&self.checkpoint_pointer(bookmark), r.to_hex().as_bytes())?;
afaba5f1 Andy McCall ↶ 69
        Ok((r, payload_len))
252e8cb2 Andy McCall ↶ 70
    }
71
afaba5f1 Andy McCall ↶ 72
    /// Store `state` as a version 2 payload (spec §11): the graphs of large
73
    /// files as blobs of their own, shared with earlier checkpoints when
74
    /// unchanged, and the index naming them. Returns the index blob and its size.
75
    pub(crate) fn store_checkpoint_payload(&self, state: &State) -> Result<(ObjectRef, usize)> {
76
        let graphs = state.checkpoint_file_graphs();
77
        for (_, enc) in &graphs {
78
            if enc.len() >= crate::state::INLINE_GRAPH_LIMIT {
79
                self.store_blob(enc)?;
80
            }
81
        }
82
        let index = state.checkpoint_index(&graphs, Repo::blob_ref);
83
        Ok((self.store_blob(&index)?, index.len()))
84
    }
85
86
    /// The graph blobs a checkpoint's payload names (none for version 1).
87
    pub(crate) fn checkpoint_graph_blobs(&self, cp: &Checkpoint) -> Result<Vec<ObjectRef>> {
88
        let payload = self.read_blob(&cp.payload)?;
89
        Ok(State::checkpoint_graph_refs(&payload)?)
90
    }
91
92
    /// Decode a checkpoint's payload, reading its graph blobs from the store.
93
    pub(crate) fn state_from_checkpoint(&self, payload: &[u8]) -> Result<State> {
94
        Ok(State::from_checkpoint(payload, |refs| {
95
            self.read_blobs(refs).map_err(|e| {
96
                crate::objects::ObjectError::Schema(format!("checkpoint graph blobs: {e}"))
97
            })
98
        })?)
99
    }
100
252e8cb2 Andy McCall ↶ 101
    /// Checkpoint the current bookmark's state now.
102
    pub fn checkpoint(&self) -> Result<(ObjectRef, usize, usize)> {
103
        let heads = self.bookmark_heads()?;
104
        let state = self.load_state_full(&heads)?;
105
        let (r, bytes) = self.write_checkpoint(&self.config.bookmark, &state, &heads)?;
106
        Ok((r, state.len(), bytes))
107
    }
108
2863456a Andy McCall ↶ 109
    /// Point `bookmark` at an already stored checkpoint (a shallow clone's base).
110
    pub(crate) fn install_checkpoint(&self, bookmark: &str, r: &ObjectRef) -> Result<()> {
111
        Checkpoint::decode(&self.store.get_typed(r, ObjectType::Checkpoint)?)?;
112
        fs::create_dir_all(self.oxy.join("checkpoints"))?;
113
        write_atomic(&self.checkpoint_pointer(bookmark), r.to_hex().as_bytes())
114
    }
115
80b069fa Andy McCall ↶ 116
    /// The checkpoint of exactly `heads`: the bookmark's latest when it
117
    /// is that state, else one made now (kept as the bookmark's when the
118
    /// bookmark is at `heads`).
119
    fn checkpoint_ref_for(&self, heads: &[ChangeHash]) -> Result<ObjectRef> {
120
        let _t = crate::timing::phase("checkpoint: find or make");
2863456a Andy McCall ↶ 121
        let wanted = StateObject::new(heads.to_vec()).heads;
122
        let bookmark = self.config.bookmark.clone();
123
        let current = self.bookmark_heads()?;
80b069fa Andy McCall ↶ 124
        Ok(match self.latest_checkpoint(&bookmark)? {
2863456a Andy McCall ↶ 125
            Some((r, cp)) if cp.heads == wanted => r,
126
            _ => {
127
                let state = self.load_state(&wanted)?;
128
                if current == wanted {
129
                    self.write_checkpoint(&bookmark, &state, &wanted)?.0
130
                } else {
afaba5f1 Andy McCall ↶ 131
                    let (blob, _) = self.store_checkpoint_payload(&state)?;
75167672 Andy McCall 132
                    let cp = Checkpoint {
133
                        heads: wanted.clone(),
134
                        count: state.len() as u64,
135
                        payload: blob,
136
                    };
2863456a Andy McCall ↶ 137
                    self.store.put(ObjectType::Checkpoint, &cp.encode())?
138
                }
139
            }
80b069fa Andy McCall ↶ 140
        })
141
    }
142
143
    /// A checkpoint of exactly `heads` without its graph blobs (spec
144
    /// §16.1 `checkpoint-index`, oxygen#137): the checkpoint object, its
145
    /// payload blob and the blob's chunks. The client asks for the graph
146
    /// blobs the payload names in parts, with `objects`, so neither side
147
    /// holds a big state's whole answer at once.
148
    pub fn checkpoint_index_for(&self, heads: &[ChangeHash]) -> Result<Bundle> {
149
        let r = self.checkpoint_ref_for(heads)?;
2863456a Andy McCall ↶ 150
        let cp = Checkpoint::decode(&self.store.get_typed(&r, ObjectType::Checkpoint)?)?;
151
        let mut bundle = Bundle::default();
80b069fa Andy McCall ↶ 152
        let chunks = Blob::decode(&self.store.get_typed(&cp.payload, ObjectType::Blob)?)?.chunks;
153
        for got in self.store.get_many(&chunks) {
154
            bundle.objects.push(got?);
2863456a Andy McCall ↶ 155
        }
156
        bundle.objects.push(self.store.get(&cp.payload)?);
80b069fa Andy McCall ↶ 157
        bundle.objects.push((ObjectType::Checkpoint, cp.encode()));
158
        Ok(bundle)
159
    }
160
161
    /// A checkpoint of exactly `heads` with its graph blobs and their
162
    /// chunks, in one answer (spec §16.1 `checkpoint`): what a client
163
    /// older than `checkpoint-index` asks for.
164
    pub fn checkpoint_bundle_for(&self, heads: &[ChangeHash]) -> Result<Bundle> {
165
        let mut bundle = self.checkpoint_index_for(heads)?;
166
        let _t = crate::timing::phase("checkpoint: gather its objects");
167
        let cp = match bundle.objects.last() {
168
            Some((ObjectType::Checkpoint, body)) => Checkpoint::decode(body)?,
169
            _ => return fail("a checkpoint index without its checkpoint"),
170
        };
f3811eb2 Andy McCall ↶ 171
        // The graph blobs and their chunks in two parallel reads (#137): a
172
        // big state's are tens of thousands, and one by one they took six
173
        // minutes for VS Code.
174
        let graphs = self.checkpoint_graph_blobs(&cp)?;
175
        let mut blobs = Vec::with_capacity(graphs.len());
176
        for got in self.store.get_many(&graphs) {
177
            blobs.push(got?);
178
        }
179
        let mut chunks = Vec::new();
180
        for (ty, body) in &blobs {
181
            if *ty != ObjectType::Blob {
182
                return fail("a checkpoint graph is not a blob");
afaba5f1 Andy McCall ↶ 183
            }
f3811eb2 Andy McCall ↶ 184
            chunks.extend(Blob::decode(body)?.chunks);
185
        }
80b069fa Andy McCall ↶ 186
        let last = bundle.objects.pop();
f3811eb2 Andy McCall ↶ 187
        for got in self.store.get_many(&chunks) {
188
            bundle.objects.push(got?);
afaba5f1 Andy McCall ↶ 189
        }
f3811eb2 Andy McCall ↶ 190
        bundle.objects.extend(blobs);
80b069fa Andy McCall ↶ 191
        bundle.objects.extend(last);
2863456a Andy McCall ↶ 192
        Ok(bundle)
193
    }
194
252e8cb2 Andy McCall ↶ 195
    /// Load the state of `heads` using a checkpoint when one applies.
196
    pub(crate) fn load_state_checkpointed(&self, heads: &[ChangeHash]) -> Result<Option<State>> {
e8d38e47 Andy McCall ↶ 197
        self.load_state_checkpointed_as(heads, false)
198
    }
199
200
    /// As [`Repo::load_state_checkpointed`]; `history` builds no file graph.
201
    pub(crate) fn load_state_checkpointed_as(
202
        &self,
203
        heads: &[ChangeHash],
204
        history: bool,
205
    ) -> Result<Option<State>> {
252e8cb2 Andy McCall ↶ 206
        let mut bookmarks: Vec<String> = vec![self.config.bookmark.clone()];
207
        if let Ok(rd) = fs::read_dir(self.oxy.join("checkpoints")) {
208
            for e in rd.flatten() {
54cd08a1 Andy McCall ↶ 209
                let name = file_name_to_name(&e.file_name().to_string_lossy());
252e8cb2 Andy McCall ↶ 210
                if !bookmarks.contains(&name) {
211
                    bookmarks.push(name);
212
                }
213
            }
214
        }
215
        for b in bookmarks {
75167672 Andy McCall 216
            let Some((_, cp)) = self.latest_checkpoint(&b)? else {
217
                continue;
218
            };
0bdc3f91 Andy McCall ↶ 219
            let payload = {
220
                let _t = crate::timing::phase("load: read checkpoint");
221
                self.read_blob(&cp.payload)?
222
            };
223
            let base = {
224
                let _t = crate::timing::phase("load: decode checkpoint");
e8d38e47 Andy McCall ↶ 225
                if history {
226
                    State::from_checkpoint_history(&payload)?
227
                } else {
228
                    self.state_from_checkpoint(&payload)?
229
                }
0bdc3f91 Andy McCall ↶ 230
            };
252e8cb2 Andy McCall ↶ 231
            let cp_heads: BTreeSet<ChangeHash> = cp.heads.iter().copied().collect();
232
            let mut found: BTreeSet<ChangeHash> = BTreeSet::new();
75051941 Andy McCall ↶ 233
            let mut delta: Vec<(ChangeHash, Change, Vec<crate::objects::TextRef>)> = Vec::new();
252e8cb2 Andy McCall ↶ 234
            let mut seen: BTreeSet<ChangeHash> = BTreeSet::new();
235
            let mut stack: Vec<ChangeHash> = heads.to_vec();
236
            while let Some(h) = stack.pop() {
237
                if !seen.insert(h) {
238
                    continue;
239
                }
240
                if base.contains(&h) {
241
                    if cp_heads.contains(&h) {
242
                        found.insert(h);
243
                    }
244
                    continue;
245
                }
75051941 Andy McCall ↶ 246
                // For history, the skeleton: text is read only for the
247
                // graphs built later, and a state holding none can be
248
                // indexed (oxygen#93).
249
                let (c, texts) = if history {
250
                    self.store.get_change_lazy(&h)?
251
                } else {
252
                    (self.store.get_change(&h)?, Vec::new())
253
                };
252e8cb2 Andy McCall ↶ 254
                stack.extend(c.deps.iter().copied());
75051941 Andy McCall ↶ 255
                delta.push((h, c, texts));
252e8cb2 Andy McCall ↶ 256
            }
257
            if found.len() != cp_heads.len() {
258
                continue;
259
            }
0bdc3f91 Andy McCall ↶ 260
            let _t = crate::timing::phase("load: apply delta and validate");
252e8cb2 Andy McCall ↶ 261
            let mut state = base;
75051941 Andy McCall ↶ 262
            for (h, c, texts) in delta {
263
                if history {
264
                    state.add_known(h, c, texts);
265
                } else {
266
                    state.add_unchecked(c);
267
                }
252e8cb2 Andy McCall ↶ 268
            }
269
            state.validate()?;
270
            return Ok(Some(state));
271
        }
272
        Ok(None)
273
    }
274
}