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 held lease, once claimed. Barriers refuse to run without it.
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 verify_not_deposed_pub(&self) -> LeanResult<()>
pub async fn verify_not_deposed_pub(&self) -> LeanResult<()>
The in-loop honor paths’ fence check (boundary-verbs plan D2):
Syncer::sync carries no lease/epoch check of its own, so a
straggler consuming a sync sentinel between deposal and its next
cooperative fence would apply the successor’s manifest onto its
zombie tree and ack SUCCESS.
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 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,
fenced: bool,
) -> LeanResult<()>
pub fn write_capabilities( &self, posture: &SentinelPosture, fenced: bool, ) -> 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
pub fn load_stage(&self) -> LeanResult<PendingStage>
pub fn save_stage(&self, s: &PendingStage) -> LeanResult<()>
Sourcepub fn orphans_key(&self) -> String
pub fn orphans_key(&self) -> String
The key D9’s durable orphan summary lives at.
Sourcepub async fn surface_orphans(&self, stage: &PendingStage) -> LeanResult<bool>
pub async fn surface_orphans(&self, stage: &PendingStage) -> LeanResult<bool>
Surface the staged-but-uncited set into the BUCKET (D9).
pending.json lives on the emptyDir, which is exactly the thing
a pure-spot replacement destroys — so on the routine failure of
this fleet the pending record can name nothing, and the operator
(a different process, often a different cluster) has no way to
know that durable work is sitting uncited. This is that summary,
and it is written where it survives cluster loss.
Written only when the candidate set CHANGES: a re-PUT per tick would be a request the design does not need, and an empty doc rewritten forever would be worse than none. The clearing write matters as much as the surfacing one — a stale summary claiming candidates pages an operator about work that was cited minutes ago.
Sourcepub async fn upload_lane(&mut self) -> LeanResult<LaneReport>
pub async fn upload_lane(&mut self) -> LeanResult<LaneReport>
The upload lane: durability, every floor tick, no citation.
Sourcepub async fn declared_lane(&mut self) -> LeanResult<LaneReport>
pub async fn declared_lane(&mut self) -> LeanResult<LaneReport>
The lane pass behind a DECLARED boundary — a publish sentinel
(D1 × D6) or the preStop drain (D10). The citation that follows
is going to be acked, so a delete the agent made before the
touch may not sit out the boundary waiting for a second walk:
confirm_absences takes that observation by lstat instead.
Sourcepub fn repair_candidates(&self) -> LeanResult<Vec<String>>
pub fn repair_candidates(&self) -> LeanResult<Vec<String>>
Paths whose INTEGRATED object (the baseline) differs from the manifest’s citation: a consumed HITL write, or checkout’s S3-wins adoption. The fused barrier has carried this repair since the amputation leg; gated mode never runs the fused barrier, so without the same pass here an acked HITL write is adopted into the tree, scans clean forever after, and stays cited at its PREVIOUS version — invisible to exactly the pinned readers D13 governs.
Sourcepub fn citation_due(
&self,
sentinel_pending: bool,
) -> LeanResult<Option<CitationSource>>
pub fn citation_due( &self, sentinel_pending: bool, ) -> LeanResult<Option<CitationSource>>
Which coherent point, if any, is due right now.
pub async fn citation_pass( &mut self, source: CitationSource, ) -> LeanResult<CitationReport>
Sourcepub async fn gated_tick(
&mut self,
sentinel_pending: bool,
) -> LeanResult<(LaneReport, CitationReport)>
pub async fn gated_tick( &mut self, sentinel_pending: bool, ) -> LeanResult<(LaneReport, CitationReport)>
The gated floor tick: lane, then a citation if one is due.
Both halves are reported because the floor arm has to describe them separately — a tick that staged 40 files and cited nothing is the mode working, and a tick that staged nothing and cited nothing is idle. Collapsing them into one “no change” would make gated mode indistinguishable from a wedged one in the log.
Sourcepub async fn gated_startup_check(&mut self) -> LeanResult<()>
pub async fn gated_startup_check(&mut self) -> LeanResult<()>
The startup gate for gated mode (D8, D11): probe the version
surface BEFORE a single byte is staged, and refuse rather than
degrade. Inert in cadence/hybrid, which need no version
surface at all — a gate that took every default workspace down
with it would be worse than the hazard.
Sourcepub async fn recover_staged(&mut self) -> LeanResult<RecoverReport>
pub async fn recover_staged(&mut self) -> LeanResult<RecoverReport>
flint-sync recover-staged (D9): re-cite durable-but-uncited
work as ONE flagged boundary — a manifest CAS with no data
movement, because the versions already exist.
The routine path into this verb is a pure-spot pod replacement:
the emptyDir that held pending.json is gone, so the pending
record can name nothing and the bucket is the only source of
truth. Two shapes are recovered, and they overlap:
- uncited work — the key’s current version is not the one the manifest cites (or the key is not cited at all: a brand-new file staged and never installed);
- a dangling citation — the cited version no longer exists, because gated staging made it NONCURRENT and the retention backstop ran its clock against live cited data (D8’s inversion). Checkout refuses on this rather than serving a hole, so recovery is what makes the workspace usable again.
Recovery rolls forward, onto the newer bytes, and says so: every re-citation writes a conflict record. It is deliberately NOT a three-way merge — a replacement pod has no merge base, and merging against an empty one would classify the entire existing manifest as foreign and queue the whole tree into the HITL inbox.
Sourcepub async fn versioning_conformance(&self) -> LeanResult<()>
pub async fn versioning_conformance(&self) -> LeanResult<()>
The versioning conformance probe (D8) — the SHARED
implementation in flint_store::probe, because the operator
runs the same probe on its reconcile cadence and the two
verdicts must never disagree. A workspace the operator calls
conformant and the syncer refuses to start is worse than
either answer alone.
A project-scoped proxy that strips x-amz-version-id degrades
gated mode SILENTLY — pending entries would carry None, and
citation would fall back to etag semantics on a key whose
current version is uncited, which is precisely the torn view the
mode exists to prevent. So the probe REFUSES rather than
degrading, and is re-run at startup and on the operator cadence:
proxies upgrade, and a bucket’s posture changes under you.
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 citation installed. Bumps the forced counter when the source was a cap rather than a declared coherent point.
Sourcepub fn note_durable(&self) -> LeanResult<()>
pub fn note_durable(&self) -> LeanResult<()>
Record that bytes were made durable without being cited (the gated upload lane).
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 the healthy path must not rewrite the file every heartbeat for no change.
Sourcepub fn write_gauges(
&self,
fenced: bool,
withheld: Option<Withheld>,
) -> LeanResult<Gauges>
pub fn write_gauges( &self, fenced: bool, 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
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 refuse_what_this_incarnation_can_never_honor(
&mut self,
) -> LeanResult<bool>
pub async fn refuse_what_this_incarnation_can_never_honor( &mut self, ) -> LeanResult<bool>
D2’s refused-ack rule for the restarted claimant, which was the last open corner of the protocol.
A syncer SIGKILLed mid-honor runs no cooperative fence path, so
nothing settles the pending sentinel it left in the emptyDir. If the
kubelet then restarts it over that surviving tree while a successor
holds the lease, it blocks in claim — the successor’s token keeps
advancing, so quiet polls never accumulate — and settle_pending_at_ startup is unreachable, because that runs only after claim returns.
The agent is left polling an ack that will never come, behind a
capabilities.json that still says state: "live" with live verbs.
This process can never honor what it owes. It says so, once, on the
first Waiting poll: a refused-fenced ack per owed verb and a fenced
marker, so the agent stops waiting and the marker stops lying.
Gated on a pending record actually standing. A fresh replacement pod waiting out its 60 s of quiet polls is HEALTHY, not fenced, and its emptyDir carries no pending record — so it takes none of this.
Sourcepub fn refuse_pending(
&mut self,
verb: Verb,
observed_epoch: Option<u64>,
) -> LeanResult<()>
pub fn refuse_pending( &mut self, verb: Verb, observed_epoch: Option<u64>, ) -> LeanResult<()>
The refused ack (D2). Deposal must never strand a waiting agent.
Sourcepub fn mark_fenced(&self) -> LeanResult<()>
pub fn mark_fenced(&self) -> LeanResult<()>
Flip the capability marker to fenced: no verbs, state: "fenced", so agents stop touching sentinels on a zombie.
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 heartbeat_tick(&mut self) -> LeanResult<()>
pub async fn heartbeat_tick(&mut self) -> LeanResult<()>
The D12 heartbeat renewal arm, as a tick of its own.
It is a method rather than three lines in the run loop because
of what it owes on a fence. This arm renews on its own
non-resettable interval — min(floor, 30) s, decoupled from
publish cadence, which is the whole reason D12 added it — so on
deposal it is usually the FIRST arm to find out, ahead of the
floor tick and ahead of any poll that has nothing to honor.
Returning from here without settling would strand every owed
ack and leave capabilities.json advertising live verbs on a
zombie: the hole D2’s refused acks exist to close, re-opened at
the arm D12 added to close a different one.
Sourcepub async fn floor_tick(&mut self) -> LeanResult<FloorOutcome>
pub async fn floor_tick(&mut self) -> LeanResult<FloorOutcome>
One floor tick: renew (D12), then 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.