use std::collections::HashMap;
use flint_store::{crc64_nvme, crc64_to_b64, GenerationStamps, PutCondition, StoreError};
use super::{manifest, now_unix, LeanResult, Syncer};
pub const UNTRACKED_GRACE_SECS: u64 = 600;
pub fn ingress_flush(etag: &str) -> String {
let clean: String = etag.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
format!("ingress-{clean}")
}
impl Syncer {
pub async fn track_untracked(&self, now: u64) -> LeanResult<Vec<String>> {
let loaded = manifest::load(self.store.as_ref(), &self.cfg).await?;
let cited: HashMap<&str, &str> = loaded
.as_ref()
.map(|l| l.manifest.entries.iter().map(|(p, e)| (p.as_str(), e.key.as_str())).collect())
.unwrap_or_default();
let listed = self.store.list(&self.cfg.files_prefix()).await?;
let mut tracked = vec![];
for obj in listed {
if self.cfg.handle_parts(&obj.key).is_some() {
continue;
}
let Some(path) = obj.key.strip_prefix(&self.cfg.files_prefix()) else { continue };
if path.is_empty() || path.ends_with('/') {
continue;
}
let Some(modified) = obj.last_modified_unix else { continue };
if now.saturating_sub(modified) < self.cfg.untracked_grace_secs {
continue;
}
let handle = self.cfg.handle_key(path, &ingress_flush(&obj.etag));
if cited.get(path).is_some_and(|k| *k == handle.as_str()) {
continue;
}
let stamps = GenerationStamps {
generation: 0,
epoch: 0,
flush_uuid: ingress_flush(&obj.etag),
boundary_source: None,
posix: None,
};
let copied = match self
.store
.copy_object(&obj.key, Some(&obj.etag), &handle, &PutCondition::IfNoneMatchAny, &stamps)
.await
{
Ok(m) => m,
Err(StoreError::PreconditionFailed(_)) => match self.store.head(&handle).await {
Ok(m) => m,
Err(StoreError::NotFound(_)) => continue,
Err(e) => return Err(e.into()),
},
Err(StoreError::NotFound(_)) => continue,
Err(e) => return Err(e.into()),
};
let crc64_b64 = match copied.crc64_b64.clone() {
Some(c) => c,
None => {
let (_, bytes) = self.store.get_whole(&handle, Some(&copied.etag)).await?;
crc64_to_b64(crc64_nvme(&bytes))
}
};
let seen = cited.get(path).map(|k| k.to_string());
let committed = manifest::commit_edit(self.store.as_ref(), &self.cfg, &ingress_flush(&obj.etag), |_, doc| {
let was = doc.entries.get(path);
if was.map(|e| e.key.clone()) != seen {
return Err(()); }
let entry = manifest::LeanEntry {
key: handle.clone(),
etag: copied.etag.clone(),
crc64_b64: crc64_b64.clone(),
size: copied.size,
mode: was.map(|e| e.mode).unwrap_or(0o644),
mtime_unix: now_unix() as i64,
generation: was.map(|e| e.generation).unwrap_or(0) + 1,
epoch: 0,
};
doc.tombstones.remove(path);
doc.entries.insert(path.to_string(), entry);
Ok(())
})
.await;
match committed {
Ok(()) => {
self.trace("untracked", serde_json::json!({"path": path, "etag": obj.etag, "handle": handle,
"cited": seen}));
tracked.push(path.to_string());
}
Err(manifest::EditError::Refused(())) | Err(manifest::EditError::Contended) => continue,
Err(manifest::EditError::Store(e)) => return Err(e),
}
}
Ok(tracked)
}
pub async fn sweep_untracked_if_due(&self, now: u64) {
if self.cfg.untracked_sweep_secs == 0 || self.cfg.access.is_read() {
return;
}
let last = match self.state.load_untracked_sweep_at() {
Ok(t) => t,
Err(e) => {
eprintln!("flint-sync: untracked sweep clock unreadable (skipping): {e}");
return;
}
};
if last == 0 {
let _ = self.state.save_untracked_sweep_at(now);
return;
}
if now.saturating_sub(last) < self.cfg.untracked_sweep_secs {
return;
}
match self.track_untracked(now).await {
Ok(paths) => {
if !paths.is_empty() {
eprintln!(
"flint-sync: tracked {} upload(s) no commit cited, left by a lost writer: {paths:?}",
paths.len()
);
}
let _ = self.state.save_untracked_sweep_at(now);
}
Err(e) => eprintln!("flint-sync: untracked sweep failed (retrying next tick): {e}"),
}
}
pub(crate) async fn sweep_orphans_if_due(
&mut self,
installed: &manifest::LeanManifest,
own_flush: &str,
now: u64,
spared: &std::collections::HashSet<String>,
) -> LeanResult<usize> {
if self.cfg.untracked_sweep_secs == 0 {
return Ok(0);
}
let last = self.state.load_orphan_sweep_at()?;
if last == 0 {
self.state.save_orphan_sweep_at(now)?;
return Ok(0);
}
if now.saturating_sub(last) < self.cfg.untracked_sweep_secs {
return Ok(0);
}
let n = self.sweep_orphans(installed, own_flush, now, spared).await?;
self.state.save_orphan_sweep_at(now)?;
Ok(n)
}
pub(crate) async fn sweep_orphans(
&mut self,
installed: &manifest::LeanManifest,
own_flush: &str,
now: u64,
spared: &std::collections::HashSet<String>,
) -> LeanResult<usize> {
let cited: std::collections::HashSet<&str> =
installed.entries.values().map(|e| e.key.as_str()).collect();
let listed = self.store.list(&self.cfg.files_prefix()).await?;
let mut orphans: Vec<String> = vec![];
for obj in listed {
let Some((_, flush)) = self.cfg.handle_parts(&obj.key) else { continue };
if flush == own_flush || cited.contains(obj.key.as_str()) || spared.contains(&obj.key) {
continue;
}
let Some(modified) = obj.last_modified_unix else { continue };
if now.saturating_sub(modified) < self.cfg.untracked_grace_secs {
continue;
}
orphans.push(obj.key);
}
if orphans.is_empty() {
return Ok(0);
}
let report = self.delete_many_renewing(&orphans).await?;
let taken: Vec<&String> = orphans.iter().filter(|k| !report.failed.iter().any(|(f, _)| f == *k)).collect();
self.trace("sweep", serde_json::json!({"what": "orphans", "candidates": orphans.len(),
"removed": report.deleted, "refused": report.failed.len(), "keys": taken}));
if report.deleted > 0 {
eprintln!(
"flint-sync: collected {} orphan handle(s) no commit cited, left by a lost writer",
report.deleted
);
}
Ok(report.deleted)
}
}
#[derive(Default)]
pub(crate) struct RetireLedger {
pub spared: std::collections::HashSet<String>,
pub due: Vec<(String, manifest::RetireLog)>,
}
pub(crate) const DELETE_RENEW_BATCH: usize = 1000;
impl Syncer {
pub(crate) async fn delete_many_renewing(&mut self, keys: &[String]) -> LeanResult<flint_store::DeleteManyReport> {
let mut all = flint_store::DeleteManyReport::default();
for batch in keys.chunks(DELETE_RENEW_BATCH) {
super::lease::renew_if_due(self).await?;
let r = self.store.delete_many(batch).await?;
all.deleted += r.deleted;
all.failed.extend(r.failed);
}
Ok(all)
}
pub(crate) async fn retire_ledger(&self, now: u64) -> LeanResult<RetireLedger> {
let listed = self.store.list(&manifest::retired_prefix(&self.cfg)).await?;
let mut cache = self.state.load_retire_cache();
let present: std::collections::BTreeSet<&str> = listed.iter().map(|o| o.key.as_str()).collect();
cache.retain(|k, _| present.contains(k.as_str()));
let mut ledger = RetireLedger::default();
for o in &listed {
let Some(at) = manifest::retire_log_at(&self.cfg, &o.key) else { continue };
let log = match cache.get(&o.key) {
Some(l) => l.clone(),
None => match self.store.get_whole(&o.key, None).await {
Ok((_, body)) => serde_json::from_slice(&body).unwrap_or_default(),
Err(StoreError::NotFound(_)) => continue,
Err(e) => return Err(e.into()),
},
};
cache.insert(o.key.clone(), log.clone());
if now.saturating_sub(at) >= self.cfg.retire_grace_secs {
ledger.due.push((o.key.clone(), log));
} else {
ledger.spared.extend(log.handles.iter().cloned());
ledger.spared.extend(log.chunks.iter().map(|a| self.cfg.chunk_key(a)));
}
}
self.state.save_retire_cache(&cache)?;
Ok(ledger)
}
pub(crate) async fn reap_retired(
&mut self,
ledger: &RetireLedger,
installed: &manifest::LeanManifest,
own_flush: &str,
) -> LeanResult<usize> {
let cited: std::collections::HashSet<&str> = installed.entries.values().map(|e| e.key.as_str()).collect();
let mut handles: Vec<String> = ledger
.due
.iter()
.flat_map(|(_, l)| l.handles.iter().cloned())
.filter(|k| !cited.contains(k.as_str()) && !ledger.spared.contains(k))
.collect();
handles.sort();
handles.dedup();
let mut collected = 0;
if !handles.is_empty() {
let report = self.delete_many_renewing(&handles).await?;
collected = report.deleted;
let taken: Vec<&String> = handles.iter().filter(|k| !report.failed.iter().any(|(f, _)| f == *k)).collect();
self.trace("sweep", serde_json::json!({"flush": own_flush, "what": "retired", "removed": report.deleted,
"refused": report.failed.len(), "keys": taken}));
if !report.failed.is_empty() {
return Ok(collected);
}
}
super::lease::renew_if_due(self).await?;
let mut cache = self.state.load_retire_cache();
for (key, _) in &ledger.due {
match self.store.delete(key).await {
Ok(()) | Err(StoreError::NotFound(_)) => {
cache.remove(key);
}
Err(e) => eprintln!("flint-sync: could not drop the retire log {key} ({e}); next time"),
}
}
self.state.save_retire_cache(&cache)?;
Ok(collected)
}
}