Skip to main content

Syncer

Struct Syncer 

Source
pub struct Syncer {
    pub store: Arc<dyn ObjectStore>,
    pub cfg: LeanConfig,
    pub state: SyncerState,
    pub lease: Option<EpochLease>,
    pub cell_written_at: Option<Instant>,
    pub noted_not_regular: BTreeSet<String>,
}
Expand description

Everything a syncer operation needs: store + config + durable state.

Fields§

§store: Arc<dyn ObjectStore>§cfg: LeanConfig§state: SyncerState§lease: Option<EpochLease>

The lease held for the CURRENT commit section, if any. None between barriers: the cell arbitrates who installs a manifest and nothing else (design 2026-09-13 §4), so a barrier claims after its uploads and releases after its baseline rewrite.

§cell_written_at: Option<Instant>

When this process last WROTE the cell (a claim, a renew): the clock the commit section’s deletes fence on (review 2026-09-18, C2). None while no write is known — an adopted claim writes nothing — so the first delete renews.

§noted_not_regular: BTreeSet<String>

Standing conditions already written to conflicts.jsonl by THIS process, so a condition that persists across every poll tick is recorded once rather than per tick. Deliberately in-memory: a restart is a new incarnation and should re-record what it finds.

Implementations§

Source§

impl Syncer

Source

pub async fn consume_owed(&mut self) -> LeanResult<(usize, Seen)>

Step 1 (P1-lite, 2026-09-25): what the tree is OWED, derived from the document and taken in one pass. A path is owed where the document differs from the baseline — which is also the merge base — and the tree is clean there; only a path the tree HOLDS or its scope covers (what the scope declined stays declined). Nothing is stored between deriving it and taking it, so nothing can be overtaken: the queue this replaces needed a prune for that (L-123). A DIRTY path is the agent’s newer work and is not owed: it publishes, and the merge records what it publishes over (R7, the delete override). Unless its bytes already ARE the document’s — a restart between this writer’s CAS and step 7 leaves exactly that — and then the baseline simply follows (content convergence). Returns how many paths it took, and what its read of the document established (Seen) — the barrier’s fast path needs exactly that, and must not pay a second GET for it (an idle tick is the cell and the pointer).

Source

pub async fn read_cell_requests(&mut self) -> LeanResult<()>

The cell’s verb requests alone (§2.5), as a barrier’s step 1 reads them: no consume, no fetch.

Source

pub async fn run_barrier(&mut self) -> LeanResult<BarrierReport>

Steps 2–7. barrier = one full publish cycle (the cadence arm).

Source

pub async fn cadence_barrier(&mut self) -> LeanResult<BarrierReport>

The floor tick’s barrier. Stamped cadence so the bucket can answer “which clock installed this boundary?” without the ack — the ack is a LOCAL file, and the fleet-visible question is asked by operators and by the gateway’s /status, neither of which can see it.

Source

pub async fn declared_barrier(&mut self) -> LeanResult<BarrierReport>

A barrier whose result somebody is going to ACK: a sentinel honor (D1) or the preStop drain (D10). Identical to run_barrier except that it confirms first-absence paths rather than acking a boundary that withholds them.

Source

pub async fn declared_barrier_as( &mut self, source: &str, ) -> LeanResult<BarrierReport>

declared_barrier under an explicit provenance stamp — the drain and the budget-deferred honor are the same barrier reporting a different clock.

Source§

impl Syncer

Source

pub async fn checkout(&mut self) -> LeanResult<CheckoutReport>

Materialize the workspace from the manifest. Idempotent across crashes (resume skips present paths); refuses over budget BEFORE the first byte.

Source

pub async fn checkout_scoped( &mut self, scope: Option<Vec<String>>, ) -> LeanResult<CheckoutReport>

Source§

impl Syncer

Source

pub async fn rescope( &mut self, target: Option<Vec<String>>, ) -> LeanResult<RescopeReport>

Move the workspace to target: stop holding what it no longer admits, and fetch what it newly does.

§Why this cannot be rm plus a scope edit

classify reads exactly the two states a narrow passes through: present-in-scan-absent-from-baseline is an upload, and present-in-baseline-absent-from-scan-and-prev_scan is a delete. So unlink-then-uncite crashes into publishing deletions, and uncite-then-unlink crashes into re-uploading identical bytes and re-citing everything just dropped. Neither order is safe alone. The invariant, stated for the model: a narrow is an unwatch, never an absence.

What makes it safe is the INTENT, not the order: the target is durable before the first mutation and cleared after the last, run_barrier replays any intent it finds before it does anything else, and the replay is idempotent. The order within still matters for the window where the intent itself is lost — uncite first, so the failure mode is a file that gets re-uploaded rather than one that gets DELETED from the bucket.

