use std::sync::Arc;
use flint_store::{EpochLease, ObjectStore, StoreError};
use super::state::Incarnation;
use super::{manifest, LeanError, LeanResult, Syncer};
pub const QUIET_POLLS: u32 = 6;
pub enum ClaimOutcome {
Claimed(EpochLease),
Waiting { quiet_polls: u32 },
}
pub async fn warn_if_prefix_is_shared(sc: &Syncer) {
if sc.cfg.sole_writer {
return;
}
let found = flint_store::layout::neighbours(
sc.store.as_ref(),
&sc.cfg.prefix,
flint_store::layout::Writer::LeanWorkspace,
)
.await
.unwrap_or_default();
for f in found {
eprintln!("flint-sync: {}", f.report());
}
}
pub async fn claim_step(sc: &mut Syncer) -> LeanResult<ClaimOutcome> {
let store: &Arc<dyn ObjectStore> = &sc.store;
let key = sc.cfg.epoch_key();
let mut inc = sc.state.load_incarnation()?.unwrap_or_else(|| Incarnation {
holder_id: format!("lean-{}", uuid::Uuid::new_v4()),
epoch: 0,
last_token: None,
quiet_polls: 0,
});
let observed = store.epoch_read(&key).await?;
match observed {
None => match store.epoch_acquire(&key, &inc.holder_id, None).await {
Ok(lease) => {
inc.epoch = lease.epoch;
inc.last_token = None;
inc.quiet_polls = 0;
sc.state.save_incarnation(&inc)?;
sc.lease = Some(lease.clone());
Ok(ClaimOutcome::Claimed(lease))
}
Err(StoreError::PreconditionFailed(_)) | Err(StoreError::Conflict(_)) => {
Ok(ClaimOutcome::Waiting { quiet_polls: 0 })
}
Err(e) => Err(e.into()),
},
Some(state) => {
let ours = state.holder_id == inc.holder_id;
let quiet = inc.last_token.as_deref() == Some(state.token.as_str());
if ours || state.released || (quiet && inc.quiet_polls + 1 >= QUIET_POLLS) {
let rotate = !ours && !state.released;
match store.epoch_acquire(&key, &inc.holder_id, Some(&state)).await {
Ok(lease) => {
if rotate {
manifest::rotate_for_takeover(store.as_ref(), &sc.cfg, lease.epoch)
.await?;
}
inc.epoch = lease.epoch;
inc.last_token = None;
inc.quiet_polls = 0;
sc.state.save_incarnation(&inc)?;
sc.lease = Some(lease.clone());
Ok(ClaimOutcome::Claimed(lease))
}
Err(StoreError::PreconditionFailed(_)) | Err(StoreError::Conflict(_)) => {
inc.last_token = None;
inc.quiet_polls = 0;
sc.state.save_incarnation(&inc)?;
Ok(ClaimOutcome::Waiting { quiet_polls: 0 })
}
Err(e) => Err(e.into()),
}
} else {
inc.quiet_polls = if quiet { inc.quiet_polls + 1 } else { 0 };
inc.last_token = Some(state.token.clone());
let polls = inc.quiet_polls;
sc.state.save_incarnation(&inc)?;
Ok(ClaimOutcome::Waiting { quiet_polls: polls })
}
}
}
}
fn observed_echo(sc: &Syncer) -> Option<String> {
let g = sc.load_gauges().ok()?;
let staged = sc.load_stage().map(|s| s.entries.len() as u64).unwrap_or(0);
let (seq, unix) = g.last_boundary.as_ref().map(|b| (b.seq, b.unix)).unwrap_or((0, 0));
let caps: Option<super::control::Capabilities> =
std::fs::read(sc.cfg.control_dir().join(super::control::CAPABILITIES))
.ok()
.and_then(|b| serde_json::from_slice(&b).ok());
serde_json::to_string(&flint_store::LeaseEcho {
syncer_version: super::SYNCER_VERSION.to_string(),
protocol: super::SENTINEL_PROTOCOL,
active_boundary_mode: sc.cfg.boundary_mode.as_str().to_string(),
last_cited_seq: seq,
last_cited_unix: unix,
staged_uncited_count: staged,
sentinel_verbs_active: caps.map(|c| !c.verbs.is_empty()).unwrap_or(false),
metrics_bound: sc.load_metrics_posture().filter(|m| m.enabled).map(|m| m.bound),
})
.ok()
}
pub async fn renew(sc: &mut Syncer) -> LeanResult<()> {
let key = sc.cfg.epoch_key();
let lease = sc
.lease
.clone()
.ok_or_else(|| LeanError::State("renew without a lease".into()))?;
let echo = observed_echo(sc);
match sc.store.epoch_renew(&key, &lease, echo.as_deref()).await {
Ok(l) => {
sc.lease = Some(l);
let _ = sc.clear_auth_pause();
Ok(())
}
Err(StoreError::PreconditionFailed(e)) => {
match sc.store.epoch_read(&key).await {
Ok(Some(state))
if state.holder_id == lease.holder_id
&& state.epoch == lease.epoch
&& !state.released =>
{
eprintln!(
"flint-sync: renew 412 on a cell that is still ours (epoch {}): a lost \
renew response — adopting its token, not fencing",
state.epoch
);
sc.lease = Some(EpochLease {
holder_id: state.holder_id,
epoch: state.epoch,
token: state.token,
});
let _ = sc.clear_auth_pause();
Ok(())
}
_ => {
sc.lease = None;
Err(LeanError::Fenced(format!("deposed at renew: {e}")))
}
}
}
Err(e @ StoreError::Auth(_)) => {
let _ = sc.note_auth_pause();
Err(e.into())
}
Err(e) => Err(e.into()),
}
}
pub async fn verify_claim(sc: &Syncer) -> LeanResult<()> {
let Some(mine) = sc.cfg.project_id.as_deref() else { return Ok(()) };
let key = sc.cfg.claim_key();
match sc.store.get_whole(&key, None).await {
Ok((_, body)) => {
let doc: serde_json::Value = serde_json::from_slice(&body)
.map_err(|e| LeanError::State(format!("claim cell {key} is unparseable: {e}")))?;
match doc.get("project_id").and_then(|v| v.as_str()) {
Some(p) if p == mine => Ok(()),
Some(p) => Err(LeanError::Refused(format!(
"prefix {} is claimed by project {p:?}; this workspace is project {mine:?} — \
refusing to check out or publish over another project's data (delete the \
standing claim explicitly if this reuse is intended)",
sc.cfg.prefix
))),
None => Err(LeanError::State(format!("claim cell {key} names no project_id"))),
}
}
Err(StoreError::NotFound(_)) => Ok(()),
Err(e) => Err(e.into()),
}
}
pub async fn release(sc: &mut Syncer) -> LeanResult<()> {
let key = sc.cfg.epoch_key();
if let Some(lease) = sc.lease.take() {
match sc.store.epoch_release(&key, &lease).await {
Ok(()) => Ok(()),
Err(StoreError::PreconditionFailed(_)) => Ok(()), Err(e) => Err(e.into()),
}
} else {
Ok(())
}
}