1use std::sync::{Arc, Mutex};
25
26use gunnar_store::git_storage_trait::{
27 Caps, Extent, GcReport, GitOps, GitServe, ObjType, Oid, PackProgress, PackStats, ReachSet,
28 RefCas, RefRow, RefUpdate, Stored, TxId,
29};
30
31use crate::control::{BlobId, ControlStore, DocVersion};
32use crate::txlog::{Kind, Log, Meta, RefEdit};
33
34fn hex(bytes: &[u8]) -> String {
35 hex::encode(bytes)
36}
37
38fn blob_hex(id: &BlobId) -> String {
39 hex(id.as_bytes())
40}
41
42pub struct LoggedGit {
46 inner: Arc<dyn GitServe>,
47 log: Arc<Log>,
48 repo: String,
49 hash: String,
50}
51
52impl LoggedGit {
53 pub fn new(inner: Arc<dyn GitServe>, log: Arc<Log>, repo: impl Into<String>, hash: impl Into<String>) -> Self {
56 Self {
57 inner,
58 log,
59 repo: repo.into(),
60 hash: hash.into(),
61 }
62 }
63
64 pub fn inner(&self) -> &Arc<dyn GitServe> {
66 &self.inner
67 }
68
69 fn log_pack(&self, bytes: &[u8]) -> anyhow::Result<u64> {
70 let meta = Meta::Pack {
71 repo: self.repo.clone(),
72 hash: self.hash.clone(),
73 };
74 Ok(self.log.append(Kind::Pack, &meta, bytes)?)
75 }
76
77 fn log_refs(&self, atomic: bool, edits: Vec<RefEdit>) -> anyhow::Result<u64> {
78 let meta = Meta::Refs {
79 repo: self.repo.clone(),
80 atomic,
81 edits,
82 };
83 Ok(self.log.append(Kind::Refs, &meta, &[])?)
84 }
85}
86
87pub fn edit_of_update(u: &RefUpdate) -> RefEdit {
90 RefEdit {
91 name: u.name.clone(),
92 old: None,
93 new: u.target.clone(),
94 peeled: u.peeled.clone(),
95 symref: u.symref_target.clone(),
96 }
97}
98
99pub fn edit_of_cas(c: &RefCas<'_>) -> RefEdit {
101 RefEdit {
102 name: c.name.clone(),
103 old: Some(c.old.map(hex)),
104 new: c.new.map(hex),
105 peeled: c.peeled.as_deref().map(hex),
106 symref: None,
107 }
108}
109
110impl GitOps for LoggedGit {
111 fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
112 let _m = self.log.mutation();
113 self.log_pack(pack)?;
114 let _m = self.log.mutation();
115 self.log_refs(false, refs.iter().map(edit_of_update).collect())?;
116 self.inner.put(pack, refs)
117 }
118 fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
119 let _m = self.log.mutation();
120 self.log_pack(bytes)?;
121 self.inner.put_pack(bytes)
122 }
123 fn put_pack_observed(&self, bytes: &[u8], progress: &dyn PackProgress) -> anyhow::Result<TxId> {
124 let _m = self.log.mutation();
125 self.log_pack(bytes)?;
126 self.inner.put_pack_observed(bytes, progress)
127 }
128 fn publishes_pack_progress(&self) -> bool {
129 self.inner.publishes_pack_progress()
130 }
131 fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
132 let _m = self.log.mutation();
133 self.log_refs(false, updates.iter().map(edit_of_update).collect())?;
134 self.inner.put_refs(updates)
135 }
136 fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
137 self.inner.get(oid)
138 }
139 fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
140 self.inner.has(oid)
141 }
142 fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
143 self.inner.size(oid)
144 }
145 fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
146 self.inner.extents(oids)
147 }
148 fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
149 self.inner.refs()
150 }
151 fn update_ref(&self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>) -> anyhow::Result<TxId> {
152 let _m = self.log.mutation();
153 self.log_refs(
154 true,
155 vec![RefEdit {
156 name: name.to_string(),
157 old: Some(old.map(hex)),
158 new: new.map(hex),
159 peeled: None,
160 symref: None,
161 }],
162 )?;
163 self.inner.update_ref(name, old, new)
164 }
165 fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
166 let _m = self.log.mutation();
167 self.log_refs(true, edits.iter().map(edit_of_cas).collect())?;
168 self.inner.put_refs_cas(edits)
169 }
170 fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
171 self.inner.reachable(want, have)
172 }
173 fn gc(&self) -> anyhow::Result<GcReport> {
174 self.inner.gc()
177 }
178}
179
180impl GitServe for LoggedGit {
181 fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
182 self.inner.read(oid)
183 }
184 fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
185 self.inner.header(oid)
186 }
187 fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
188 self.inner.sizes(oids)
189 }
190 fn index_now(&self) -> anyhow::Result<usize> {
191 self.inner.index_now()
192 }
193 fn head(&self) -> anyhow::Result<Option<RefRow>> {
194 self.inner.head()
195 }
196 fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
197 let _m = self.log.mutation();
198 self.log_refs(
199 false,
200 vec![RefEdit {
201 name: "HEAD".to_string(),
202 old: None,
203 new: None,
204 peeled: None,
205 symref: Some(target.to_string()),
206 }],
207 )?;
208 self.inner.set_head(target)
209 }
210 fn set_head_projected(&self, target: &str) -> anyhow::Result<TxId> {
211 let _m = self.log.mutation();
212 self.log_refs(
213 false,
214 vec![RefEdit {
215 name: "HEAD".to_string(),
216 old: None,
217 new: None,
218 peeled: None,
219 symref: Some(target.to_string()),
220 }],
221 )?;
222 self.inner.set_head_projected(target)
223 }
224 fn emit_pack(&self, objects: &[Oid<'_>], have: &[Oid<'_>], caps: &Caps, out: &mut dyn std::io::Write) -> anyhow::Result<PackStats> {
225 self.inner.emit_pack(objects, have, caps, out)
226 }
227 fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
228 self.inner.select(want, have)
229 }
230}
231
232pub struct LoggedControlStore {
236 inner: Arc<dyn ControlStore>,
237 log: Arc<Log>,
238}
239
240impl LoggedControlStore {
241 pub fn new(inner: Arc<dyn ControlStore>, log: Arc<Log>) -> Self {
243 Self { inner, log }
244 }
245 pub fn inner(&self) -> &Arc<dyn ControlStore> {
247 &self.inner
248 }
249}
250
251impl ControlStore for LoggedControlStore {
252 fn put_blob(&self, bytes: &[u8]) -> crate::Result<BlobId> {
262 let id = BlobId::of(bytes);
263 let meta = Meta::ControlBlob { id: blob_hex(&id) };
264 let _m = self.log.mutation();
265 self.log.stage(Kind::ControlBlob, &meta, bytes).map_err(log_err)?;
266 self.inner.put_blob_unsynced(bytes)
267 }
268 fn get_blob(&self, id: &BlobId) -> crate::Result<Option<Vec<u8>>> {
269 self.inner.get_blob(id)
270 }
271 fn read_doc(&self, name: &str) -> crate::Result<Option<BlobId>> {
272 self.inner.read_doc(name)
273 }
274 fn commit_doc(&self, name: &str, expected: Option<BlobId>, new: BlobId) -> crate::Result<bool> {
275 let meta = Meta::ControlDoc {
276 name: name.to_string(),
277 expected: expected.as_ref().map(blob_hex),
278 new: blob_hex(&new),
279 };
280 let _m = self.log.mutation();
281 self.log.append(Kind::ControlDoc, &meta, &[]).map_err(log_err)?;
282 self.inner.commit_doc(name, expected, new)
283 }
284 fn history(&self, name: &str) -> crate::Result<Vec<DocVersion>> {
285 self.inner.history(name)
286 }
287}
288
289fn log_err(e: crate::txlog::Error) -> crate::Error {
290 match e {
291 crate::txlog::Error::Released(_) => crate::Error::OffStripe {
298 why: "the transaction log is released for the remount".into(),
299 },
300 crate::txlog::Error::Follower => crate::Error::LogFollows,
303 e => crate::Error::ControlStore(format!("transaction log: {e}")),
304 }
305}
306
307pub struct LoggedSecretStore<S> {
312 inner: S,
313 log: Arc<Log>,
314 create: Mutex<()>,
317}
318
319impl<S: gunnar_store::SecretStore> LoggedSecretStore<S> {
320 pub fn new(inner: S, log: Arc<Log>) -> Self {
322 Self {
323 inner,
324 log,
325 create: Mutex::new(()),
326 }
327 }
328 pub fn inner(&self) -> &S {
330 &self.inner
331 }
332 fn log_secret(&self, key: &str, value: &[u8], deleted: bool) -> gunnar_store::Result<()> {
333 let meta = Meta::Secret {
334 key: key.to_string(),
335 deleted,
336 };
337 self.log
338 .append(Kind::Secret, &meta, value)
339 .map(|_| ())
340 .map_err(|e| gunnar_store::Error::Io(std::io::Error::other(e.to_string())))
341 }
342}
343
344impl<S: gunnar_store::SecretStore> gunnar_store::SecretStore for LoggedSecretStore<S> {
345 fn get(&self, key: &str) -> gunnar_store::Result<Option<Vec<u8>>> {
346 self.inner.get(key)
347 }
348 fn put(&self, key: &str, value: &[u8]) -> gunnar_store::Result<()> {
349 let _m = self.log.mutation();
350 self.log_secret(key, value, false)?;
351 self.inner.put(key, value)
352 }
353 fn delete(&self, key: &str) -> gunnar_store::Result<()> {
354 let _m = self.log.mutation();
355 self.log_secret(key, &[], true)?;
356 self.inner.delete(key)
357 }
358 fn get_or_create(&self, key: &str, mk: &mut dyn FnMut() -> gunnar_store::Result<Vec<u8>>) -> gunnar_store::Result<Vec<u8>> {
359 let _one = self.create.lock().expect("secret create");
360 if let Some(v) = self.inner.get(key)? {
361 return Ok(v);
362 }
363 let value = mk()?;
364 self.put(key, &value)?;
365 Ok(value)
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::txlog::Entry;
373 use gunnar_store::SecretStore as _;
374
375 fn log() -> (tempfile::TempDir, Arc<Log>) {
376 let dir = tempfile::tempdir().unwrap();
377 let log = Arc::new(Log::open(dir.path().join("txlog")).unwrap());
378 (dir, log)
379 }
380
381 fn entries(log: &Log) -> Vec<Entry> {
382 log.range(log.first_seq(), log.head_seq()).unwrap()
383 }
384
385 #[test]
392 fn a_control_write_while_the_log_is_released_is_off_the_stripe_never_a_store_failure() {
393 let (_d, log) = log();
394 let store = LoggedControlStore::new(Arc::new(crate::control::RedbControlStore::in_memory().unwrap()), Arc::clone(&log));
395 log.release().unwrap();
396 let refused = store.put_blob(b"{\"principals\":[]}").unwrap_err();
397 assert!(matches!(refused, crate::Error::OffStripe { .. }), "{refused:?}");
398 let said = refused.to_string();
399 assert!(said.contains("the data set") && said.contains("remount"), "{said}");
400 log.reacquire().unwrap();
401 let id = store.put_blob(b"{\"principals\":[]}").expect("after the remount the write goes through");
402 assert!(store.commit_doc("refs/gunnar/auth", None, id).unwrap());
403 }
404
405 #[test]
406 fn a_control_write_is_a_blob_frame_then_a_doc_frame_before_the_store_sees_it() {
407 let (_d, log) = log();
408 let store = LoggedControlStore::new(Arc::new(crate::control::RedbControlStore::in_memory().unwrap()), Arc::clone(&log));
409 let id = store.put_blob(b"{\"principals\":[]}").unwrap();
410 assert!(store.commit_doc("refs/gunnar/auth", None, id).unwrap());
411 let id2 = store.put_blob(b"{\"principals\":[1]}").unwrap();
413 assert!(!store.commit_doc("refs/gunnar/auth", None, id2).unwrap());
414 let e = entries(&log);
415 assert_eq!(e.iter().map(|e| e.kind).collect::<Vec<_>>(), vec![3, 4, 3, 4]);
416 match e[1].meta().unwrap() {
417 Meta::ControlDoc { name, expected, new } => {
418 assert_eq!(name, "refs/gunnar/auth");
419 assert_eq!(expected, None);
420 assert_eq!(new, blob_hex(&id));
421 }
422 other => panic!("{other:?}"),
423 }
424 assert_eq!(e[0].blob, b"{\"principals\":[]}");
425 }
426
427 #[test]
431 fn a_blob_frame_is_durable_with_the_document_frame_that_publishes_it() {
432 let (d, log) = log();
433 let store = LoggedControlStore::new(Arc::new(crate::control::RedbControlStore::in_memory().unwrap()), Arc::clone(&log));
434 let syncs = log.frame_syncs();
435 let id = store.put_blob(b"{\"principals\":[]}").unwrap();
436 assert_eq!((log.head_seq(), log.unsynced()), (0, 1), "the blob frame waits for its document");
437 assert!(store.commit_doc("refs/gunnar/auth", None, id).unwrap());
438 assert_eq!((log.head_seq(), log.unsynced()), (2, 0), "durable together");
439 assert_eq!(log.frame_syncs() - syncs, 1, "one fsync for the blob and its document");
440 drop(store);
441 drop(log);
442 let reopened = Log::open(d.path().join("txlog")).unwrap();
443 let kinds: Vec<u16> = (1..=reopened.head_seq()).map(|s| reopened.read(s).unwrap().kind).collect();
444 assert_eq!(kinds, vec![Kind::ControlBlob as u16, Kind::ControlDoc as u16]);
445 }
446
447 #[test]
448 fn a_secret_put_and_delete_are_frames_and_get_or_create_logs_once() {
449 let (_d, log) = log();
450 let store = LoggedSecretStore::new(gunnar_store::mem::MemSecretStore::new(), Arc::clone(&log));
451 store.put("host_key", b"sealed").unwrap();
452 let v = store.get_or_create("host_key", &mut || Ok(b"other".to_vec())).unwrap();
453 assert_eq!(v, b"sealed");
454 let c = store.get_or_create("issuer", &mut || Ok(b"minted".to_vec())).unwrap();
455 assert_eq!(c, b"minted");
456 store.delete("host_key").unwrap();
457 let e = entries(&log);
458 assert_eq!(e.len(), 3);
459 assert_eq!(e[0].blob, b"sealed");
460 assert_eq!(e[1].blob, b"minted");
461 match e[2].meta().unwrap() {
462 Meta::Secret { key, deleted } => {
463 assert_eq!(key, "host_key");
464 assert!(deleted);
465 }
466 other => panic!("{other:?}"),
467 }
468 assert_eq!(store.get("host_key").unwrap(), None);
469 }
470}