§Why the verb takes a SET

manifest::load fetches the pointer and every chunk regardless of scope, so the floor is one whole-manifest load per call. A per-path verb would pay that floor per path.

Source

pub async fn replay_scope_intent(&mut self) -> LeanResult<Option<RescopeReport>>

Finish a rescope a crash left half-applied. None when there is none in flight. Idempotent: running it twice is running it once.

Source§

impl Syncer

Source

pub fn control_path(&self, name: &str) -> PathBuf

Source

pub fn load_posture(&self) -> LeanResult<Option<SentinelPosture>>

Source

pub fn sentinel_preflight(&self) -> LeanResult<SentinelPosture>

The pre-existing-.flint/ pre-flight (D0.4).

Reserving .flint/ is a BREAKING change for a workspace already using it as data: its writes silently stop publishing and — worse — files literally named .flint/publish or .flint/sync would be consumed (renamed away) by the sentinel poll: a data grab from a non-participating workspace. When either signature is present, sentinel consumption is disabled for the workspace: the poll arm never arms, capabilities.json reports no verbs with the reason, and the operator surfaces it as SentinelVerbsActive=False.

The verdict is STICKY once written — see POSTURE.

Source

pub fn write_capabilities(&self, posture: &SentinelPosture) -> LeanResult<()>

Write .flint/capabilities.json for the current posture.

Source

pub fn read_capabilities(&self) -> Option<Capabilities>

Source

pub fn load_remote_seq(&self) -> RemoteSeq

Source

pub fn touch_remote_seq( &self, observed_seq: Option<u64>, observed_etag: Option<String>, integrated_seq: u64, ) -> LeanResult<()>

Move the ticker (D5). observed is what the barrier’s existing manifest HEAD (or its completed install) already told us — this never issues a request of its own.

updated_unix refreshes on every call (the liveness heartbeat); observed_seq/observed_etag change only when they change.

Source

pub fn carry_sync_request( &self, requested_unix: u64, requestor: &str, ) -> LeanResult<()>

Carry an advisory sync request into the ticker (D14). The asymmetry with boundary_request is about blast radius, not principle: a boundary publishes what is already on disk and touches no local file, whereas sync re-derives the tree against the current remote manifest and DELETES local files for remotely-deleted paths. Performing that on a remote’s say-so would upgrade what a leaked gateway bearer can do from “publish, plus hand over these N named objects” to “rewrite and delete across a running agent’s tree, at my timing, under a scope I choose”.

Source§

impl Syncer

Source

pub fn load_gauges(&self) -> LeanResult<Gauges>

Last written gauges, or defaults. Never fails on a corrupt file: a diagnosis surface that panics is worse than a stale one.

Source

pub fn note_boundary(&self, source: &str, seq: u64) -> LeanResult<()>

Record that a boundary installed.

Source

pub fn note_auth_pause(&self) -> LeanResult<()>

Record that the store refused our credentials on a renewal. First refusal wins: the gauge answers “since when”, so a second consecutive failure must not keep resetting the clock to now.

Source

pub fn clear_auth_pause(&self) -> LeanResult<()>

Clear the pause. Writes only on an actual transition: this runs on every successful renewal and floor tick, and the healthy path must not rewrite the file every tick for no change.

Source

pub fn write_gauges(&self, withheld: Option<Withheld>) -> LeanResult<Gauges>

Recompute and write the gauges. Deliberately synchronous and store-free: a scrape, a tick and an exec all cost zero bucket requests, and the type system is what says so.

Source§

impl Syncer

Source§

impl Syncer

Source

pub fn refuse_if_read(&self, what: &str) -> LeanResult<()>

The refusal every publishing or fencing path gives a reader. Refused because it is configuration, not contention: retrying cannot make a read-only syncer write.

Source

pub async fn reader_pull(&mut self) -> LeanResult<Pull>

One reader tick: two GETs, and a sync only when there is news.

Source§

impl Syncer

Source

pub fn load_pending(&self, verb: Verb) -> LeanResult<Option<PendingSentinel>>

Source

pub fn load_budget(&self) -> LeanResult<SentinelBudget>

Source

pub fn charge_budget(&self, published_bytes: u64) -> LeanResult<u64>

D3.1: charge the meter for a completed sentinel honor. published_bytes == 0 (a no-diff honor) costs NOTHING — the budget exists to bound work and a no-diff honor does none; the min-interval remains its only bound.

Source

pub fn sentinel_due(&self) -> LeanResult<Due>

Whether a standing pending sentinel may be honored on this tick.

Source

pub fn poll_sentinels(&mut self) -> LeanResult<Vec<Verb>>

One poll tick: lstat exactly two fixed paths, and stat the two staging names — no inotify dependency, no directory scan.

Returns the verbs whose sentinel was consumed (or coalesced) on this tick.

