use std::sync::Arc;
use std::time::{Duration, Instant};
use flint_store::{EpochLease, EpochState, ObjectStore, StoreError};
use super::state::Incarnation;
use super::{manifest, LeanError, LeanResult, Syncer};
pub const QUIET_POLLS: u32 = 6;
pub const HANDOFF_QUIET_POLLS: u32 = 2;
pub const QUIET_SPACING_SECS: u64 = 10;
pub const CLAIM_POLL_SECS: u64 = 1;
pub const CLAIM_POLL_HEAD_MS: u64 = 200;
pub const CLAIM_DEADLINE_SECS: u64 = 150;
pub enum ClaimOutcome {
Claimed(EpochLease),
Waiting { quiet_polls: u32, behind: Option<String>, next: bool },
}
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 fn incarnation(sc: &Syncer) -> LeanResult<Incarnation> {
match sc.state.load_incarnation()? {
Some(i) => Ok(i),
None => {
let i = Incarnation {
holder_id: format!("lean-{}", uuid::Uuid::new_v4()),
epoch: 0,
last_token: None,
quiet_polls: 0,
};
sc.state.save_incarnation(&i)?;
Ok(i)
}
}
}
pub async fn claim_step(sc: &mut Syncer, count: bool) -> LeanResult<ClaimOutcome> {
let store: &Arc<dyn ObjectStore> = &sc.store;
let key = sc.cfg.epoch_key();
let mut inc = incarnation(sc)?;
let observed = store.epoch_read(&key).await?;
let Some(state) = observed else {
return 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());
sc.cell_written_at = Some(std::time::Instant::now());
sc.trace("claim", serde_json::json!({"verdict": "claimed", "how": "fresh", "epoch": lease.epoch}));
Ok(ClaimOutcome::Claimed(lease))
}
Err(StoreError::PreconditionFailed(_)) | Err(StoreError::Conflict(_)) => {
Ok(ClaimOutcome::Waiting { quiet_polls: 0, behind: None, next: false })
}
Err(e) => Err(e.into()),
};
};
let same_holder = state.holder_id == inc.holder_id;
let ours = same_holder && state.epoch == inc.epoch && !state.released;
let quiet = inc.last_token.as_deref() == Some(state.token.as_str());
let counted_quiet = if count {
if quiet {
inc.quiet_polls + 1
} else {
0
}
} else {
inc.quiet_polls
};
if ours {
let lease = EpochLease {
holder_id: state.holder_id,
epoch: state.epoch,
token: state.token,
waiters: state.waiters,
};
inc.last_token = None;
inc.quiet_polls = 0;
sc.state.save_incarnation(&inc)?;
sc.lease = Some(lease.clone());
sc.cell_written_at = None;
sc.trace("claim", serde_json::json!({"verdict": "claimed", "how": "adopted-own", "epoch": lease.epoch}));
return Ok(ClaimOutcome::Claimed(lease));
}
let (take, rotate, behind) = if same_holder && !state.released {
(true, true, None)
} else if state.released {
match state.handoff.as_deref() {
None => (true, false, None),
Some(h) if h == inc.holder_id => (true, false, None),
Some(h) => (counted_quiet >= HANDOFF_QUIET_POLLS, false, Some(h.to_string())),
}
} else {
(counted_quiet >= QUIET_POLLS, true, Some(state.holder_id.clone()))
};
if take {
let prior = serde_json::json!({"holder": state.holder_id, "epoch": state.epoch, "released": state.released,
"handoff": state.handoff, "waiters": state.waiters});
let how = if rotate && same_holder {
"orphaned-own"
} else if rotate {
"deposed"
} else if state.handoff.as_deref().map(|h| h != inc.holder_id).unwrap_or(false) {
"skipped-handoff"
} else {
"released"
};
return 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());
sc.cell_written_at = Some(std::time::Instant::now());
sc.trace("claim", serde_json::json!({"verdict": "claimed", "how": how, "epoch": lease.epoch, "prior": prior}));
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, behind, next: false })
}
Err(e) => Err(e.into()),
};
}
let token = if state.waiters.iter().any(|w| w == &inc.holder_id) {
state.token.clone()
} else {
match store.epoch_enqueue(&key, &state, &inc.holder_id).await {
Ok(after) => after.token,
Err(StoreError::PreconditionFailed(_)) | Err(StoreError::Conflict(_)) => {
state.token.clone()
}
Err(e) => return Err(e.into()),
}
};
if count {
inc.quiet_polls = counted_quiet;
inc.last_token = Some(token);
sc.state.save_incarnation(&inc)?;
}
let next = !state.released
&& state.waiters.iter().find(|w| **w != state.holder_id).map(|w| w == &inc.holder_id).unwrap_or(true);
Ok(ClaimOutcome::Waiting { quiet_polls: counted_quiet, behind, next })
}
pub async fn claim(sc: &mut Syncer) -> LeanResult<EpochLease> {
let started = Instant::now();
let mut last_counted: Option<Instant> = None;
let mut reported: Option<String> = None;
let spacing = Duration::from_secs(sc.cfg.claim_quiet_spacing_secs);
let deadline = Duration::from_secs(sc.cfg.claim_deadline_secs);
let poll = Duration::from_secs(sc.cfg.claim_poll_secs);
let head_poll = Duration::from_millis(sc.cfg.claim_poll_head_ms).min(poll);
loop {
let count = last_counted.map(|t| t.elapsed() >= spacing).unwrap_or(true);
match claim_step(sc, count).await? {
ClaimOutcome::Claimed(lease) => return Ok(lease),
ClaimOutcome::Waiting { quiet_polls, behind, next } => {
if count {
last_counted = Some(Instant::now());
sc.trace("claim", serde_json::json!({"verdict": "waiting", "behind": behind, "quiet_polls": quiet_polls,
"waited_ms": started.elapsed().as_millis() as u64}));
let line = format!(
"flint-sync: waiting for the publish fence behind {} (quiet {quiet_polls})",
behind.as_deref().unwrap_or("a rival claim")
);
if reported.as_deref() != Some(line.as_str()) {
eprintln!("{line}");
reported = Some(line);
}
}
if started.elapsed() >= deadline {
sc.trace("claim", serde_json::json!({"verdict": "deadline", "behind": behind,
"waited_ms": started.elapsed().as_millis() as u64}));
return Err(LeanError::State(format!(
"could not acquire the publish fence within {}s \
(behind {}); this barrier is abandoned and retried at the next floor — \
its uploads are durable and the next barrier adopts them",
deadline.as_secs(),
behind.as_deref().unwrap_or("a rival claim")
)));
}
tokio::time::sleep(if next { head_poll } else { poll }).await;
}
}
}
}
fn observed_echo(sc: &Syncer) -> Option<String> {
let g = sc.load_gauges().ok()?;
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,
last_cited_seq: seq,
last_cited_unix: unix,
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()
}
async fn still_ours(sc: &Syncer, key: &str, lease: &EpochLease) -> Option<EpochLease> {
match sc.store.epoch_read(key).await {
Ok(Some(state))
if state.holder_id == lease.holder_id && state.epoch == lease.epoch && !state.released =>
{
Some(EpochLease {
holder_id: state.holder_id,
epoch: state.epoch,
token: state.token,
waiters: state.waiters,
})
}
_ => None,
}
}
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);
sc.cell_written_at = Some(std::time::Instant::now());
let _ = sc.clear_auth_pause();
Ok(())
}
Err(StoreError::PreconditionFailed(e)) => {
match still_ours(sc, &key, &lease).await {
Some(adopted) => {
eprintln!(
"flint-sync: renew 412 on a cell that is still ours (epoch {}, {} queued): \
adopting its token, not fencing",
adopted.epoch,
adopted.waiters.len()
);
match sc.store.epoch_renew(&key, &adopted, echo.as_deref()).await {
Ok(l) => {
sc.lease = Some(l);
sc.cell_written_at = Some(std::time::Instant::now());
}
Err(_) => sc.lease = Some(adopted),
}
let _ = sc.clear_auth_pause();
Ok(())
}
None => {
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 renew_if_due(sc: &mut Syncer) -> LeanResult<()> {
if sc.lease.is_none() {
return Ok(());
}
let after = Duration::from_secs(QUIET_POLLS as u64 * sc.cfg.claim_quiet_spacing_secs / 2);
let due = sc.cell_written_at.map_or(true, |t| t.elapsed() >= after);
if !due {
return Ok(());
}
match renew(sc).await {
Ok(()) => {
sc.trace("renew", serde_json::json!({"epoch": sc.lease.as_ref().map(|l| l.epoch)}));
Ok(())
}
Err(e @ LeanError::Fenced(_)) => {
sc.trace("fence", serde_json::json!({"where": "renew", "detail": e.to_string()}));
Err(e)
}
Err(e) => {
eprintln!("flint-sync: renew inside the commit section failed ({e}); the next one retries");
Ok(())
}
}
}
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();
let Some(lease) = sc.lease.take() else { return Ok(()) };
let echo = observed_echo(sc);
sc.trace("release", serde_json::json!({"epoch": lease.epoch, "waiters_at_claim": lease.waiters}));
let mut current = lease;
for attempt in 0..RELEASE_ATTEMPTS {
match sc.store.epoch_handoff(&key, ¤t, echo.as_deref()).await {
Ok(()) => {
sc.trace("handed_off", serde_json::json!({"epoch": current.epoch, "retried": attempt > 0, "attempts": attempt + 1}));
return Ok(());
}
Err(StoreError::PreconditionFailed(_)) | Err(StoreError::Conflict(_)) => {
if attempt > 0 {
tokio::time::sleep(Duration::from_millis(20 * attempt as u64)).await;
}
match sc.store.epoch_read(&key).await {
Ok(Some(state))
if state.holder_id == current.holder_id
&& state.epoch == current.epoch
&& !state.released =>
{
current = EpochLease {
holder_id: state.holder_id,
epoch: state.epoch,
token: state.token,
waiters: state.waiters,
};
}
Ok(_) => return Ok(()),
Err(e) if attempt + 1 < RELEASE_ATTEMPTS => {
eprintln!("flint-sync: re-reading the publish fence for its handoff: {e}; retrying");
}
Err(e) => return Err(e.into()),
}
}
Err(e) => return Err(e.into()),
}
}
Err(LeanError::State(format!(
"the publish fence handoff lost {RELEASE_ATTEMPTS} races to the queue; the cell stays ours \
until this writer's next claim adopts it, or a waiter deposes it"
)))
}
pub const RELEASE_ATTEMPTS: u32 = 8;
pub async fn release_stale_own(sc: &mut Syncer) -> LeanResult<()> {
let key = sc.cfg.epoch_key();
let inc = incarnation(sc)?;
if let Some(state) = sc.store.epoch_read(&key).await? {
if state.holder_id == inc.holder_id && !state.released {
eprintln!(
"flint-sync: the publish fence was left held by a previous container of this pod \
(epoch {}); releasing it",
state.epoch
);
let rotated = state.epoch != inc.epoch;
if rotated {
manifest::rotate_for_takeover(sc.store.as_ref(), &sc.cfg, state.epoch).await?;
}
sc.trace("start", serde_json::json!({"held": true, "rotated": rotated}));
sc.lease = Some(EpochLease {
holder_id: state.holder_id,
epoch: state.epoch,
token: state.token,
waiters: state.waiters,
});
return release(sc).await;
}
}
sc.trace("start", serde_json::json!({"held": false, "rotated": false}));
Ok(())
}
pub type Observed = EpochState;