pub struct Syncer {
pub store: Arc<dyn ObjectStore>,
pub cfg: LeanConfig,
pub state: SyncerState,
pub lease: Option<EpochLease>,
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.
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_inbox(&mut self) -> LeanResult<Vec<InboxEntry>>
pub async fn consume_inbox(&mut self) -> LeanResult<Vec<InboxEntry>>
Step 1: consume the inbox. A barrier NEVER runs against an unconsumed inbox — this is what makes HITL uploads structurally un-amputatable. Returns the entries integrated (dropped from the cell at the window-open CAS).
Sourcepub async fn consume_inbox_doc(
&mut self,
doc: &InboxDoc,
) -> LeanResult<Vec<InboxEntry>>
pub async fn consume_inbox_doc( &mut self, doc: &InboxDoc, ) -> LeanResult<Vec<InboxEntry>>
consume_inbox over a cell the caller has already read — the
barrier reads it once and hands the same document to
apply_removals, so the removal pass adds no request.
Sourcepub async fn apply_removals(
&mut self,
doc: &InboxDoc,
) -> LeanResult<RemovalPass>
pub async fn apply_removals( &mut self, doc: &InboxDoc, ) -> LeanResult<RemovalPass>
Step 1b: perform the DECLARED removals (delete/rename design
§4-§6). Runs after consume_inbox_doc, so a rename’s destination
is in the tree before its source leaves it — §5: create first,
removal second; a partial application leaves an EXTRA file,
never a missing one.
Per removal, in order:
- the path must be containable, as a consume’s must;
- a rename WAITS until its destination is integrated (in the baseline): a consume that deferred the destination defers the removal with it;
- a locally-DIRTY path — unpublished edits, or a file the agent created there — is REFUSED, nothing applied, the agent’s work untouched. The same rule a consume applies to a HITL write over dirty bytes, and the reason a declared removal cannot be “just a delete”. Refused for good: the cell is told, and a retry that waited for the agent to publish would delete the very edit the refusal protected;
- a clean file is unlinked, and the unlink is CONFIRMED by lstat
before the path is declared — an unlink that did not take
publishes no deletion (
confirm_absences’ rule, kept); - a path already absent locally and known to the baseline or the merge base is declared as it stands: the agent got there first, or this tree never held it (a scoped workspace).
A declared path skips the two-scan guard (§4): that guard protects against absence INFERRED by a walk, and a declaration is not an inference. That is what lets a rename’s two halves ride one manifest generation.
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 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 — no inotify
dependency, no directory scan, ~2 lstats/s when idle.
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 — exactly as shipped, including advancing
seq/manifest_etag.