pub struct LeanConfig {Show 35 fields
pub prefix: String,
pub root: PathBuf,
pub floor_secs: u64,
pub sole_writer: bool,
pub access: Access,
pub whole_put_max: u64,
pub chunked: bool,
pub orphan_grace_secs: u64,
pub chunk_target: usize,
pub chunk_min: usize,
pub chunk_max: usize,
pub max_bytes: u64,
pub max_files: u64,
pub window_slack_secs: u64,
pub fanout: usize,
pub upload_fanout: usize,
pub project_id: Option<String>,
pub fetch_inflight_max_bytes: u64,
pub fetch_drivers: usize,
pub range_get_min_bytes: u64,
pub range_get_chunk_bytes: u64,
pub range_get_parallelism: usize,
pub sentinel_mode: SentinelMode,
pub sentinel_min_interval_secs: u64,
pub sentinel_hourly_budget: u64,
pub sentinel_poll_secs: u64,
pub claim_poll_secs: u64,
pub claim_poll_head_ms: u64,
pub claim_quiet_spacing_secs: u64,
pub claim_deadline_secs: u64,
pub drill_hold_commit_secs: u64,
pub drill_hold_gc_secs: u64,
pub event_trace: Option<Sink>,
pub untracked_sweep_secs: u64,
pub untracked_grace_secs: u64,
}Fields§
§prefix: StringBucket key prefix for this subtree (project- or subtree-scoped; the proxy’s tenancy boundary).
root: PathBufWorkspace root (the agent’s tree).
floor_secs: u64Publish cadence floor, seconds (the durability contract).
sole_writer: boolStamp sole_writer on every manifest this syncer installs.
Set for a workspace that is PUBLISHED rather than worked in — forge’s legible export is the shipped case. It tells every later reader that an object off its citation was moved by a stranger, so the reader refuses instead of adopting.
access: AccessRead or read-write (Access). Default read-write.
whole_put_max: u64Whole-object ceiling; larger files use multipart compose.
chunked: boolEntries per manifest chunk, in expectation, and the floor and ceiling around it (chunked design §3). Config rather than constants because §4 already anticipates this moving: the pointer body is O(entries / target), and past ~10M entries the target is one of the two things that can be changed instead of adding a second level. Not surfaced on the CRD — nothing has needed to tune it per workspace yet, and a knob with no caller is a support burden. Publish the manifest as a CHUNK LIST rather than one generation object per citation (chunked design). Default ON since 2026-09-04.
The migration is FAIL-CLOSED and one-way per workspace: a pointer-era binary cannot parse a chunked pointer, which is the point — every layout change has to make an old reader REFUSE rather than conclude the project is empty and re-seed over it.
Small projects are not penalised, which is what made turning it on unconditional rather than threshold-gated: under the target a project is ONE chunk, so it costs a pointer plus one object and two GETs to read — exactly what the single-generation layout already cost. The indirection was paid for by step one.
false is still a supported READ path forever (a workspace that
has not published since the migration) and is what the
single-layout tests set explicitly.
orphan_grace_secs: u64How long an unreferenced chunk must have sat before the reaper
may take it. Config rather than a constant because the model
makes it load-bearing in a workload-dependent way: it must
outlive the LONGEST publish, not the longest plausible one
(LeanChunkGCRacyGrace).
chunk_target: usize§chunk_min: usize§chunk_max: usize§max_bytes: u64Checkout refusal budgets (0 = unlimited).
max_files: u64§window_slack_secs: u64Window deadline slack beyond the barrier start, seconds. A dead syncer’s window is ignorable past this deadline.
fanout: usizeBounded concurrency for uploads and checkout fetches. The 0b rig measured the sequential loops at 561-854 PUTs/s and 1,000-2,000 GETs/s; fan-out multiplies directly against those.
upload_fanout: usizeConcurrent UPLOADS. Deliberately separate from fanout, and
measured separately: the read path’s width is what the checkout
rig moved, this one is what the publish rig moved. Both paths are
byte-bounded, by different gates — the read side by
fetch_inflight_max_bytes here, the upload side by the store’s
own gate (with_upload_inflight_max_bytes, 256 MiB from the
binary), which charges every whole body and every multipart part
from before its read until its PUT returns. So this number sets
request concurrency, not peak RSS.
project_id: Option<String>The project this workspace claims to be, stamped from the CR
(FLINT_SYNC_PROJECT_ID). When set, lease::verify_claim
refuses to run over a prefix whose claim cell names another
project — the operator’s refuse-foreign, enforced on the DATA
plane (audit 2026-09-03, finding 5). None = unstamped: no
check, the pre-operator posture.
fetch_inflight_max_bytes: u64Ceiling on bytes in flight across the checkout fan-out window.
fanout bounds the number of concurrent fetches but NOT their
size, and get_whole holds each whole object in RAM before it
reaches disk — so peak RSS was fanout x largest object, an
unbounded product of two numbers nobody sets together. A 32-wide
window over 64 MiB objects is 2 GiB in a syncer that ships with
no memory limit at all. Each entry takes permits proportional to
its size, so small-file trees still run the full width.
fetch_drivers: usizeDriver tasks for the checkout fan-out (see materialize). Each
runs fanout / fetch_drivers fetches at a time over its own
slice of the tree, so the per-request SDK work spreads over that
many cores instead of one. Default: the machine’s cores, at most
8 (FLINT_SYNC_FETCH_DRIVERS / spec.fetchDrivers, 0 = auto).
Measured 2026-09-12: one driver is the ~3,300 files/s plateau on
i4i.xlarge whatever fanout says; four are 6,188-6,521 at
fanout 256, and fanout 256 beats 128 for the first time.
range_get_min_bytes: u64Materialise an object this size or larger as PARALLEL RANGES
rather than one whole-object GET. 0 disables it.
Defaults to 8 MiB on the measurement that the repo’s rule asked for — runcr, i4i.large spot, us-west-1, one AZ, n=3, arms interleaved per rep, RANGES not means:
6 x 1 GiB 71.33-71.88 s -> 21.24-23.96 s 4 GiB + 2k x 16 K 57.02-57.83 s -> 15.30-18.30 s 20k x 8 KiB 16.92-19.44 s -> 16.40-17.39 s
The third row is the one that justifies a THRESHOLD rather than
ranging everything: every object there is under it, the ranged
path never fires, and the arms agree. Before this, the shipped
path lost a cold 6 GiB checkout to a plain aws s3 cp of the
same prefix on the same node (21.7 s); it now ties it.
Ranging also LOWERS peak RSS for a large object — see below — so this default costs memory nothing and returns some.
NOTE the interaction with range_get_chunk_bytes: a single part
is “one whole GET with extra steps” and fetch_ranged declines
it, so an object in [range_get_min_bytes, range_get_chunk_bytes)
passes this threshold and still takes the whole-object path. With
the defaults below the EFFECTIVE threshold is therefore 16 MiB,
not 8.
Two costs of get_whole are paid together and this is what
removes both: one object is one TCP stream, so a multi-GiB
checkpoint runs at single-connection throughput no matter how
wide the fan-out; and the body is held whole in RAM before it
reaches disk, which is the product fetch_inflight_max_bytes
exists to bound. Ranges write at their offset as they land, so
peak RSS per object becomes range_get_chunk_bytes x range_get_parallelism instead of the object’s size.
range_get_chunk_bytes: u64Bytes per range request. Large enough that per-request overhead is noise against S3’s first-byte latency; small enough that a retried range is cheap.
range_get_parallelism: usizeRanges in flight FOR ONE OBJECT. The checkout’s byte semaphore still bounds the whole window, so this multiplies concurrency only where the fan-out has run out of objects to work on — which is exactly the tail this exists to shorten.
sentinel_mode: SentinelModeSentinel posture (D0.4 pre-flight override).
sentinel_min_interval_secs: u64A consumed sentinel arriving sooner than this after the previous sentinel-honoring barrier waits; touches inside the interval coalesce into the pending record.
sentinel_hourly_budget: u64Work-metered hourly cap (D3.1): a honor charges
max(1, ceil(published_bytes / whole_put_max)) units, or 0 for
a no-diff honor. Exhaustion defers honors to the floor tick —
the workspace degrades to exactly cadence behavior.
sentinel_poll_secs: u64Sentinel poll cadence (env-only, not a fleet contract).
claim_poll_secs: u64How often a barrier waiting for the cell re-reads it.
claim_poll_head_ms: u64The poll cadence of the waiter at the head of the queue
(lease::CLAIM_POLL_HEAD_MS).
claim_quiet_spacing_secs: u64Minimum spacing between two observations that COUNT toward the
dead-holder (QUIET_POLLS) and abandoned-handoff thresholds.
The tests set it to 0 so a deposal takes six polls, not a minute.
claim_deadline_secs: u64Longest a barrier waits for the cell before it is abandoned and retried at the next floor.
drill_hold_commit_secs: u64DRILL-ONLY (FLINT_SYNC_DRILL_HOLD_COMMIT_SECS, never stamped by
the operator): sleep this long inside the commit section, right
after the claim. The one straggler the per-barrier lease still
has is a holder that stalls INSIDE its commit section, and that
section is milliseconds long — no drill can hit it by timing, so
this is how run-writers.sh opens the window. 0 = off.
drill_hold_gc_secs: u64DRILL-ONLY (FLINT_SYNC_DRILL_HOLD_GC_SECS, never stamped by the
operator): sleep this long between the garbage collector’s HEAD of
a path and its conditional DELETE, once per barrier. That gap is
where another writer’s lease-free upload can land (the model’s
LeanBarrierLeaseGCUnconditional), and it is two requests long, so
a drill opens it on purpose. 0 = off.
event_trace: Option<Sink>The protocol event trace (trace.rs); None = off.
untracked_sweep_secs: u64How often a WRITER sweeps for an upload a lost writer left at a
cited key (untracked.rs, finding 10), seconds;
FLINT_SYNC_UNTRACKED_SWEEP_SECS, 0 = off. Each sweep is one LIST of
the files prefix plus a manifest and an inbox read.
untracked_grace_secs: u64How long such an object must have sat untracked before the sweep
tracks it — the gateway’s UNTRACKED_GRACE_SECS, past any live
writer’s claim and commit.
Implementations§
Source§impl LeanConfig
impl LeanConfig
pub fn new(prefix: &str, root: impl Into<PathBuf>) -> Self
pub fn file_key(&self, path: &str) -> String
Sourcepub fn manifest_key(&self) -> String
pub fn manifest_key(&self) -> String
The LEGACY single-object manifest key. Still read (a workspace
written before the pointer layout has one) and, once migrated,
overwritten with a document that cannot parse as a manifest —
see manifest::REFUSAL_DOC for why deleting it would be a
data-loss shape rather than a tidy-up.
Sourcepub fn current_key(&self) -> String
pub fn current_key(&self) -> String
The pointer: the ONLY mutable metadata object. Small, CAS’d on every citation and on every takeover rotation.
Sourcepub fn generation_key(&self, seq: u64, flush_uuid: &str) -> String
pub fn generation_key(&self, seq: u64, flush_uuid: &str) -> String
One immutable generation of the entries.
Zero-padded seq FIRST, so a plain lexical listing is also
chronological — what the reaper and a human with mc ls both
want — and the writer’s flush uuid after it, so the key is
unique per WRITE rather than per generation. Two writers that
reach the same seq therefore write two different objects and
race only at the pointer, which is the one place a race should
be decided; and a path that legitimately rewrites without
bumping is not refused by a write-once key it never meant to
collide with.
Sourcepub fn chunk_key(&self, addr: &str) -> String
pub fn chunk_key(&self, addr: &str) -> String
A chunk object, addressed by CONTENT rather than by generation. Two generations that share a chunk share the object, which is where the O(changed) publish actually comes from — naming chunks by seq would rewrite every one of them on every publish even when their contents were identical.
pub fn inbox_key(&self) -> String
pub fn epoch_key(&self) -> String
Sourcepub fn claim_key(&self) -> String
pub fn claim_key(&self) -> String
The operator’s claim cell for this prefix (lean_operator:: reconcile::claim_key writes it; the syncer only READS it).
pub fn conflict_key(&self, uuid: &str, path: &str) -> String
Sourcepub fn draft_body_key(&self, user: &str, path: &str) -> String
pub fn draft_body_key(&self, user: &str, path: &str) -> String
A per-user DRAFT: a durable edit that has NOT been published.
Under LEAN_DIR, which is the whole safety argument: that
namespace is never scanned, never checked out, never cited by a
manifest, and never swept (both sweeps are prefix-scoped —
manifest::sweep_generations to manifests/, sweep_chunks to
chunks/). So a draft is durable in the bucket while being
invisible to classify, to checkout, to every other user, and
to the agent — which is exactly what “saved but not published”
has to mean.
Body and meta live under DISJOINT subtrees rather than sharing
one with a suffix: with drafts/<u>/<path>.meta a file legally
named notes.meta would have its BODY at the key that is
notes’s META, and one user’s draft would silently read as
another draft’s base etag.
Sourcepub fn draft_meta_key(&self, user: &str, path: &str) -> String
pub fn draft_meta_key(&self, user: &str, path: &str) -> String
The draft’s base: which version of files/<path> it was edited
against. Written AFTER the body, so the only crash residue is a
body with no meta — an INCOMPLETE draft, which promote refuses
by name rather than publishing against a base it had to guess.
Sourcepub fn draft_meta_prefix(&self, user: &str) -> String
pub fn draft_meta_prefix(&self, user: &str) -> String
Listing prefix for one user’s draft metas (the resume view).
pub fn state_dir(&self) -> PathBuf
Sourcepub fn control_dir(&self) -> PathBuf
pub fn control_dir(&self) -> PathBuf
The workspace-local control namespace (D0).
Trait Implementations§
Source§impl Clone for LeanConfig
impl Clone for LeanConfig
Source§fn clone(&self) -> LeanConfig
fn clone(&self) -> LeanConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more