1use std::sync::Arc;
87use std::time::Duration;
88
89use alkstore::{JobHandle, Queue, QueueOpts, Schedule, ScheduleOpts, StopToken, Store};
90
91use alkblobs::Store as BlobStore;
92use serde_json::json;
93
94use crate::packgen::PackCache;
95use crate::reactor::{
96 AppliedWatermark, MaintenanceJob, Reactor, MAINTENANCE_LOCK, MAINTENANCE_QUEUE,
97};
98
99pub const SWEEP_SCHEDULE: &str = "alkgit-sweep-schedule";
104
105pub const LOCKED_OUT_RETRY_SECS: i64 = 2;
109
110const STOP_GRACE_MS: u64 = 50;
113
114#[derive(Clone, Debug)]
124pub struct MaintenanceOpts {
125 pub sweep_spec: String,
128 pub reconcile_tick: Duration,
132 pub worker_poll: Duration,
134 pub wake_retry_backoff: Duration,
137 pub lock_ttl_secs: i64,
141}
142
143impl Default for MaintenanceOpts {
144 fn default() -> Self {
145 MaintenanceOpts {
146 sweep_spec: "@every 30s".to_owned(),
147 reconcile_tick: Duration::from_secs(30),
148 worker_poll: Duration::from_millis(500),
149 wake_retry_backoff: Duration::from_secs(1),
150 lock_ttl_secs: 30,
151 }
152 }
153}
154
155#[derive(Clone, Debug, PartialEq, Eq)]
160pub enum WorkerEvent {
161 Sweep(SweepVerdict),
163 PackHousekeeping(usize),
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
171pub enum SweepVerdict {
172 Report(alkblobs::SweepReport),
177 LockedOut,
182 Failed(String),
187}
188
189pub struct MaintenanceChain {
211 store: Arc<dyn Store>,
212 blobs: Arc<BlobStore>,
213 reactor: Arc<Reactor>,
214 applied: Arc<AppliedWatermark>,
215 packs: Option<Arc<PackCache>>,
216 opts: MaintenanceOpts,
217}
218
219impl MaintenanceChain {
220 pub fn new(
225 store: Arc<dyn Store>,
226 blobs: Arc<BlobStore>,
227 reactor: Arc<Reactor>,
228 applied: Arc<AppliedWatermark>,
229 packs: Option<Arc<PackCache>>,
230 opts: MaintenanceOpts,
231 ) -> Self {
232 MaintenanceChain {
233 store,
234 blobs,
235 reactor,
236 applied,
237 packs,
238 opts,
239 }
240 }
241
242 pub fn store(&self) -> &Arc<dyn Store> {
245 &self.store
246 }
247
248 pub fn blobs(&self) -> &Arc<BlobStore> {
250 &self.blobs
251 }
252
253 pub fn opts(&self) -> &MaintenanceOpts {
255 &self.opts
256 }
257
258 pub async fn register_sweep_schedule(&self, spec: &str) -> alkstore::Result<Schedule> {
264 let payload = json!(MaintenanceJob::Sweep);
265 self.store
266 .schedule(
267 SWEEP_SCHEDULE,
268 spec,
269 MAINTENANCE_QUEUE,
270 payload,
271 ScheduleOpts::default(),
272 )
273 .await
274 }
275
276 pub async fn run(
307 &self,
308 worker_id: &str,
309 stop: StopToken,
310 mut on_event: impl FnMut(WorkerEvent) + Send,
311 ) -> alkstore::Result<()> {
312 self.register_sweep_schedule(&self.opts.sweep_spec).await?;
313 let queue = self
314 .store
315 .queue(MAINTENANCE_QUEUE, QueueOpts::default())
316 .await?;
317
318 let chain_stop = StopToken::new();
319
320 let (sched_done, sched_rx) = tokio::sync::oneshot::channel();
325 let sched_store = self.store.clone();
326 let sched_stop = stop.clone();
327 let handler = tokio::spawn(async move {
328 let outcome = loop {
329 match sched_store.run_schedules(sched_stop.clone()).await {
330 Err(alkstore::Error::LeadershipLost) => continue,
331 outcome => break outcome,
332 }
333 };
334 let _ = sched_done.send(outcome);
335 });
336
337 let wake_reactor = self.reactor.clone();
338 let wake_stop = stop.clone();
339 let wake_chain_stop = chain_stop.clone();
340 let backoff = self.opts.wake_retry_backoff;
341 let wake = tokio::spawn(async move {
342 loop {
343 if wake_stop.is_cancelled() || wake_chain_stop.is_cancelled() {
344 return;
345 }
346 let runner = wake_reactor.run_wake_runner(wake_stop.clone());
347 tokio::pin!(runner);
348 tokio::select! {
349 _ = &mut runner => {}
350 _ = stop_grace(&wake_chain_stop) => return,
351 }
352 if wake_stop.is_cancelled() || wake_chain_stop.is_cancelled() {
353 return;
354 }
355 tokio::time::sleep(backoff).await;
356 }
357 });
358
359 let mut poll = tokio::time::interval(self.opts.worker_poll);
360 poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
361 let mut rec_tick = tokio::time::interval(self.opts.reconcile_tick);
362 rec_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
363 tokio::pin!(sched_rx);
364
365 let outcome = loop {
366 tokio::select! {
367 _ = stop_grace(&stop) => break Ok(()),
368 sched_result = &mut sched_rx => break match sched_result {
369 Ok(result) => result,
370 Err(_) => Err(alkstore::Error::database(
371 "the scheduler runner task failed without reporting".to_owned(),
372 )),
373 },
374 _ = rec_tick.tick() => {
375 let _ = self.reactor.reconcile(&self.applied).await;
376 }
377 _ = poll.tick() => {
378 let _ = self.drain(&*queue, worker_id, &stop, &mut on_event).await;
379 }
380 }
381 };
382
383 chain_stop.cancel();
392 join_legs(handler, wake).await;
393 outcome
394 }
395
396 async fn drain(
404 &self,
405 queue: &dyn Queue,
406 worker_id: &str,
407 stop: &StopToken,
408 on_event: &mut (impl FnMut(WorkerEvent) + Send),
409 ) -> alkstore::Result<()> {
410 loop {
411 if stop.is_cancelled() {
412 return Ok(());
413 }
414 let Some(handle) = queue.claim_one(worker_id).await? else {
415 return Ok(());
416 };
417 self.dispatch(handle, worker_id, stop, on_event).await;
418 }
419 }
420
421 async fn dispatch(
427 &self,
428 handle: Box<dyn JobHandle>,
429 worker_id: &str,
430 stop: &StopToken,
431 on_event: &mut (impl FnMut(WorkerEvent) + Send),
432 ) {
433 match handle.job().payload_as::<MaintenanceJob>() {
434 Err(_) => {
435 let _ = handle
436 .fail(Some("unparseable maintenance payload".to_owned()))
437 .await;
438 }
439 Ok(MaintenanceJob::PackHousekeeping { .. }) => {
440 let released = self
441 .packs
442 .as_ref()
443 .map(|packs| packs.release_all())
444 .unwrap_or(0);
445 let _ = handle.ack().await;
446 on_event(WorkerEvent::PackHousekeeping(released));
447 }
448 Ok(MaintenanceJob::Sweep) => {
449 match self
450 .store
451 .try_lock(MAINTENANCE_LOCK, worker_id, self.opts.lock_ttl_secs)
452 .await
453 {
454 Ok(Some(lease)) => {
455 let verdict = self.sweep_under_lock(stop).await;
456 let _ = lease.release().await;
457 match &verdict {
458 None => {}
459 Some(SweepVerdict::Report(_)) => {
460 let _ = handle.ack().await;
461 }
462 Some(SweepVerdict::Failed(reason)) => {
463 let _ = handle.retry(Some(reason.clone()), None).await;
464 }
465 Some(SweepVerdict::LockedOut) => {
466 let _ = handle
467 .retry(
468 Some("the sweeper lock is held elsewhere".to_owned()),
469 Some(LOCKED_OUT_RETRY_SECS),
470 )
471 .await;
472 }
473 }
474 if let Some(verdict) = verdict {
475 on_event(WorkerEvent::Sweep(verdict));
476 }
477 }
478 Ok(None) => {
479 let _ = handle
480 .retry(
481 Some("the sweeper lock is held elsewhere".to_owned()),
482 Some(LOCKED_OUT_RETRY_SECS),
483 )
484 .await;
485 on_event(WorkerEvent::Sweep(SweepVerdict::LockedOut));
486 }
487 Err(err) => {
488 let _ = handle.retry(Some(err.to_string()), None).await;
489 on_event(WorkerEvent::Sweep(SweepVerdict::Failed(err.to_string())));
490 }
491 }
492 }
493 }
494 }
495
496 async fn sweep_under_lock(&self, stop: &StopToken) -> Option<SweepVerdict> {
503 let sweep = self.blobs.sweep();
504 tokio::pin!(sweep);
505 tokio::select! {
506 report = &mut sweep => {
507 Some(match report {
508 Ok(report) => SweepVerdict::Report(report),
509 Err(err) => SweepVerdict::Failed(err.to_string()),
510 })
511 }
512 _ = stop_grace(stop) => None,
513 }
514 }
515}
516
517async fn stop_grace(stop: &StopToken) {
521 let mut tick = tokio::time::interval(Duration::from_millis(STOP_GRACE_MS));
522 loop {
523 tick.tick().await;
524 if stop.is_cancelled() {
525 return;
526 }
527 }
528}
529async fn join_legs(handler: tokio::task::JoinHandle<()>, wake: tokio::task::JoinHandle<()>) {
533 let handler_abort = handler.abort_handle();
534 let wake_abort = wake.abort_handle();
535 let joins = async move {
536 let _ = handler.await;
537 let _ = wake.await;
538 };
539 if tokio::time::timeout(Duration::from_millis(STOP_GRACE_MS * 20), joins)
540 .await
541 .is_err()
542 {
543 handler_abort.abort();
544 wake_abort.abort();
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use std::sync::atomic::{AtomicU64, Ordering};
551 use std::sync::{Arc, Mutex};
552 use std::time::Duration;
553
554 use alkstore::{JobState, QueueOpts, StopToken, Store, WakeReceiver};
555 use serde_json::Value;
556
557 use alkblobs::Store as BlobStore;
558 #[cfg(feature = "sqlite")]
559 use alkgit::GitRegistryStore as _;
560 use alkgit::{GitPackGen as _, GitRefs, Limits, RefUpdate};
561 use gix_hash::ObjectId;
562 use gix_object::Kind;
563
564 use crate::odb::{key_to_oid, ObjectFront};
565 use crate::packgen::{PackCache, PackGen};
566 use crate::reactor::{AppliedWatermark, MaintenanceJob, Reactor, RegistryEvent};
567 use crate::refs::InMemoryRefTable;
568 use crate::registry::{
569 CachedRegistryProvider, InMemoryRegistry, RegistryRead as _, RegistryWrite,
570 };
571 #[cfg(feature = "sqlite")]
572 use crate::testutil::scratch_dir;
573 use crate::testutil::{record, wait_until};
574
575 use super::{
576 MaintenanceChain, MaintenanceOpts, SweepVerdict, WorkerEvent, MAINTENANCE_LOCK,
577 MAINTENANCE_QUEUE,
578 };
579
580 type Seen = Arc<Mutex<Vec<WorkerEvent>>>;
581
582 fn saw_sweep_verdict(seen: &Seen, pred: impl Fn(&SweepVerdict) -> bool) -> bool {
583 seen.lock()
584 .unwrap()
585 .iter()
586 .any(|event| matches!(event, WorkerEvent::Sweep(v) if pred(v)))
587 }
588
589 struct MemFixture {
593 store: Arc<dyn Store>,
594 blobs: Arc<BlobStore>,
595 reactor: Arc<Reactor>,
596 registry: Arc<InMemoryRegistry>,
597 provider: Arc<CachedRegistryProvider>,
598 applied: Arc<AppliedWatermark>,
599 chain: Arc<MaintenanceChain>,
600 }
601
602 fn mem_fixture(opts: MaintenanceOpts, packs: Option<Arc<PackCache>>) -> MemFixture {
603 let store: Arc<dyn Store> = Arc::new(alkstore_mem::MemStore::new());
604 let blobs = Arc::new(BlobStore::mem_only());
605 let registry = Arc::new(InMemoryRegistry::new());
606 let provider = Arc::new(CachedRegistryProvider::new());
607 let reactor = Arc::new(Reactor::new(
608 store.clone(),
609 registry.clone() as Arc<dyn RegistryWrite>,
610 provider.clone(),
611 ));
612 let applied = AppliedWatermark::new();
613 let chain = Arc::new(MaintenanceChain::new(
614 store.clone(),
615 blobs.clone(),
616 reactor.clone(),
617 applied.clone(),
618 packs,
619 opts,
620 ));
621 MemFixture {
622 store,
623 blobs,
624 reactor,
625 registry,
626 provider,
627 applied,
628 chain,
629 }
630 }
631
632 struct WiredLiveness {
637 front: ObjectFront,
638 refs: Arc<InMemoryRefTable>,
639 _source: Arc<crate::liveness::RefClosure>,
640 }
641
642 fn wire_liveness(fixture: &MemFixture) -> WiredLiveness {
643 let front = ObjectFront::new(fixture.blobs.clone());
644 let refs = Arc::new(InMemoryRefTable::new());
645 let writes = Arc::new(AtomicU64::new(0));
646 let source = crate::liveness::RefClosure::new(
647 front.clone(),
648 refs.clone() as Arc<dyn crate::liveness::RefRows>,
649 writes,
650 );
651 source.register(fixture.blobs.as_ref()).unwrap();
652 WiredLiveness {
653 front,
654 refs,
655 _source: source,
656 }
657 }
658
659 async fn put_object(front: &ObjectFront, kind: Kind, content: &[u8]) -> ObjectId {
660 let (key, _pin) = front.put(kind, content).await.unwrap();
661 key_to_oid(&key).expect("the put's key is the family key")
662 }
663
664 async fn create_ref<T: GitRefs + ?Sized>(refs: &T, id: &str, name: &str, oid: ObjectId) {
665 let outcomes = GitRefs::apply_updates(
666 refs,
667 &record(id),
668 vec![RefUpdate {
669 name: name.to_owned(),
670 expected: None,
671 new: Some(oid),
672 }],
673 false,
674 )
675 .await
676 .unwrap();
677 assert!(outcomes.iter().all(|o| o.result.is_ok()));
678 }
679
680 fn quiet_opts() -> MaintenanceOpts {
683 MaintenanceOpts {
684 sweep_spec: "@every 600s".to_owned(),
685 reconcile_tick: Duration::from_secs(600),
686 worker_poll: Duration::from_millis(25),
687 wake_retry_backoff: Duration::from_millis(50),
688 lock_ttl_secs: 30,
689 }
690 }
691
692 fn spawn_run(
693 chain: Arc<MaintenanceChain>,
694 stop: StopToken,
695 seen: Seen,
696 ) -> tokio::task::JoinHandle<alkstore::Result<()>> {
697 tokio::spawn(async move {
698 chain
699 .run("worker", stop, move |event| {
700 seen.lock().unwrap().push(event)
701 })
702 .await
703 })
704 }
705
706 async fn stop_and_join(stop: StopToken, join: tokio::task::JoinHandle<alkstore::Result<()>>) {
707 stop.cancel();
708 let outcome = tokio::time::timeout(Duration::from_secs(5), join)
709 .await
710 .expect("the run ended inside the deadline")
711 .expect("the run ended clean");
712 if let Err(err) = outcome {
713 panic!("the run ended with an error: {err}");
714 }
715 }
716
717 async fn wait_object_gone(front: &ObjectFront, oid: &ObjectId) {
720 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
721 let mut gone = false;
722 while tokio::time::Instant::now() < deadline {
723 gone = front.stat(oid).await.unwrap().is_none();
724 if gone {
725 return;
726 }
727 tokio::time::sleep(Duration::from_millis(10)).await;
728 }
729 assert!(gone, "the object did not reclaim inside the deadline");
730 }
731
732 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
739 async fn the_schedule_fires_and_the_worker_sweeps() {
740 let fixture = mem_fixture(
741 MaintenanceOpts {
742 sweep_spec: "@every 1s".to_owned(),
743 ..quiet_opts()
744 },
745 None,
746 );
747 let wired = wire_liveness(&fixture);
748
749 let covered = put_object(&wired.front, Kind::Blob, b"covered\n").await;
750 let orphan = put_object(&wired.front, Kind::Blob, b"orphan\n").await;
751 create_ref(&*wired.refs, "repo", "refs/heads/main", covered).await;
752
753 let seen: Seen = Arc::new(Mutex::new(Vec::new()));
754 let stop = StopToken::new();
755 let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen.clone());
756
757 wait_until(|| {
758 saw_sweep_verdict(&seen, |verdict| {
759 matches!(
760 verdict,
761 SweepVerdict::Report(report) if report.aborted.is_none()
762 )
763 })
764 })
765 .await;
766 wait_object_gone(&wired.front, &orphan).await;
767 assert!(
768 wired.front.stat(&covered).await.unwrap().is_some(),
769 "the covered closure survives the chain's sweep"
770 );
771
772 stop_and_join(stop, runner).await;
773 }
774
775 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
781 async fn a_locked_out_sweep_is_a_skip_and_short_retry() {
782 let fixture = mem_fixture(quiet_opts(), None);
783 let wired = wire_liveness(&fixture);
784
785 let covered = put_object(&wired.front, Kind::Blob, b"covered\n").await;
786 let orphan = put_object(&wired.front, Kind::Blob, b"orphan\n").await;
787 create_ref(&*wired.refs, "repo", "refs/heads/main", covered).await;
788
789 let lease = fixture
790 .store
791 .try_lock(MAINTENANCE_LOCK, "an-external-holder", 30)
792 .await
793 .unwrap()
794 .expect("the outsider holds the lock");
795
796 let queue = fixture
797 .store
798 .queue(MAINTENANCE_QUEUE, QueueOpts::default())
799 .await
800 .unwrap();
801 let sweep_id = fixture
802 .reactor
803 .enqueue(&MaintenanceJob::Sweep)
804 .await
805 .unwrap();
806
807 let seen: Seen = Arc::new(Mutex::new(Vec::new()));
808 let stop = StopToken::new();
809 let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen.clone());
810
811 wait_until(|| saw_sweep_verdict(&seen, |v| matches!(v, SweepVerdict::LockedOut))).await;
812 let row = queue.get_job(sweep_id).await.unwrap().expect("a live row");
813 assert_eq!(
814 row.state,
815 JobState::Pending,
816 "the locked-out sweep is a retry — back in pending for the short delay"
817 );
818 assert_eq!(
819 row.last_error, None,
820 "a retried (live) row carries no error the locked-out skip surfaces"
821 );
822
823 lease.release().await.unwrap();
824 wait_until(|| {
825 saw_sweep_verdict(&seen, |v| {
826 matches!(
827 v,
828 SweepVerdict::Report(report) if report.aborted.is_none()
829 )
830 })
831 })
832 .await;
833 wait_object_gone(&wired.front, &orphan).await;
834 assert!(
835 queue.get_job(sweep_id).await.unwrap().is_none(),
836 "the sweep job acked away"
837 );
838
839 stop_and_join(stop, runner).await;
840 }
841
842 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
848 async fn the_wake_runner_is_supervised_through_a_closed_source() {
849 wake_supervision_shape(FaultyListenStore::closed_once(Box::new(
850 alkstore_mem::MemStore::new(),
851 )))
852 .await;
853 }
854
855 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
859 async fn the_wake_runner_is_supervised_through_a_listen_failure() {
860 wake_supervision_shape(FaultyListenStore::database_once(Box::new(
861 alkstore_mem::MemStore::new(),
862 )))
863 .await;
864 }
865
866 async fn wake_supervision_shape(store: Arc<FaultyListenStore>) {
867 let blobs = Arc::new(BlobStore::mem_only());
868 let registry = Arc::new(InMemoryRegistry::new());
869 let provider = Arc::new(CachedRegistryProvider::new());
870 let reactor = Arc::new(Reactor::new(
871 store.clone() as Arc<dyn Store>,
872 registry.clone() as Arc<dyn RegistryWrite>,
873 provider.clone(),
874 ));
875 let chain = Arc::new(MaintenanceChain::new(
876 store.clone() as Arc<dyn Store>,
877 blobs,
878 reactor.clone(),
879 AppliedWatermark::new(),
880 None,
881 quiet_opts(),
882 ));
883
884 let seen: Seen = Arc::new(Mutex::new(Vec::new()));
885 let stop = StopToken::new();
886 let runner = spawn_run(chain, stop.clone(), seen.clone());
887 tokio::time::sleep(Duration::from_millis(300)).await;
888
889 let seq = registry.put(&record("owner/wake")).await.unwrap();
890 reactor
891 .announce(&RegistryEvent::Put {
892 seq,
893 record: record("owner/wake"),
894 })
895 .await;
896
897 wait_until(|| provider.resolve("owner/wake").is_some()).await;
898 assert_eq!(
899 provider.resolve("owner/wake"),
900 Some(record("owner/wake")),
901 "the wake-driven refresh landed after the supervised restart"
902 );
903 let listens = store.listens.load(Ordering::Relaxed);
904 assert!(
905 listens >= 2,
906 "the runner restarted — a fresh `listen` re-attached (listens: {listens})"
907 );
908
909 stop_and_join(stop, runner).await;
910 }
911
912 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
918 async fn the_reconcile_tick_drives_the_stale_provider() {
919 let fixture = mem_fixture(
920 MaintenanceOpts {
921 reconcile_tick: Duration::from_millis(100),
922 ..quiet_opts()
923 },
924 None,
925 );
926
927 fixture.registry.put(&record("owner/tick")).await.unwrap();
928 assert_eq!(
929 fixture.provider.resolve("owner/tick"),
930 None,
931 "no announce: the cache is stale until the tick"
932 );
933
934 let seen: Seen = Arc::new(Mutex::new(Vec::new()));
935 let stop = StopToken::new();
936 let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen);
937 wait_until(|| fixture.provider.resolve("owner/tick").is_some()).await;
938 assert!(
939 fixture.applied.get() > 0,
940 "the tick also advanced the applied watermark"
941 );
942 stop_and_join(stop, runner).await;
943 }
944
945 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
950 async fn pack_housekeeping_runs_the_release_path() {
951 let cache = Arc::new(PackCache::new(2));
952 let fixture = mem_fixture(quiet_opts(), Some(cache.clone()));
953 let front = ObjectFront::new(fixture.blobs.clone());
954 let blob = put_object(&front, Kind::Blob, b"pack me\n").await;
955 let tree = put_object(&front, Kind::Tree, &tree_bytes(&[("b", &blob)])).await;
956 let commit = put_object(&front, Kind::Commit, &commit_bytes(&tree, &[])).await;
957
958 let packgen = PackGen::through_store_with(front.clone(), cache.clone());
959 let sink = VecSink::default();
960 packgen
961 .generate(
962 &record("repo"),
963 vec![commit],
964 vec![],
965 Limits::default(),
966 Box::new(sink.clone()),
967 )
968 .await
969 .unwrap();
970 assert!(
971 sink.bytes().starts_with(b"PACK"),
972 "generation produced a pack (the cache holds it)"
973 );
974
975 fixture
976 .reactor
977 .enqueue(&MaintenanceJob::PackHousekeeping {
978 repo_id: "repo".to_owned(),
979 })
980 .await
981 .unwrap();
982
983 let seen: Seen = Arc::new(Mutex::new(Vec::new()));
984 let stop = StopToken::new();
985 let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen.clone());
986 wait_until(|| {
987 seen.lock()
988 .unwrap()
989 .iter()
990 .any(|event| matches!(event, WorkerEvent::PackHousekeeping(1)))
991 })
992 .await;
993 assert_eq!(
994 cache.release_all(),
995 0,
996 "the job's release path drained the cache"
997 );
998 stop_and_join(stop, runner).await;
999 }
1000
1001 #[cfg(feature = "sqlite")]
1007 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1008 async fn the_chain_reclaims_a_deleted_repos_objects() {
1009 use crate::liveness::{RefClosure, RefRows};
1010
1011 let dir = scratch_dir("maintenance", "deletion");
1012 let db = dir.join("deletion.sqlite3");
1013 let assembly = crate::sqlite::SqliteRegistryAssembly::open(&db)
1014 .await
1015 .unwrap();
1016 let blobs = Arc::new(
1017 BlobStore::kv_only_sqlite(&db, 128 * 1024, Duration::ZERO)
1018 .await
1019 .unwrap(),
1020 );
1021 let front = ObjectFront::new(blobs.clone());
1022 let source = RefClosure::new(
1023 front.clone(),
1024 assembly.refs().clone() as Arc<dyn RefRows>,
1025 assembly.refs_version().clone(),
1026 );
1027 source.register(blobs.as_ref()).unwrap();
1028
1029 let victim = record("owner/victimized");
1030 let survivor = record("owner/surviving");
1031 assembly.backend().put_repo(&victim).await.unwrap();
1032 assembly.backend().put_repo(&survivor).await.unwrap();
1033
1034 let survivor_covered = put_object(&front, Kind::Blob, b"survivor\n").await;
1035 create_ref(
1036 &**assembly.refs(),
1037 "owner/surviving",
1038 "refs/heads/main",
1039 survivor_covered,
1040 )
1041 .await;
1042
1043 let covered = put_object(&front, Kind::Blob, b"victim's only object\n").await;
1044 let orphan = put_object(&front, Kind::Blob, b"victim's stray\n").await;
1045 create_ref(
1046 &**assembly.refs(),
1047 "owner/victimized",
1048 "refs/heads/main",
1049 covered,
1050 )
1051 .await;
1052
1053 assembly
1054 .backend()
1055 .remove_repo("owner/victimized")
1056 .await
1057 .unwrap();
1058
1059 let chain = Arc::new(MaintenanceChain::new(
1060 assembly.store().clone(),
1061 blobs.clone(),
1062 assembly.reactor().clone(),
1063 AppliedWatermark::new(),
1064 None,
1065 quiet_opts(),
1066 ));
1067 let queue = assembly
1068 .store()
1069 .queue(MAINTENANCE_QUEUE, QueueOpts::default())
1070 .await
1071 .unwrap();
1072 for _ in 0..3 {
1073 queue
1074 .enqueue(
1075 serde_json::to_value(MaintenanceJob::Sweep).unwrap(),
1076 Default::default(),
1077 )
1078 .await
1079 .unwrap();
1080 }
1081
1082 let seen: Seen = Arc::new(Mutex::new(Vec::new()));
1083 let stop = StopToken::new();
1084 let runner = spawn_run(chain, stop.clone(), seen.clone());
1085 wait_object_gone(&front, &covered).await;
1086 wait_object_gone(&front, &orphan).await;
1087 assert!(
1088 front.stat(&survivor_covered).await.unwrap().is_some(),
1089 "the surviving repo's covered object is untouched"
1090 );
1091 wait_until(|| {
1096 saw_sweep_verdict(&seen, |v| {
1097 matches!(
1098 v,
1099 SweepVerdict::Report(report) if report.aborted.is_none() && report.deleted >= 2
1100 )
1101 })
1102 })
1103 .await;
1104
1105 stop_and_join(stop, runner).await;
1106 drop(queue);
1107 drop(assembly);
1108 drop(blobs);
1109 drop(front);
1110 let _ = std::fs::remove_dir_all(&dir);
1111 }
1112
1113 fn tree_bytes(entries: &[(&str, &ObjectId)]) -> Vec<u8> {
1114 let mut tree = Vec::new();
1115 for (name, oid) in entries {
1116 tree.extend_from_slice(format!("100644 {name}\0").as_bytes());
1117 tree.extend_from_slice(oid.as_bytes());
1118 }
1119 tree
1120 }
1121
1122 fn commit_bytes(tree: &ObjectId, parents: &[&ObjectId]) -> Vec<u8> {
1123 let mut body = format!("tree {tree}\n");
1124 for parent in parents {
1125 body.push_str(&format!("parent {parent}\n"));
1126 }
1127 body.push_str(
1128 "author A <a@example.invalid> 1750000000 +0000\n\
1129 committer A <a@example.invalid> 1750000000 +0000\n\nthe message\n",
1130 );
1131 body.into_bytes()
1132 }
1133
1134 #[derive(Clone, Default)]
1135 struct VecSink(Arc<Mutex<Vec<u8>>>);
1136
1137 impl VecSink {
1138 fn bytes(&self) -> Vec<u8> {
1139 self.0.lock().unwrap().clone()
1140 }
1141 }
1142
1143 impl std::io::Write for VecSink {
1144 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1145 self.0.lock().unwrap().extend_from_slice(buf);
1146 Ok(buf.len())
1147 }
1148 fn flush(&mut self) -> std::io::Result<()> {
1149 Ok(())
1150 }
1151 }
1152
1153 struct FaultyListenStore {
1157 inner: Box<dyn Store>,
1158 closed_left: AtomicU64,
1159 database_left: AtomicU64,
1160 listens: AtomicU64,
1161 }
1162
1163 impl FaultyListenStore {
1164 fn closed_once(inner: Box<dyn Store>) -> Arc<Self> {
1165 Arc::new(FaultyListenStore {
1166 inner,
1167 closed_left: AtomicU64::new(1),
1168 database_left: AtomicU64::new(0),
1169 listens: AtomicU64::new(0),
1170 })
1171 }
1172
1173 fn database_once(inner: Box<dyn Store>) -> Arc<Self> {
1174 Arc::new(FaultyListenStore {
1175 inner,
1176 closed_left: AtomicU64::new(0),
1177 database_left: AtomicU64::new(1),
1178 listens: AtomicU64::new(0),
1179 })
1180 }
1181 }
1182
1183 struct DeadReceiver;
1186
1187 impl WakeReceiver for DeadReceiver {
1188 fn recv<'a>(&'a mut self) -> alkstore::BoxedFuture<'a, Option<alkstore::Wake>> {
1189 Box::pin(async { None })
1190 }
1191
1192 fn try_recv(&mut self) -> alkstore::Result<Option<alkstore::Wake>> {
1193 Err(alkstore::Error::Closed)
1194 }
1195
1196 fn recv_timeout<'a>(
1197 &'a mut self,
1198 _timeout: Duration,
1199 ) -> alkstore::BoxedFuture<'a, alkstore::Result<Option<alkstore::Wake>>> {
1200 Box::pin(async { Err(alkstore::Error::Closed) })
1201 }
1202 }
1203
1204 impl Store for FaultyListenStore {
1205 fn begin_tx(
1206 &self,
1207 ) -> alkstore::BoxedFuture<'_, alkstore::Result<Box<dyn alkstore::TxHandle + Send>>>
1208 {
1209 self.inner.begin_tx()
1210 }
1211
1212 fn notify<'a>(
1213 &'a self,
1214 channel: &str,
1215 payload: Value,
1216 ) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
1217 self.inner.notify(channel, payload)
1218 }
1219
1220 fn listen<'a>(
1221 &'a self,
1222 channel: &str,
1223 ) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn WakeReceiver>>> {
1224 self.listens.fetch_add(1, Ordering::Relaxed);
1225 let inner = &self.inner;
1226 if self.closed_left.swap(0, Ordering::Relaxed) > 0 {
1227 return Box::pin(
1228 async move { Ok(Box::new(DeadReceiver) as Box<dyn WakeReceiver>) },
1229 );
1230 }
1231 if self.database_left.swap(0, Ordering::Relaxed) > 0 {
1232 return Box::pin(async {
1233 Err(alkstore::Error::database("listen storage failure (probe)"))
1234 });
1235 }
1236 inner.listen(channel)
1237 }
1238
1239 fn stream<'a>(
1240 &'a self,
1241 name: &str,
1242 ) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::StreamHandle>>> {
1243 self.inner.stream(name)
1244 }
1245
1246 fn queue<'a>(
1247 &'a self,
1248 name: &str,
1249 opts: QueueOpts,
1250 ) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Queue>>> {
1251 self.inner.queue(name, opts)
1252 }
1253
1254 fn outbox<'a>(
1255 &'a self,
1256 name: &str,
1257 ) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Outbox>>> {
1258 self.inner.outbox(name)
1259 }
1260
1261 fn try_lock<'a>(
1262 &'a self,
1263 name: &str,
1264 owner: &str,
1265 ttl: i64,
1266 ) -> alkstore::BoxedFuture<'a, alkstore::Result<Option<Box<dyn alkstore::Lock>>>> {
1267 self.inner.try_lock(name, owner, ttl)
1268 }
1269
1270 fn schedule<'a>(
1271 &'a self,
1272 name: &str,
1273 spec: &str,
1274 queue: &str,
1275 payload: Value,
1276 opts: alkstore::ScheduleOpts,
1277 ) -> alkstore::BoxedFuture<'a, alkstore::Result<alkstore::Schedule>> {
1278 self.inner.schedule(name, spec, queue, payload, opts)
1279 }
1280
1281 fn unschedule<'a>(
1282 &'a self,
1283 name: &str,
1284 ) -> alkstore::BoxedFuture<'a, alkstore::Result<bool>> {
1285 self.inner.unschedule(name)
1286 }
1287
1288 fn run_schedules<'a>(
1289 &'a self,
1290 stop: StopToken,
1291 ) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
1292 self.inner.run_schedules(stop)
1293 }
1294 }
1295}