use std::sync::Arc;
use std::time::Duration;
use alkstore::{JobHandle, Queue, QueueOpts, Schedule, ScheduleOpts, StopToken, Store};
use alkblobs::Store as BlobStore;
use serde_json::json;
use crate::packgen::PackCache;
use crate::reactor::{
AppliedWatermark, MaintenanceJob, Reactor, MAINTENANCE_LOCK, MAINTENANCE_QUEUE,
};
pub const SWEEP_SCHEDULE: &str = "alkgit-sweep-schedule";
pub const LOCKED_OUT_RETRY_SECS: i64 = 2;
const STOP_GRACE_MS: u64 = 50;
#[derive(Clone, Debug)]
pub struct MaintenanceOpts {
pub sweep_spec: String,
pub reconcile_tick: Duration,
pub worker_poll: Duration,
pub wake_retry_backoff: Duration,
pub lock_ttl_secs: i64,
}
impl Default for MaintenanceOpts {
fn default() -> Self {
MaintenanceOpts {
sweep_spec: "@every 30s".to_owned(),
reconcile_tick: Duration::from_secs(30),
worker_poll: Duration::from_millis(500),
wake_retry_backoff: Duration::from_secs(1),
lock_ttl_secs: 30,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WorkerEvent {
Sweep(SweepVerdict),
PackHousekeeping(usize),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SweepVerdict {
Report(alkblobs::SweepReport),
LockedOut,
Failed(String),
}
pub struct MaintenanceChain {
store: Arc<dyn Store>,
blobs: Arc<BlobStore>,
reactor: Arc<Reactor>,
applied: Arc<AppliedWatermark>,
packs: Option<Arc<PackCache>>,
opts: MaintenanceOpts,
}
impl MaintenanceChain {
pub fn new(
store: Arc<dyn Store>,
blobs: Arc<BlobStore>,
reactor: Arc<Reactor>,
applied: Arc<AppliedWatermark>,
packs: Option<Arc<PackCache>>,
opts: MaintenanceOpts,
) -> Self {
MaintenanceChain {
store,
blobs,
reactor,
applied,
packs,
opts,
}
}
pub fn store(&self) -> &Arc<dyn Store> {
&self.store
}
pub fn blobs(&self) -> &Arc<BlobStore> {
&self.blobs
}
pub fn opts(&self) -> &MaintenanceOpts {
&self.opts
}
pub async fn register_sweep_schedule(&self, spec: &str) -> alkstore::Result<Schedule> {
let payload = json!(MaintenanceJob::Sweep);
self.store
.schedule(
SWEEP_SCHEDULE,
spec,
MAINTENANCE_QUEUE,
payload,
ScheduleOpts::default(),
)
.await
}
pub async fn run(
&self,
worker_id: &str,
stop: StopToken,
mut on_event: impl FnMut(WorkerEvent) + Send,
) -> alkstore::Result<()> {
self.register_sweep_schedule(&self.opts.sweep_spec).await?;
let queue = self
.store
.queue(MAINTENANCE_QUEUE, QueueOpts::default())
.await?;
let chain_stop = StopToken::new();
let (sched_done, sched_rx) = tokio::sync::oneshot::channel();
let sched_store = self.store.clone();
let sched_stop = stop.clone();
let handler = tokio::spawn(async move {
let outcome = loop {
match sched_store.run_schedules(sched_stop.clone()).await {
Err(alkstore::Error::LeadershipLost) => continue,
outcome => break outcome,
}
};
let _ = sched_done.send(outcome);
});
let wake_reactor = self.reactor.clone();
let wake_stop = stop.clone();
let wake_chain_stop = chain_stop.clone();
let backoff = self.opts.wake_retry_backoff;
let wake = tokio::spawn(async move {
loop {
if wake_stop.is_cancelled() || wake_chain_stop.is_cancelled() {
return;
}
let runner = wake_reactor.run_wake_runner(wake_stop.clone());
tokio::pin!(runner);
tokio::select! {
_ = &mut runner => {}
_ = stop_grace(&wake_chain_stop) => return,
}
if wake_stop.is_cancelled() || wake_chain_stop.is_cancelled() {
return;
}
tokio::time::sleep(backoff).await;
}
});
let mut poll = tokio::time::interval(self.opts.worker_poll);
poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut rec_tick = tokio::time::interval(self.opts.reconcile_tick);
rec_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
tokio::pin!(sched_rx);
let outcome = loop {
tokio::select! {
_ = stop_grace(&stop) => break Ok(()),
sched_result = &mut sched_rx => break match sched_result {
Ok(result) => result,
Err(_) => Err(alkstore::Error::database(
"the scheduler runner task failed without reporting".to_owned(),
)),
},
_ = rec_tick.tick() => {
let _ = self.reactor.reconcile(&self.applied).await;
}
_ = poll.tick() => {
let _ = self.drain(&*queue, worker_id, &stop, &mut on_event).await;
}
}
};
chain_stop.cancel();
join_legs(handler, wake).await;
outcome
}
async fn drain(
&self,
queue: &dyn Queue,
worker_id: &str,
stop: &StopToken,
on_event: &mut (impl FnMut(WorkerEvent) + Send),
) -> alkstore::Result<()> {
loop {
if stop.is_cancelled() {
return Ok(());
}
let Some(handle) = queue.claim_one(worker_id).await? else {
return Ok(());
};
self.dispatch(handle, worker_id, stop, on_event).await;
}
}
async fn dispatch(
&self,
handle: Box<dyn JobHandle>,
worker_id: &str,
stop: &StopToken,
on_event: &mut (impl FnMut(WorkerEvent) + Send),
) {
match handle.job().payload_as::<MaintenanceJob>() {
Err(_) => {
let _ = handle
.fail(Some("unparseable maintenance payload".to_owned()))
.await;
}
Ok(MaintenanceJob::PackHousekeeping { .. }) => {
let released = self
.packs
.as_ref()
.map(|packs| packs.release_all())
.unwrap_or(0);
let _ = handle.ack().await;
on_event(WorkerEvent::PackHousekeeping(released));
}
Ok(MaintenanceJob::Sweep) => {
match self
.store
.try_lock(MAINTENANCE_LOCK, worker_id, self.opts.lock_ttl_secs)
.await
{
Ok(Some(lease)) => {
let verdict = self.sweep_under_lock(stop).await;
let _ = lease.release().await;
match &verdict {
None => {}
Some(SweepVerdict::Report(_)) => {
let _ = handle.ack().await;
}
Some(SweepVerdict::Failed(reason)) => {
let _ = handle.retry(Some(reason.clone()), None).await;
}
Some(SweepVerdict::LockedOut) => {
let _ = handle
.retry(
Some("the sweeper lock is held elsewhere".to_owned()),
Some(LOCKED_OUT_RETRY_SECS),
)
.await;
}
}
if let Some(verdict) = verdict {
on_event(WorkerEvent::Sweep(verdict));
}
}
Ok(None) => {
let _ = handle
.retry(
Some("the sweeper lock is held elsewhere".to_owned()),
Some(LOCKED_OUT_RETRY_SECS),
)
.await;
on_event(WorkerEvent::Sweep(SweepVerdict::LockedOut));
}
Err(err) => {
let _ = handle.retry(Some(err.to_string()), None).await;
on_event(WorkerEvent::Sweep(SweepVerdict::Failed(err.to_string())));
}
}
}
}
}
async fn sweep_under_lock(&self, stop: &StopToken) -> Option<SweepVerdict> {
let sweep = self.blobs.sweep();
tokio::pin!(sweep);
tokio::select! {
report = &mut sweep => {
Some(match report {
Ok(report) => SweepVerdict::Report(report),
Err(err) => SweepVerdict::Failed(err.to_string()),
})
}
_ = stop_grace(stop) => None,
}
}
}
async fn stop_grace(stop: &StopToken) {
let mut tick = tokio::time::interval(Duration::from_millis(STOP_GRACE_MS));
loop {
tick.tick().await;
if stop.is_cancelled() {
return;
}
}
}
async fn join_legs(handler: tokio::task::JoinHandle<()>, wake: tokio::task::JoinHandle<()>) {
let handler_abort = handler.abort_handle();
let wake_abort = wake.abort_handle();
let joins = async move {
let _ = handler.await;
let _ = wake.await;
};
if tokio::time::timeout(Duration::from_millis(STOP_GRACE_MS * 20), joins)
.await
.is_err()
{
handler_abort.abort();
wake_abort.abort();
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use alkstore::{JobState, QueueOpts, StopToken, Store, WakeReceiver};
use serde_json::Value;
use alkblobs::Store as BlobStore;
#[cfg(feature = "sqlite")]
use alkgit::GitRegistryStore as _;
use alkgit::{GitPackGen as _, GitRefs, Limits, RefUpdate};
use gix_hash::ObjectId;
use gix_object::Kind;
use crate::odb::{key_to_oid, ObjectFront};
use crate::packgen::{PackCache, PackGen};
use crate::reactor::{AppliedWatermark, MaintenanceJob, Reactor, RegistryEvent};
use crate::refs::InMemoryRefTable;
use crate::registry::{
CachedRegistryProvider, InMemoryRegistry, RegistryRead as _, RegistryWrite,
};
#[cfg(feature = "sqlite")]
use crate::testutil::scratch_dir;
use crate::testutil::{record, wait_until};
use super::{
MaintenanceChain, MaintenanceOpts, SweepVerdict, WorkerEvent, MAINTENANCE_LOCK,
MAINTENANCE_QUEUE,
};
type Seen = Arc<Mutex<Vec<WorkerEvent>>>;
fn saw_sweep_verdict(seen: &Seen, pred: impl Fn(&SweepVerdict) -> bool) -> bool {
seen.lock()
.unwrap()
.iter()
.any(|event| matches!(event, WorkerEvent::Sweep(v) if pred(v)))
}
struct MemFixture {
store: Arc<dyn Store>,
blobs: Arc<BlobStore>,
reactor: Arc<Reactor>,
registry: Arc<InMemoryRegistry>,
provider: Arc<CachedRegistryProvider>,
applied: Arc<AppliedWatermark>,
chain: Arc<MaintenanceChain>,
}
fn mem_fixture(opts: MaintenanceOpts, packs: Option<Arc<PackCache>>) -> MemFixture {
let store: Arc<dyn Store> = Arc::new(alkstore_mem::MemStore::new());
let blobs = Arc::new(BlobStore::mem_only());
let registry = Arc::new(InMemoryRegistry::new());
let provider = Arc::new(CachedRegistryProvider::new());
let reactor = Arc::new(Reactor::new(
store.clone(),
registry.clone() as Arc<dyn RegistryWrite>,
provider.clone(),
));
let applied = AppliedWatermark::new();
let chain = Arc::new(MaintenanceChain::new(
store.clone(),
blobs.clone(),
reactor.clone(),
applied.clone(),
packs,
opts,
));
MemFixture {
store,
blobs,
reactor,
registry,
provider,
applied,
chain,
}
}
struct WiredLiveness {
front: ObjectFront,
refs: Arc<InMemoryRefTable>,
_source: Arc<crate::liveness::RefClosure>,
}
fn wire_liveness(fixture: &MemFixture) -> WiredLiveness {
let front = ObjectFront::new(fixture.blobs.clone());
let refs = Arc::new(InMemoryRefTable::new());
let writes = Arc::new(AtomicU64::new(0));
let source = crate::liveness::RefClosure::new(
front.clone(),
refs.clone() as Arc<dyn crate::liveness::RefRows>,
writes,
);
source.register(fixture.blobs.as_ref()).unwrap();
WiredLiveness {
front,
refs,
_source: source,
}
}
async fn put_object(front: &ObjectFront, kind: Kind, content: &[u8]) -> ObjectId {
let (key, _pin) = front.put(kind, content).await.unwrap();
key_to_oid(&key).expect("the put's key is the family key")
}
async fn create_ref<T: GitRefs + ?Sized>(refs: &T, id: &str, name: &str, oid: ObjectId) {
let outcomes = GitRefs::apply_updates(
refs,
&record(id),
vec![RefUpdate {
name: name.to_owned(),
expected: None,
new: Some(oid),
}],
false,
)
.await
.unwrap();
assert!(outcomes.iter().all(|o| o.result.is_ok()));
}
fn quiet_opts() -> MaintenanceOpts {
MaintenanceOpts {
sweep_spec: "@every 600s".to_owned(),
reconcile_tick: Duration::from_secs(600),
worker_poll: Duration::from_millis(25),
wake_retry_backoff: Duration::from_millis(50),
lock_ttl_secs: 30,
}
}
fn spawn_run(
chain: Arc<MaintenanceChain>,
stop: StopToken,
seen: Seen,
) -> tokio::task::JoinHandle<alkstore::Result<()>> {
tokio::spawn(async move {
chain
.run("worker", stop, move |event| {
seen.lock().unwrap().push(event)
})
.await
})
}
async fn stop_and_join(stop: StopToken, join: tokio::task::JoinHandle<alkstore::Result<()>>) {
stop.cancel();
let outcome = tokio::time::timeout(Duration::from_secs(5), join)
.await
.expect("the run ended inside the deadline")
.expect("the run ended clean");
if let Err(err) = outcome {
panic!("the run ended with an error: {err}");
}
}
async fn wait_object_gone(front: &ObjectFront, oid: &ObjectId) {
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
let mut gone = false;
while tokio::time::Instant::now() < deadline {
gone = front.stat(oid).await.unwrap().is_none();
if gone {
return;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(gone, "the object did not reclaim inside the deadline");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_schedule_fires_and_the_worker_sweeps() {
let fixture = mem_fixture(
MaintenanceOpts {
sweep_spec: "@every 1s".to_owned(),
..quiet_opts()
},
None,
);
let wired = wire_liveness(&fixture);
let covered = put_object(&wired.front, Kind::Blob, b"covered\n").await;
let orphan = put_object(&wired.front, Kind::Blob, b"orphan\n").await;
create_ref(&*wired.refs, "repo", "refs/heads/main", covered).await;
let seen: Seen = Arc::new(Mutex::new(Vec::new()));
let stop = StopToken::new();
let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen.clone());
wait_until(|| {
saw_sweep_verdict(&seen, |verdict| {
matches!(
verdict,
SweepVerdict::Report(report) if report.aborted.is_none()
)
})
})
.await;
wait_object_gone(&wired.front, &orphan).await;
assert!(
wired.front.stat(&covered).await.unwrap().is_some(),
"the covered closure survives the chain's sweep"
);
stop_and_join(stop, runner).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_locked_out_sweep_is_a_skip_and_short_retry() {
let fixture = mem_fixture(quiet_opts(), None);
let wired = wire_liveness(&fixture);
let covered = put_object(&wired.front, Kind::Blob, b"covered\n").await;
let orphan = put_object(&wired.front, Kind::Blob, b"orphan\n").await;
create_ref(&*wired.refs, "repo", "refs/heads/main", covered).await;
let lease = fixture
.store
.try_lock(MAINTENANCE_LOCK, "an-external-holder", 30)
.await
.unwrap()
.expect("the outsider holds the lock");
let queue = fixture
.store
.queue(MAINTENANCE_QUEUE, QueueOpts::default())
.await
.unwrap();
let sweep_id = fixture
.reactor
.enqueue(&MaintenanceJob::Sweep)
.await
.unwrap();
let seen: Seen = Arc::new(Mutex::new(Vec::new()));
let stop = StopToken::new();
let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen.clone());
wait_until(|| saw_sweep_verdict(&seen, |v| matches!(v, SweepVerdict::LockedOut))).await;
let row = queue.get_job(sweep_id).await.unwrap().expect("a live row");
assert_eq!(
row.state,
JobState::Pending,
"the locked-out sweep is a retry — back in pending for the short delay"
);
assert_eq!(
row.last_error, None,
"a retried (live) row carries no error the locked-out skip surfaces"
);
lease.release().await.unwrap();
wait_until(|| {
saw_sweep_verdict(&seen, |v| {
matches!(
v,
SweepVerdict::Report(report) if report.aborted.is_none()
)
})
})
.await;
wait_object_gone(&wired.front, &orphan).await;
assert!(
queue.get_job(sweep_id).await.unwrap().is_none(),
"the sweep job acked away"
);
stop_and_join(stop, runner).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_wake_runner_is_supervised_through_a_closed_source() {
wake_supervision_shape(FaultyListenStore::closed_once(Box::new(
alkstore_mem::MemStore::new(),
)))
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_wake_runner_is_supervised_through_a_listen_failure() {
wake_supervision_shape(FaultyListenStore::database_once(Box::new(
alkstore_mem::MemStore::new(),
)))
.await;
}
async fn wake_supervision_shape(store: Arc<FaultyListenStore>) {
let blobs = Arc::new(BlobStore::mem_only());
let registry = Arc::new(InMemoryRegistry::new());
let provider = Arc::new(CachedRegistryProvider::new());
let reactor = Arc::new(Reactor::new(
store.clone() as Arc<dyn Store>,
registry.clone() as Arc<dyn RegistryWrite>,
provider.clone(),
));
let chain = Arc::new(MaintenanceChain::new(
store.clone() as Arc<dyn Store>,
blobs,
reactor.clone(),
AppliedWatermark::new(),
None,
quiet_opts(),
));
let seen: Seen = Arc::new(Mutex::new(Vec::new()));
let stop = StopToken::new();
let runner = spawn_run(chain, stop.clone(), seen.clone());
tokio::time::sleep(Duration::from_millis(300)).await;
let seq = registry.put(&record("owner/wake")).await.unwrap();
reactor
.announce(&RegistryEvent::Put {
seq,
record: record("owner/wake"),
})
.await;
wait_until(|| provider.resolve("owner/wake").is_some()).await;
assert_eq!(
provider.resolve("owner/wake"),
Some(record("owner/wake")),
"the wake-driven refresh landed after the supervised restart"
);
let listens = store.listens.load(Ordering::Relaxed);
assert!(
listens >= 2,
"the runner restarted — a fresh `listen` re-attached (listens: {listens})"
);
stop_and_join(stop, runner).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_reconcile_tick_drives_the_stale_provider() {
let fixture = mem_fixture(
MaintenanceOpts {
reconcile_tick: Duration::from_millis(100),
..quiet_opts()
},
None,
);
fixture.registry.put(&record("owner/tick")).await.unwrap();
assert_eq!(
fixture.provider.resolve("owner/tick"),
None,
"no announce: the cache is stale until the tick"
);
let seen: Seen = Arc::new(Mutex::new(Vec::new()));
let stop = StopToken::new();
let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen);
wait_until(|| fixture.provider.resolve("owner/tick").is_some()).await;
assert!(
fixture.applied.get() > 0,
"the tick also advanced the applied watermark"
);
stop_and_join(stop, runner).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pack_housekeeping_runs_the_release_path() {
let cache = Arc::new(PackCache::new(2));
let fixture = mem_fixture(quiet_opts(), Some(cache.clone()));
let front = ObjectFront::new(fixture.blobs.clone());
let blob = put_object(&front, Kind::Blob, b"pack me\n").await;
let tree = put_object(&front, Kind::Tree, &tree_bytes(&[("b", &blob)])).await;
let commit = put_object(&front, Kind::Commit, &commit_bytes(&tree, &[])).await;
let packgen = PackGen::through_store_with(front.clone(), cache.clone());
let sink = VecSink::default();
packgen
.generate(
&record("repo"),
vec![commit],
vec![],
Limits::default(),
Box::new(sink.clone()),
)
.await
.unwrap();
assert!(
sink.bytes().starts_with(b"PACK"),
"generation produced a pack (the cache holds it)"
);
fixture
.reactor
.enqueue(&MaintenanceJob::PackHousekeeping {
repo_id: "repo".to_owned(),
})
.await
.unwrap();
let seen: Seen = Arc::new(Mutex::new(Vec::new()));
let stop = StopToken::new();
let runner = spawn_run(fixture.chain.clone(), stop.clone(), seen.clone());
wait_until(|| {
seen.lock()
.unwrap()
.iter()
.any(|event| matches!(event, WorkerEvent::PackHousekeeping(1)))
})
.await;
assert_eq!(
cache.release_all(),
0,
"the job's release path drained the cache"
);
stop_and_join(stop, runner).await;
}
#[cfg(feature = "sqlite")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_chain_reclaims_a_deleted_repos_objects() {
use crate::liveness::{RefClosure, RefRows};
let dir = scratch_dir("maintenance", "deletion");
let db = dir.join("deletion.sqlite3");
let assembly = crate::sqlite::SqliteRegistryAssembly::open(&db)
.await
.unwrap();
let blobs = Arc::new(
BlobStore::kv_only_sqlite(&db, 128 * 1024, Duration::ZERO)
.await
.unwrap(),
);
let front = ObjectFront::new(blobs.clone());
let source = RefClosure::new(
front.clone(),
assembly.refs().clone() as Arc<dyn RefRows>,
assembly.refs_version().clone(),
);
source.register(blobs.as_ref()).unwrap();
let victim = record("owner/victimized");
let survivor = record("owner/surviving");
assembly.backend().put_repo(&victim).await.unwrap();
assembly.backend().put_repo(&survivor).await.unwrap();
let survivor_covered = put_object(&front, Kind::Blob, b"survivor\n").await;
create_ref(
&**assembly.refs(),
"owner/surviving",
"refs/heads/main",
survivor_covered,
)
.await;
let covered = put_object(&front, Kind::Blob, b"victim's only object\n").await;
let orphan = put_object(&front, Kind::Blob, b"victim's stray\n").await;
create_ref(
&**assembly.refs(),
"owner/victimized",
"refs/heads/main",
covered,
)
.await;
assembly
.backend()
.remove_repo("owner/victimized")
.await
.unwrap();
let chain = Arc::new(MaintenanceChain::new(
assembly.store().clone(),
blobs.clone(),
assembly.reactor().clone(),
AppliedWatermark::new(),
None,
quiet_opts(),
));
let queue = assembly
.store()
.queue(MAINTENANCE_QUEUE, QueueOpts::default())
.await
.unwrap();
for _ in 0..3 {
queue
.enqueue(
serde_json::to_value(MaintenanceJob::Sweep).unwrap(),
Default::default(),
)
.await
.unwrap();
}
let seen: Seen = Arc::new(Mutex::new(Vec::new()));
let stop = StopToken::new();
let runner = spawn_run(chain, stop.clone(), seen.clone());
wait_object_gone(&front, &covered).await;
wait_object_gone(&front, &orphan).await;
assert!(
front.stat(&survivor_covered).await.unwrap().is_some(),
"the surviving repo's covered object is untouched"
);
wait_until(|| {
saw_sweep_verdict(&seen, |v| {
matches!(
v,
SweepVerdict::Report(report) if report.aborted.is_none() && report.deleted >= 2
)
})
})
.await;
stop_and_join(stop, runner).await;
drop(queue);
drop(assembly);
drop(blobs);
drop(front);
let _ = std::fs::remove_dir_all(&dir);
}
fn tree_bytes(entries: &[(&str, &ObjectId)]) -> Vec<u8> {
let mut tree = Vec::new();
for (name, oid) in entries {
tree.extend_from_slice(format!("100644 {name}\0").as_bytes());
tree.extend_from_slice(oid.as_bytes());
}
tree
}
fn commit_bytes(tree: &ObjectId, parents: &[&ObjectId]) -> Vec<u8> {
let mut body = format!("tree {tree}\n");
for parent in parents {
body.push_str(&format!("parent {parent}\n"));
}
body.push_str(
"author A <a@example.invalid> 1750000000 +0000\n\
committer A <a@example.invalid> 1750000000 +0000\n\nthe message\n",
);
body.into_bytes()
}
#[derive(Clone, Default)]
struct VecSink(Arc<Mutex<Vec<u8>>>);
impl VecSink {
fn bytes(&self) -> Vec<u8> {
self.0.lock().unwrap().clone()
}
}
impl std::io::Write for VecSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
struct FaultyListenStore {
inner: Box<dyn Store>,
closed_left: AtomicU64,
database_left: AtomicU64,
listens: AtomicU64,
}
impl FaultyListenStore {
fn closed_once(inner: Box<dyn Store>) -> Arc<Self> {
Arc::new(FaultyListenStore {
inner,
closed_left: AtomicU64::new(1),
database_left: AtomicU64::new(0),
listens: AtomicU64::new(0),
})
}
fn database_once(inner: Box<dyn Store>) -> Arc<Self> {
Arc::new(FaultyListenStore {
inner,
closed_left: AtomicU64::new(0),
database_left: AtomicU64::new(1),
listens: AtomicU64::new(0),
})
}
}
struct DeadReceiver;
impl WakeReceiver for DeadReceiver {
fn recv<'a>(&'a mut self) -> alkstore::BoxedFuture<'a, Option<alkstore::Wake>> {
Box::pin(async { None })
}
fn try_recv(&mut self) -> alkstore::Result<Option<alkstore::Wake>> {
Err(alkstore::Error::Closed)
}
fn recv_timeout<'a>(
&'a mut self,
_timeout: Duration,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Option<alkstore::Wake>>> {
Box::pin(async { Err(alkstore::Error::Closed) })
}
}
impl Store for FaultyListenStore {
fn begin_tx(
&self,
) -> alkstore::BoxedFuture<'_, alkstore::Result<Box<dyn alkstore::TxHandle + Send>>>
{
self.inner.begin_tx()
}
fn notify<'a>(
&'a self,
channel: &str,
payload: Value,
) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
self.inner.notify(channel, payload)
}
fn listen<'a>(
&'a self,
channel: &str,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn WakeReceiver>>> {
self.listens.fetch_add(1, Ordering::Relaxed);
let inner = &self.inner;
if self.closed_left.swap(0, Ordering::Relaxed) > 0 {
return Box::pin(
async move { Ok(Box::new(DeadReceiver) as Box<dyn WakeReceiver>) },
);
}
if self.database_left.swap(0, Ordering::Relaxed) > 0 {
return Box::pin(async {
Err(alkstore::Error::database("listen storage failure (probe)"))
});
}
inner.listen(channel)
}
fn stream<'a>(
&'a self,
name: &str,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::StreamHandle>>> {
self.inner.stream(name)
}
fn queue<'a>(
&'a self,
name: &str,
opts: QueueOpts,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Queue>>> {
self.inner.queue(name, opts)
}
fn outbox<'a>(
&'a self,
name: &str,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Outbox>>> {
self.inner.outbox(name)
}
fn try_lock<'a>(
&'a self,
name: &str,
owner: &str,
ttl: i64,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Option<Box<dyn alkstore::Lock>>>> {
self.inner.try_lock(name, owner, ttl)
}
fn schedule<'a>(
&'a self,
name: &str,
spec: &str,
queue: &str,
payload: Value,
opts: alkstore::ScheduleOpts,
) -> alkstore::BoxedFuture<'a, alkstore::Result<alkstore::Schedule>> {
self.inner.schedule(name, spec, queue, payload, opts)
}
fn unschedule<'a>(
&'a self,
name: &str,
) -> alkstore::BoxedFuture<'a, alkstore::Result<bool>> {
self.inner.unschedule(name)
}
fn run_schedules<'a>(
&'a self,
stop: StopToken,
) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
self.inner.run_schedules(stop)
}
}
}