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
impl Syncer
Sourcepub async fn consume_owed(&mut self) -> LeanResult<(usize, Seen)>
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).
Sourcepub async fn read_cell_requests(&mut self) -> LeanResult<()>
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.
Sourcepub async fn run_barrier(&mut self) -> LeanResult<BarrierReport>
pub async fn run_barrier(&mut self) -> LeanResult<BarrierReport>
Steps 2–7. barrier = one full publish cycle (the cadence arm).
Sourcepub async fn cadence_barrier(&mut self) -> LeanResult<BarrierReport>
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.
Sourcepub async fn declared_barrier(&mut self) -> LeanResult<BarrierReport>
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.
Sourcepub async fn declared_barrier_as(
&mut self,
source: &str,
) -> LeanResult<BarrierReport>
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
impl Syncer
Sourcepub async fn checkout(&mut self) -> LeanResult<CheckoutReport>
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.
pub async fn checkout_scoped( &mut self, scope: Option<Vec<String>>, ) -> LeanResult<CheckoutReport>
Source§impl Syncer
impl Syncer
Sourcepub async fn rescope(
&mut self,
target: Option<Vec<String>>,
) -> LeanResult<RescopeReport>
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.
Sourcepub async fn replay_scope_intent(&mut self) -> LeanResult<Option<RescopeReport>>
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
impl Syncer
pub fn control_path(&self, name: &str) -> PathBuf
pub fn load_posture(&self) -> LeanResult<Option<SentinelPosture>>
Sourcepub fn sentinel_preflight(&self) -> LeanResult<SentinelPosture>
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.
Sourcepub fn write_capabilities(&self, posture: &SentinelPosture) -> LeanResult<()>
pub fn write_capabilities(&self, posture: &SentinelPosture) -> LeanResult<()>
Write .flint/capabilities.json for the current posture.
pub fn read_capabilities(&self) -> Option<Capabilities>
pub fn load_remote_seq(&self) -> RemoteSeq
Sourcepub fn touch_remote_seq(
&self,
observed_seq: Option<u64>,
observed_etag: Option<String>,
integrated_seq: u64,
) -> LeanResult<()>
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.
Sourcepub fn carry_sync_request(
&self,
requested_unix: u64,
requestor: &str,
) -> LeanResult<()>
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
impl Syncer
Sourcepub fn load_gauges(&self) -> LeanResult<Gauges>
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.
Sourcepub fn note_boundary(&self, source: &str, seq: u64) -> LeanResult<()>
pub fn note_boundary(&self, source: &str, seq: u64) -> LeanResult<()>
Record that a boundary installed.
Sourcepub fn note_auth_pause(&self) -> LeanResult<()>
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.
Sourcepub fn clear_auth_pause(&self) -> LeanResult<()>
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.
Sourcepub fn write_gauges(&self, withheld: Option<Withheld>) -> LeanResult<Gauges>
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
impl Syncer
pub fn save_metrics_posture(&self, p: &MetricsPosture) -> LeanResult<()>
pub fn load_metrics_posture(&self) -> Option<MetricsPosture>
Source§impl Syncer
impl Syncer
Sourcepub async fn news_poll(&mut self) -> LeanResult<Option<u64>>
pub async fn news_poll(&mut self) -> LeanResult<Option<u64>>
One poll: what an idle tick’s first two steps read, nothing else.
A writer reads the cell first, so a gateway boundary request becomes
the pending publish the sentinel arm honours within its next second
(§2.5’s door, a second instead of a floor). Then the pointer:
Some(seq) when it names a document newer than the ticker’s
observed_seq, which moves to it now, so the agent sees the news
before a barrier takes it. integrated_seq stays where it was.
Sourcepub fn news_start(&self) -> LeanResult<()>
pub fn news_start(&self) -> LeanResult<()>
Once, before the arm’s first poll: the ticker names the document the tree holds. A checkout writes no ticker, so a fresh tree’s first poll would otherwise read its own checkout as news and run a barrier.
Sourcepub async fn news_tick(&mut self) -> LeanResult<FloorOutcome>
pub async fn news_tick(&mut self) -> LeanResult<FloorOutcome>
The early barrier: the floor’s own, without the floor’s honours. A writer’s cadence barrier, a reader’s pull.
Sourcepub async fn news_arm(
&mut self,
clock: &mut NewsClock,
now: Instant,
) -> LeanResult<Option<FloorOutcome>>
pub async fn news_arm( &mut self, clock: &mut NewsClock, now: Instant, ) -> LeanResult<Option<FloorOutcome>>
The loop’s news arm, one tick: nothing while the workspace is idle; else a poll, and the early barrier when news is due. A failed barrier keeps the news pending and still counts against the interval, so a failing store is retried at that pace, not every second.
Source§impl Syncer
impl Syncer
Sourcepub fn refuse_if_read(&self, what: &str) -> LeanResult<()>
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.
Sourcepub async fn reader_pull(&mut self) -> LeanResult<Pull>
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
impl Syncer
pub fn load_pending(&self, verb: Verb) -> LeanResult<Option<PendingSentinel>>
pub fn load_budget(&self) -> LeanResult<SentinelBudget>
Sourcepub fn charge_budget(&self, published_bytes: u64) -> LeanResult<u64>
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.
Sourcepub fn sentinel_due(&self) -> LeanResult<Due>
pub fn sentinel_due(&self) -> LeanResult<Due>
Whether a standing pending sentinel may be honored on this tick.
Sourcepub fn poll_sentinels(&mut self) -> LeanResult<Vec<Verb>>
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.
Sourcepub fn recover_consume_staging(&mut self) -> LeanResult<()>
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.
Sourcepub fn request_boundary(
&mut self,
nonce: &str,
note: Option<String>,
) -> LeanResult<()>
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.
pub fn read_ack(&self, verb: Verb) -> Option<Ack>
Sourcepub async fn honor_pending(
&mut self,
verb: Verb,
forced: bool,
) -> LeanResult<Option<Ack>>
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).
Sourcepub async fn honor_pending_as(
&mut self,
verb: Verb,
forced: bool,
source: Option<&str>,
) -> LeanResult<Option<Ack>>
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.
Sourcepub async fn settle_pending_at_startup(&mut self) -> LeanResult<()>
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
impl Syncer
Sourcepub async fn sentinel_tick(&mut self) -> LeanResult<Vec<Ack>>
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.
Sourcepub async fn floor_tick(&mut self) -> LeanResult<FloorOutcome>
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.
Sourcepub async fn drain(&mut self) -> LeanResult<Vec<Ack>>
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
impl Syncer
Sourcepub async fn sync(&mut self) -> LeanResult<SyncReport>
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).
pub async fn sync_scoped( &mut self, scope: Option<Vec<String>>, ) -> LeanResult<SyncReport>
Source§impl Syncer
impl Syncer
Sourcepub async fn track_untracked(&self, now: u64) -> LeanResult<Vec<String>>
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.
Sourcepub async fn sweep_untracked_if_due(&self, now: u64)
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.