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(
56 inner: Arc<dyn GitServe>,
57 log: Arc<Log>,
58 repo: impl Into<String>,
59 hash: impl Into<String>,
60 ) -> Self {
61 Self {
62 inner,
63 log,
64 repo: repo.into(),
65 hash: hash.into(),
66 }
67 }
68
69 pub fn inner(&self) -> &Arc<dyn GitServe> {
71 &self.inner
72 }
73
74 fn log_pack(&self, bytes: &[u8]) -> anyhow::Result<u64> {
75 let meta = Meta::Pack {
76 repo: self.repo.clone(),
77 hash: self.hash.clone(),
78 };
79 Ok(self.log.append(Kind::Pack, &meta, bytes)?)
80 }
81
82 fn log_refs(&self, atomic: bool, edits: Vec<RefEdit>) -> anyhow::Result<u64> {
83 let meta = Meta::Refs {
84 repo: self.repo.clone(),
85 atomic,
86 edits,
87 };
88 Ok(self.log.append(Kind::Refs, &meta, &[])?)
89 }
90}
91
92pub fn edit_of_update(u: &RefUpdate) -> RefEdit {
95 RefEdit {
96 name: u.name.clone(),
97 old: None,
98 new: u.target.clone(),
99 peeled: u.peeled.clone(),
100 symref: u.symref_target.clone(),
101 }
102}
103
104pub fn edit_of_cas(c: &RefCas<'_>) -> RefEdit {
106 RefEdit {
107 name: c.name.clone(),
108 old: Some(c.old.map(hex)),
109 new: c.new.map(hex),
110 peeled: c.peeled.as_deref().map(hex),
111 symref: None,
112 }
113}
114
115impl GitOps for LoggedGit {
116 fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
117 let _m = self.log.mutation();
118 self.log_pack(pack)?;
119 let _m = self.log.mutation();
120 self.log_refs(false, refs.iter().map(edit_of_update).collect())?;
121 self.inner.put(pack, refs)
122 }
123 fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
124 let _m = self.log.mutation();
125 self.log_pack(bytes)?;
126 self.inner.put_pack(bytes)
127 }
128 fn put_pack_observed(&self, bytes: &[u8], progress: &dyn PackProgress) -> anyhow::Result<TxId> {
129 let _m = self.log.mutation();
130 self.log_pack(bytes)?;
131 self.inner.put_pack_observed(bytes, progress)
132 }
133 fn publishes_pack_progress(&self) -> bool {
134 self.inner.publishes_pack_progress()
135 }
136 fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
137 let _m = self.log.mutation();
138 self.log_refs(false, updates.iter().map(edit_of_update).collect())?;
139 self.inner.put_refs(updates)
140 }
141 fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
142 self.inner.get(oid)
143 }
144 fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
145 self.inner.has(oid)
146 }
147 fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
148 self.inner.size(oid)
149 }
150 fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
151 self.inner.extents(oids)
152 }
153 fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
154 self.inner.refs()
155 }
156 fn update_ref(
157 &self,
158 name: &str,
159 old: Option<Oid<'_>>,
160 new: Option<Oid<'_>>,
161 ) -> anyhow::Result<TxId> {
162 let _m = self.log.mutation();
163 self.log_refs(
164 true,
165 vec![RefEdit {
166 name: name.to_string(),
167 old: Some(old.map(hex)),
168 new: new.map(hex),
169 peeled: None,
170 symref: None,
171 }],
172 )?;
173 self.inner.update_ref(name, old, new)
174 }
175 fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
176 let _m = self.log.mutation();
177 self.log_refs(true, edits.iter().map(edit_of_cas).collect())?;
178 self.inner.put_refs_cas(edits)
179 }
180 fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
181 self.inner.reachable(want, have)
182 }
183 fn gc(&self) -> anyhow::Result<GcReport> {
184 self.inner.gc()
187 }
188}
189
190impl GitServe for LoggedGit {
191 fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
192 self.inner.read(oid)
193 }
194 fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
195 self.inner.header(oid)
196 }
197 fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
198 self.inner.sizes(oids)
199 }
200 fn index_now(&self) -> anyhow::Result<usize> {
201 self.inner.index_now()
202 }
203 fn head(&self) -> anyhow::Result<Option<RefRow>> {
204 self.inner.head()
205 }
206 fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
207 let _m = self.log.mutation();
208 self.log_refs(
209 false,
210 vec![RefEdit {
211 name: "HEAD".to_string(),
212 old: None,
213 new: None,
214 peeled: None,
215 symref: Some(target.to_string()),
216 }],
217 )?;
218 self.inner.set_head(target)
219 }
220 fn set_head_projected(&self, target: &str) -> anyhow::Result<TxId> {
221 let _m = self.log.mutation();
222 self.log_refs(
223 false,
224 vec![RefEdit {
225 name: "HEAD".to_string(),
226 old: None,
227 new: None,
228 peeled: None,
229 symref: Some(target.to_string()),
230 }],
231 )?;
232 self.inner.set_head_projected(target)
233 }
234 fn emit_pack(
235 &self,
236 objects: &[Oid<'_>],
237 have: &[Oid<'_>],
238 caps: &Caps,
239 out: &mut dyn std::io::Write,
240 ) -> anyhow::Result<PackStats> {
241 self.inner.emit_pack(objects, have, caps, out)
242 }
243 fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
244 self.inner.select(want, have)
245 }
246}
247
248pub struct LoggedControlStore {
252 inner: Arc<dyn ControlStore>,
253 log: Arc<Log>,
254}
255
256impl LoggedControlStore {
257 pub fn new(inner: Arc<dyn ControlStore>, log: Arc<Log>) -> Self {
259 Self { inner, log }
260 }
261 pub fn inner(&self) -> &Arc<dyn ControlStore> {
263 &self.inner
264 }
265}
266
267impl ControlStore for LoggedControlStore {
268 fn put_blob(&self, bytes: &[u8]) -> crate::Result<BlobId> {
278 let id = BlobId::of(bytes);
279 let meta = Meta::ControlBlob { id: blob_hex(&id) };
280 let _m = self.log.mutation();
281 self.log
282 .stage(Kind::ControlBlob, &meta, bytes)
283 .map_err(log_err)?;
284 self.inner.put_blob_unsynced(bytes)
285 }
286 fn get_blob(&self, id: &BlobId) -> crate::Result<Option<Vec<u8>>> {
287 self.inner.get_blob(id)
288 }
289 fn read_doc(&self, name: &str) -> crate::Result<Option<BlobId>> {
290 self.inner.read_doc(name)
291 }
292 fn commit_doc(&self, name: &str, expected: Option<BlobId>, new: BlobId) -> crate::Result<bool> {
293 let meta = Meta::ControlDoc {
294 name: name.to_string(),
295 expected: expected.as_ref().map(blob_hex),
296 new: blob_hex(&new),
297 };
298 let _m = self.log.mutation();
299 self.log
300 .append(Kind::ControlDoc, &meta, &[])
301 .map_err(log_err)?;
302 self.inner.commit_doc(name, expected, new)
303 }
304 fn history(&self, name: &str) -> crate::Result<Vec<DocVersion>> {
305 self.inner.history(name)
306 }
307}
308
309fn log_err(e: crate::txlog::Error) -> crate::Error {
310 match e {
311 crate::txlog::Error::Released(_) => crate::Error::OffStripe {
318 why: "the transaction log is released for the remount".into(),
319 },
320 crate::txlog::Error::Follower => crate::Error::LogFollows,
323 e => crate::Error::ControlStore(format!("transaction log: {e}")),
324 }
325}
326
327pub struct LoggedSecretStore<S> {
332 inner: S,
333 log: Arc<Log>,
334 create: Mutex<()>,
337}
338
339impl<S: gunnar_store::SecretStore> LoggedSecretStore<S> {
340 pub fn new(inner: S, log: Arc<Log>) -> Self {
342 Self {
343 inner,
344 log,
345 create: Mutex::new(()),
346 }
347 }
348 pub fn inner(&self) -> &S {
350 &self.inner
351 }
352 fn log_secret(&self, key: &str, value: &[u8], deleted: bool) -> gunnar_store::Result<()> {
353 let meta = Meta::Secret {
354 key: key.to_string(),
355 deleted,
356 };
357 self.log
358 .append(Kind::Secret, &meta, value)
359 .map(|_| ())
360 .map_err(|e| gunnar_store::Error::Io(std::io::Error::other(e.to_string())))
361 }
362}
363
364impl<S: gunnar_store::SecretStore> gunnar_store::SecretStore for LoggedSecretStore<S> {
365 fn get(&self, key: &str) -> gunnar_store::Result<Option<Vec<u8>>> {
366 self.inner.get(key)
367 }
368 fn put(&self, key: &str, value: &[u8]) -> gunnar_store::Result<()> {
369 let _m = self.log.mutation();
370 self.log_secret(key, value, false)?;
371 self.inner.put(key, value)
372 }
373 fn delete(&self, key: &str) -> gunnar_store::Result<()> {
374 let _m = self.log.mutation();
375 self.log_secret(key, &[], true)?;
376 self.inner.delete(key)
377 }
378 fn get_or_create(
379 &self,
380 key: &str,
381 mk: &mut dyn FnMut() -> gunnar_store::Result<Vec<u8>>,
382 ) -> gunnar_store::Result<Vec<u8>> {
383 let _one = self.create.lock().expect("secret create");
384 if let Some(v) = self.inner.get(key)? {
385 return Ok(v);
386 }
387 let value = mk()?;
388 self.put(key, &value)?;
389 Ok(value)
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::txlog::Entry;
397 use gunnar_store::SecretStore as _;
398
399 fn log() -> (tempfile::TempDir, Arc<Log>) {
400 let dir = tempfile::tempdir().unwrap();
401 let log = Arc::new(Log::open(dir.path().join("txlog")).unwrap());
402 (dir, log)
403 }
404
405 fn entries(log: &Log) -> Vec<Entry> {
406 log.range(log.first_seq(), log.head_seq()).unwrap()
407 }
408
409 #[test]
416 fn a_control_write_while_the_log_is_released_is_off_the_stripe_never_a_store_failure() {
417 let (_d, log) = log();
418 let store = LoggedControlStore::new(
419 Arc::new(crate::control::RedbControlStore::in_memory().unwrap()),
420 Arc::clone(&log),
421 );
422 log.release().unwrap();
423 let refused = store.put_blob(b"{\"principals\":[]}").unwrap_err();
424 assert!(
425 matches!(refused, crate::Error::OffStripe { .. }),
426 "{refused:?}"
427 );
428 let said = refused.to_string();
429 assert!(
430 said.contains("the data set") && said.contains("remount"),
431 "{said}"
432 );
433 log.reacquire().unwrap();
434 let id = store
435 .put_blob(b"{\"principals\":[]}")
436 .expect("after the remount the write goes through");
437 assert!(store.commit_doc("refs/gunnar/auth", None, id).unwrap());
438 }
439
440 #[test]
441 fn a_control_write_is_a_blob_frame_then_a_doc_frame_before_the_store_sees_it() {
442 let (_d, log) = log();
443 let store = LoggedControlStore::new(
444 Arc::new(crate::control::RedbControlStore::in_memory().unwrap()),
445 Arc::clone(&log),
446 );
447 let id = store.put_blob(b"{\"principals\":[]}").unwrap();
448 assert!(store.commit_doc("refs/gunnar/auth", None, id).unwrap());
449 let id2 = store.put_blob(b"{\"principals\":[1]}").unwrap();
451 assert!(!store.commit_doc("refs/gunnar/auth", None, id2).unwrap());
452 let e = entries(&log);
453 assert_eq!(
454 e.iter().map(|e| e.kind).collect::<Vec<_>>(),
455 vec![3, 4, 3, 4]
456 );
457 match e[1].meta().unwrap() {
458 Meta::ControlDoc {
459 name,
460 expected,
461 new,
462 } => {
463 assert_eq!(name, "refs/gunnar/auth");
464 assert_eq!(expected, None);
465 assert_eq!(new, blob_hex(&id));
466 }
467 other => panic!("{other:?}"),
468 }
469 assert_eq!(e[0].blob, b"{\"principals\":[]}");
470 }
471
472 #[test]
476 fn a_blob_frame_is_durable_with_the_document_frame_that_publishes_it() {
477 let (d, log) = log();
478 let store = LoggedControlStore::new(
479 Arc::new(crate::control::RedbControlStore::in_memory().unwrap()),
480 Arc::clone(&log),
481 );
482 let syncs = log.frame_syncs();
483 let id = store.put_blob(b"{\"principals\":[]}").unwrap();
484 assert_eq!(
485 (log.head_seq(), log.unsynced()),
486 (0, 1),
487 "the blob frame waits for its document"
488 );
489 assert!(store.commit_doc("refs/gunnar/auth", None, id).unwrap());
490 assert_eq!((log.head_seq(), log.unsynced()), (2, 0), "durable together");
491 assert_eq!(
492 log.frame_syncs() - syncs,
493 1,
494 "one fsync for the blob and its document"
495 );
496 drop(store);
497 drop(log);
498 let reopened = Log::open(d.path().join("txlog")).unwrap();
499 let kinds: Vec<u16> = (1..=reopened.head_seq())
500 .map(|s| reopened.read(s).unwrap().kind)
501 .collect();
502 assert_eq!(
503 kinds,
504 vec![Kind::ControlBlob as u16, Kind::ControlDoc as u16]
505 );
506 }
507
508 #[test]
509 fn a_secret_put_and_delete_are_frames_and_get_or_create_logs_once() {
510 let (_d, log) = log();
511 let store =
512 LoggedSecretStore::new(gunnar_store::mem::MemSecretStore::new(), Arc::clone(&log));
513 store.put("host_key", b"sealed").unwrap();
514 let v = store
515 .get_or_create("host_key", &mut || Ok(b"other".to_vec()))
516 .unwrap();
517 assert_eq!(v, b"sealed");
518 let c = store
519 .get_or_create("issuer", &mut || Ok(b"minted".to_vec()))
520 .unwrap();
521 assert_eq!(c, b"minted");
522 store.delete("host_key").unwrap();
523 let e = entries(&log);
524 assert_eq!(e.len(), 3);
525 assert_eq!(e[0].blob, b"sealed");
526 assert_eq!(e[1].blob, b"minted");
527 match e[2].meta().unwrap() {
528 Meta::Secret { key, deleted } => {
529 assert_eq!(key, "host_key");
530 assert!(deleted);
531 }
532 other => panic!("{other:?}"),
533 }
534 assert_eq!(store.get("host_key").unwrap(), None);
535 }
536}