loonfs_core/limits.rs
1//! One source of truth for publication, verification, provider, and
2//! garbage-collection timing bounds.
3//!
4//! The GC grace window's safety proof (format spec, "Garbage collection",
5//! rule 1) is an inequality over these constants: every publication measures
6//! itself against a budget here and refuses to publish its root once the
7//! budget is spent, provider operations consume one deadline across retries,
8//! and the minimum grace window is derived — not tuned — from those bounds
9//! plus a margin for provider-timestamp skew. Callers may configure a larger
10//! grace window, never a smaller one.
11
12use loonfs_objectstore::{PROVIDER_ATTEMPT_TIMEOUT, PROVIDER_OPERATION_DEADLINE};
13
14/// Maximum semantic operations in one explicit commit, bounding how long one
15/// request can occupy the serialized publisher during planning and
16/// materialization.
17pub const MAX_COMMIT_OPERATIONS: usize = 4096;
18
19/// Maximum content-token or prepared-proof entries carried by one explicit
20/// commit, bounding preparation work for a new primary. An oversized
21/// candidate may occupy a publisher queue slot until candidate preparation
22/// rejects it.
23pub const MAX_COMMIT_CONTENT_TOKENS: usize = 4096;
24
25/// Maximum distinct new external content refs in one explicit commit, bounding
26/// its in-memory coverage work while it occupies the serialized publisher.
27pub const MAX_COMMIT_EXTERNAL_CONTENT_REFS: usize = 4096;
28
29/// Maximum byte length of a commit's optional `message` annotation, which is
30/// stored in every durable WAL record, hashed into the mutation fingerprint,
31/// and replayed by the change feed. This is the only bound on the message:
32/// no transport-level body limit is relied on.
33pub const MAX_COMMIT_MESSAGE_BYTES: usize = 4096;
34
35/// Maximum attempts for a bounded compare-and-swap or allocation contention loop.
36pub const CONTENTION_RETRY_LIMIT: usize = 8;
37
38/// Provider operation deadline, in milliseconds (`loonfs-objectstore`
39/// consumes it across every retry of one single-request operation).
40/// Multipart transfers of large immutable payloads carry no
41/// whole-operation clock — their parts are individually time- and
42/// retry-bounded — which leaves the floor derivation below untouched:
43/// every object it times (WAL segments inside the publish budget,
44/// checkpoint records, the root compare-and-swap) is a small control
45/// object on the single-request path, and publications self-enforce their
46/// budgets by wall clock regardless of per-operation deadlines.
47pub const PROVIDER_OPERATION_DEADLINE_MS: u64 = PROVIDER_OPERATION_DEADLINE.as_millis() as u64;
48
49/// One control-plane provider HTTP attempt's request timeout, in
50/// milliseconds. An operation's total wall time is bounded by
51/// `PROVIDER_OPERATION_DEADLINE_MS + PROVIDER_ATTEMPT_TIMEOUT_MS`, because the
52/// deadline gates starting an attempt rather than preempting one.
53pub const PROVIDER_ATTEMPT_TIMEOUT_MS: u64 = PROVIDER_ATTEMPT_TIMEOUT.as_millis() as u64;
54
55/// Self-enforced budget between starting a WAL segment PUT and initiating
56/// the head compare-and-swap. Overrunning it abandons the segment instead of
57/// publishing a stale-timed one. Local monotonic elapsed time only — never a
58/// validity input (format spec, "WAL head").
59pub const WAL_PUBLISH_BUDGET_MS: u64 = 60_000;
60
61/// Self-enforced budget between writing a checkpoint record and completing
62/// its post-write basis verification. Overrunning it counts as verification
63/// failure: the record may have raced the grace window, so it must not stand
64/// as a root.
65pub const CHECKPOINT_VERIFY_BUDGET_MS: u64 = 60_000;
66
67/// Self-enforced budget for one metadata publication — WAL flush or
68/// reorganization — measured from before the first table object is written
69/// until the root compare-and-swap is initiated. A publication that exceeds
70/// it aborts without publishing; its immutable outputs remain unreachable
71/// garbage-collection candidates.
72pub const METADATA_PUBLICATION_BUDGET_MS: u64 = 15 * 60 * 1000;
73
74/// Margin absorbing provider-timestamp skew against the GC caller's clock,
75/// plus scheduling slop around the budget checks.
76pub const GC_SAFETY_MARGIN_MS: u64 = 3 * 60 * 1000;
77
78/// Default candidate budget for one step-driven garbage-collection pass.
79pub const DEFAULT_GC_MAX_OBJECTS: u64 = 1024;
80
81const fn max_u64(left: u64, right: u64) -> u64 {
82 if left > right {
83 left
84 } else {
85 right
86 }
87}
88
89/// The derived minimum GC grace window and explicit namespace-repair safety
90/// window (format spec, "Garbage collection", rule 1). Every acknowledged
91/// publication starts its final compare-and-swap within a publication budget
92/// measured from its first object write, and that compare-and-swap completes
93/// within one provider operation bound. So an object older than this window
94/// that is still unreachable at delete time cannot belong to a publication
95/// that might yet succeed. `GcConfig::validate` rejects smaller windows, and
96/// namespace repair uses the same bound before reaping non-completable install
97/// debris.
98pub const GC_MIN_GRACE_WINDOW_MS: u64 = max_u64(
99 max_u64(WAL_PUBLISH_BUDGET_MS, CHECKPOINT_VERIFY_BUDGET_MS),
100 METADATA_PUBLICATION_BUDGET_MS,
101) + PROVIDER_OPERATION_DEADLINE_MS
102 + PROVIDER_ATTEMPT_TIMEOUT_MS
103 + GC_SAFETY_MARGIN_MS;
104
105/// Most parts one direct multipart upload may cut into. This is the
106/// S3-compatible ceiling, so with the session's part size it fixes the
107/// largest object that session can carry: `part_size_bytes × 10_000`.
108pub const MAX_MULTIPART_PARTS: u32 = 10_000;
109
110/// Smallest part size a `direct_multipart` session may be opened with.
111/// Every supported provider refuses a non-final part below 5 MiB.
112pub const MIN_MULTIPART_PART_BYTES: u64 = 5 * 1024 * 1024;
113
114/// Largest part size a `direct_multipart` session may be opened with.
115/// Every supported provider refuses a part above 5 GiB.
116pub const MAX_MULTIPART_PART_BYTES: u64 = 5 * 1024 * 1024 * 1024;
117
118/// Most part-upload capabilities one request may ask for. A client asks in
119/// waves as it works through a file, so this bounds one response rather than
120/// one upload.
121pub const MAX_SIGNED_PARTS_PER_REQUEST: usize = 1_000;
122
123/// Lease every fork attempt takes on the fork-owned source checkpoint it
124/// creates. An attempt that never installs its target head lets the lease
125/// pass, and garbage collection releases the record on that alone — no
126/// provider timestamp, no fork-specific age rule.
127///
128/// Two grace floors, because a fork attempt is two of the things that floor
129/// already bounds. The first covers creating the record: the WAL flush and
130/// manifest publication it may perform, the post-write basis verification,
131/// and the provider bounds and clock skew around them — which is exactly
132/// what `GC_MIN_GRACE_WINDOW_MS` is the bound for. The second covers
133/// everything after: reading the pinned manifest and the source head,
134/// installing the target head, and the guard read below. Each half is one
135/// publication plus provider bounds plus skew, so each is one floor.
136pub const FORK_CHECKPOINT_LEASE_MS: u64 = 2 * GC_MIN_GRACE_WINDOW_MS;
137
138/// Lease an upload session takes when it opens. A session is the only place
139/// retry idempotency lives, so the lease has to outlast any single transfer a
140/// client may reasonably be in the middle of — a proxied body, or a presigned
141/// direct write and the completion call after it. Once it passes, the session
142/// is abandoned by definition and upload garbage collection aborts it: unlike
143/// a namespace, a session is a lease, so reclaiming one on age alone is the
144/// correct reading and not a guess about the client.
145pub const UPLOAD_SESSION_LEASE_MS: u64 = 24 * 60 * 60 * 1000;
146
147/// How long one minted content receipt admits the content it names at commit.
148///
149/// Short on purpose: durability lives in the completed upload session, which
150/// is durable and re-mints, so the receipt only has to cover the gap between
151/// finishing an upload and committing the metadata that references it.
152pub const CONTENT_RECEIPT_TTL_MS: u64 = 60 * 60 * 1000;
153
154/// How long a completed session goes on minting receipts for its content.
155///
156/// This is the "a lost publish response never costs a retransfer" promise
157/// expressed as a number: for this long after completion, reading the
158/// session's status hands back a fresh receipt for bytes that are already
159/// durable. After it, the content is either referenced by metadata — which
160/// protects it on its own — or reclaimable.
161pub const COMPLETED_UPLOAD_RECEIPT_WINDOW_MS: u64 = 7 * 24 * 60 * 60 * 1000;
162
163/// Grace a completed upload's content object gets before content garbage
164/// collection may reclaim it as unreferenced. Derived, not tuned.
165///
166/// Three spans have to be over before "no metadata references this" can be
167/// trusted to stay true. The session may mint a receipt at any point in its
168/// receipt window; the last such receipt admits a commit for one more receipt
169/// TTL; and a commit admitted at that last instant still has a publication
170/// budget plus provider bounds and clock skew to land its root — which is
171/// exactly what `GC_MIN_GRACE_WINDOW_MS` bounds. Past their sum no new
172/// reference can appear, so a reference set collected earlier in the pass is
173/// still sound at delete time.
174pub const CONTENT_RECLAMATION_GRACE_MS: u64 =
175 COMPLETED_UPLOAD_RECEIPT_WINDOW_MS + CONTENT_RECEIPT_TTL_MS + GC_MIN_GRACE_WINDOW_MS;
176
177/// The grace floor's inequality, shared by the compile-time assertion below
178/// and the test that proves the assertion has teeth.
179const fn outlasts_every_receipt(grace_ms: u64) -> bool {
180 grace_ms >= COMPLETED_UPLOAD_RECEIPT_WINDOW_MS + CONTENT_RECEIPT_TTL_MS + GC_MIN_GRACE_WINDOW_MS
181}
182
183// Content reclamation is the one sweep that deletes bytes a user handed us,
184// and its safety is an inequality over the constants above rather than a
185// judgement call, so it is checked where a broken derivation is a compile
186// error instead of a test failure.
187const _: () = assert!(
188 outlasts_every_receipt(CONTENT_RECLAMATION_GRACE_MS),
189 "content reclamation must outlast the last receipt a completed session can mint, \
190 the commit that receipt admits, and that commit's publication"
191);
192
193/// Margin the post-publish fork guard requires between now and the source
194/// record's lease expiry before it lets a target stand.
195///
196/// The guard's evidence is one record read, and one provider operation's
197/// total wall time is `PROVIDER_OPERATION_DEADLINE_MS +
198/// PROVIDER_ATTEMPT_TIMEOUT_MS` (the deadline gates starting an attempt
199/// rather than preempting one). A record observed with more lease than that
200/// left cannot have been legally expiry-released while the guard was
201/// looking at it, so the observation still holds when the guard acts on it.
202pub const FORK_GUARD_MARGIN_MS: u64 = PROVIDER_OPERATION_DEADLINE_MS + PROVIDER_ATTEMPT_TIMEOUT_MS;
203
204// The fork lease has two jobs, and both are inequalities over the constants
205// above rather than judgement calls, so they are checked where a broken
206// derivation is a compile error instead of a test failure: it must cover a
207// whole fork attempt, and it must leave the guard something to check.
208const _: () = assert!(
209 FORK_CHECKPOINT_LEASE_MS >= GC_MIN_GRACE_WINDOW_MS,
210 "a fork attempt may take as long as any other publication"
211);
212const _: () = assert!(
213 FORK_GUARD_MARGIN_MS < FORK_CHECKPOINT_LEASE_MS,
214 "a fork that finishes promptly must still clear the guard margin"
215);
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::gc::GcConfig;
221
222 /// The compile-time assertion above is only worth having if its
223 /// predicate can fail, so the predicate is exercised from both sides
224 /// here: one millisecond below the derivation is rejected.
225 #[test]
226 fn the_content_grace_floor_rejects_a_window_one_receipt_short() {
227 assert!(outlasts_every_receipt(CONTENT_RECLAMATION_GRACE_MS));
228 assert!(!outlasts_every_receipt(CONTENT_RECLAMATION_GRACE_MS - 1));
229 assert!(!outlasts_every_receipt(
230 COMPLETED_UPLOAD_RECEIPT_WINDOW_MS + CONTENT_RECEIPT_TTL_MS
231 ));
232 // 7 days of re-minting + 1 hour of receipt life + 20.5 minutes of
233 // publication.
234 assert_eq!(CONTENT_RECLAMATION_GRACE_MS, 608_400_000 + 1_230_000);
235 }
236
237 #[test]
238 fn derived_minimum_grace_window_sits_below_the_default() {
239 // 15 min publication + 2 min provider deadline + 30 s attempt
240 // timeout + 3 min margin = 20.5 minutes.
241 assert_eq!(GC_MIN_GRACE_WINDOW_MS, 1_230_000);
242 assert!(
243 GC_MIN_GRACE_WINDOW_MS < GcConfig::default().grace_window_ms,
244 "the conservative default grace window must satisfy its own floor"
245 );
246 assert!(
247 GC_MIN_GRACE_WINDOW_MS
248 > max_u64(
249 max_u64(WAL_PUBLISH_BUDGET_MS, CHECKPOINT_VERIFY_BUDGET_MS),
250 METADATA_PUBLICATION_BUDGET_MS,
251 ) + PROVIDER_OPERATION_DEADLINE_MS,
252 "the floor keeps a margin above budget plus provider deadline"
253 );
254 }
255}