Source

pub fn recover_consume_staging(&mut self) -> LeanResult<()>

Recover a consume that crashed between the rename and the pending write. Called at startup, before the first poll.

Source

pub fn request_boundary( &mut self, nonce: &str, note: Option<String>, ) -> LeanResult<()>

A boundary asked for through a door that is NOT the file protocol — the gateway’s inbox field (§2.5) or the UDS socket.

It folds into the same pending record a .flint/publish touch would, which is the whole design of the layered doors: the extra doors are sugar over one consume path, so min-interval, coalescing, the work-metered budget, the covered-nonce ack and the crash rules apply to them without a second implementation that could disagree with the first.

Source

pub fn read_ack(&self, verb: Verb) -> Option<Ack>

Source

pub async fn honor_pending( &mut self, verb: Verb, forced: bool, ) -> LeanResult<Option<Ack>>

Honor a standing pending sentinel, if one stands and is due.

forced = this is a floor tick picking up a budget-deferred boundary; the ack is stamped sentinel-deferred. Ok(None) means nothing was owed (or it is not due yet).

Source

pub async fn honor_pending_as( &mut self, verb: Verb, forced: bool, source: Option<&str>, ) -> LeanResult<Option<Ack>>

honor_pending under an explicit provenance stamp. The drain needs it: it rewrites the ack to drain, and a manifest still stamped sentinel-deferred would have the bucket and the ack naming two different clocks for one boundary.

Source

pub async fn settle_pending_at_startup(&mut self) -> LeanResult<()>

The startup settle (the uniform crash rule). Runs before the first poll arms: a surviving pending must be honored, acked and retired before any fresh sentinel may be consumed.

Source§

impl Syncer

Source

pub async fn sentinel_tick(&mut self) -> LeanResult<Vec<Ack>>

One poll tick of the sentinel arm: consume what is there, then honor if the min-interval and the work budget allow.

In cadence mode the arm still consumes and honors — a sentinel triggers a fused barrier there too; the mode’s meaning is that there is no separate citation lane, not that the verbs are dead.

Source

pub async fn floor_tick(&mut self) -> LeanResult<FloorOutcome>

One floor tick: either honor a standing pending sentinel that the budget or min-interval held back — the boundary is honored by a REAL barrier, its ack stamped sentinel-deferred — or run the ordinary cadence barrier.

This is the arm B16 regresses on: the interval rewrite must not let the poll arm starve cadence.

Source

pub async fn drain(&mut self) -> LeanResult<Vec<Ack>>

The preStop drain (D10 rule 1): settle every owed ack BEFORE the lease release. A pending sentinel at SIGTERM is answered by the drain itself — the container-restart case, where the emptyDir and the agent both survive, must not strand a waiting agent.

Source§

impl Syncer

Source

pub async fn sync(&mut self) -> LeanResult<SyncReport>

Whole-tree sync: advances seq and the cheap path’s record once it has taken everything owed; manifest_etag stays where the last barrier left it (reader.rs relies on that).

Source

pub async fn sync_scoped( &mut self, scope: Option<Vec<String>>, ) -> LeanResult<SyncReport>

Source§

impl Syncer

Source

pub fn trace(&self, ev: &str, fields: Value)

Emit one trace event, when the trace is on.

Source

pub fn trace_requests(&self) -> Value

The store’s request counts, as a trace field.

Source§

impl Syncer

Source

pub async fn track_untracked(&self, now: u64) -> LeanResult<Vec<String>>

The INGRESS sweep (immutable-handles design 2026-09-19, R5): track every object at a BARE path key under files/ — an outside writer’s aws s3 cp, the one write nothing in this crate makes any more — that has sat there for at least untracked_grace_secs and that no entry has adopted. The object is copied server-side to a handle of its own, named after the ingress etag so a second sweep finds its copy already there, and ONE manifest CAS cites the copy (manifest::commit_edit, as the gateway commits a save) — only while the path still cites what the sweep saw; a citation that moved since leaves the object to the next sweep. The ingress object itself is never deleted and never needs a conditional DELETE: a later outside overwrite is a new ingress with a new etag, adopted the same way.

Before handles this sweep tracked a lost writer’s upload at the cited key (finding 10). Under handles such an upload is an orphan HANDLE, and the orphan sweep collects it (R4) — never adopts it.

One manifest read and a LIST of the files prefix, then one commit per object adopted. Returns the paths adopted.

Source

pub async fn sweep_untracked_if_due(&self, now: u64)

The floor’s hook: sweep when untracked_sweep_secs have passed since the last sweep (0 = never). A writer’s first tick starts the clock instead of sweeping — its own checkout has just read every citation. Best-effort: a sweep that fails is retried on the next tick and never fails the floor.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.