* chore: pin s3s to upstream git revision Use the upstream s3s git repository at 50c94aeb1e5ea9ef8e7393ef6bbc87b72f6891c6 for both s3s and s3s-sigv4. Allow the official s3s git source in cargo-deny so source checks continue to pass. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * perf(ecstore): instrument put object hot path Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * perf(ecstore): split cleanup worker wait metrics Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * upgrade version * chore(metrics): add PUT post-store stage probes Add lightweight PUT post-store stage probes for issue 2615 validation. * fix cargo deny check warn * upgrade version * perf(ecstore): bound and isolate cleanup execution (#8151) * perf(ecstore): bound and isolate cleanup execution * fix(ecstore): preserve cleanup cancellation and lease release * feat(ecstore): default isolated cleanup to CPUs 1-2 * test(ecstore): decouple budget tests from CPU affinity * upgrade version * perf(ecstore): reuse pinned directory handles for object I/O * feat(observability): add sampled local GetObject stage summaries Add an opt-in GetObject stage aggregation path that works without the OTLP metrics export. When RUSTFS_OBS_GET_STAGE_LOCAL_SUMMARY_ENABLED is set, io-metrics keeps per-(path, stage, object class, size bucket) sampled counts and duration totals in a bounded in-process map, and a startup task flushes them periodically to /var/log/rustfs/get-stage-summary.log, tunable via the sample rate and interval env keys. * build(deps): bump workspace dependency releases Patch releases: async-compression 0.4.49, tokio-rustls 0.26.6, xxhash-rust 0.8.19, suppaftp 12.1.1, russh-sftp 3.0.1. Minor release: hotpath 0.27.0. * fix(ecstore): fence distributed metadata cache mutations Add authenticated cluster-wide mutation fences for the opt-in DistErasure GetObject metadata cache. Guard recursive prefix and batch deletes, fail closed on configuration mismatch or uncertain quorum, and cover replay, restart, peer loss, and mixed-configuration behavior. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * refactor(metrics): consolidate PUT observers on improve/base (#8039) * perf(object): instrument storage hot paths Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * refactor(metrics): consolidate PUT stage observation Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: zhi22915 <qiuzgang@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> * build(deps): bump workspace dependency releases * ci: allowlist rustix OFlags WRONLY in typos config * refactor(storage): route ecstore symbols through storage_api boundaries * perf(ecstore): cache quorum-selected early-stop metadata (#8308) Publish eligible latest-version GET metadata after a validated read quorum when early-stop cancels the remaining fanout. Keep metadata errors, selection quorum, online read quorum, and mutation-generation checks as publication gates. Add slow-peer repeat-hit and concurrent mutation-fence regression tests. Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * chore: merge latest main into improve/base (#8332) * refactor(storage): bind rename rollback to its commit context (#8324) * refactor(storage): separate metadata scheduling from quorum decisions (#8327) * test(storage): verify rejected rollback inspection on real disks (#8331) * test(storage): make commit fault checkpoints independent (#8326) * test(storage): make commit fault checkpoints independent * test(storage): simplify commit checkpoint lookup * fix(listing): count logical prefixes before pagination (#8328) * fix(listing): count logical prefixes before pagination * test(listing): normalize the fixed corpus seed --------- Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * perf(ecstore): borrow metadata cache lookup key (#8333) Avoid request-scoped Arc<str> allocations on eligible cache lookups while preserving full key identity. Co-authored-by: zhi22915 <qiuzgang@gmail.com> * chore: sync improve/base with latest main (#8334) * refactor(storage): bind rename rollback to its commit context (#8324) * refactor(storage): separate metadata scheduling from quorum decisions (#8327) * test(storage): verify rejected rollback inspection on real disks (#8331) * test(storage): make commit fault checkpoints independent (#8326) * test(storage): make commit fault checkpoints independent * test(storage): simplify commit checkpoint lookup * fix(listing): count logical prefixes before pagination (#8328) * fix(listing): count logical prefixes before pagination * test(listing): normalize the fixed corpus seed * fix(scanner): isolate and coalesce dirty usage journals (#8325) * fix(scanner): isolate and coalesce dirty usage journals Bind dirty replay records to a stable node owner so peers cannot overwrite or delete another node's pending journal. Replace the unbounded command queue with bounded coalesced state and revision-aware background saves. Keep legacy records isolated and enforce replay byte limits with conservative whole-bucket or unverified fallback. Add publication-stage diagnostics and storage regressions for ownership, clearing, retry, and restart behavior. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(scanner): collapse confirmed journal clear condition Use a let-chain to retain the generation fence before removing a confirmed bucket and updating its scope byte accounting. Satisfy collapsible_if without changing journal clearing behavior. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * fix(obs): distinguish metrics by process identity (#8343) Attach process.pid to the OpenTelemetry resource and promote it for Prometheus so counter series from restarted processes remain distinct. Preserve the stable RustFS node and service instance attributes. Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * merge: sync origin/main into improve/base (#8345) * refactor(storage): bind rename rollback to its commit context (#8324) * refactor(storage): separate metadata scheduling from quorum decisions (#8327) * test(storage): verify rejected rollback inspection on real disks (#8331) * test(storage): make commit fault checkpoints independent (#8326) * test(storage): make commit fault checkpoints independent * test(storage): simplify commit checkpoint lookup * fix(listing): count logical prefixes before pagination (#8328) * fix(listing): count logical prefixes before pagination * test(listing): normalize the fixed corpus seed * fix(scanner): isolate and coalesce dirty usage journals (#8325) * fix(scanner): isolate and coalesce dirty usage journals Bind dirty replay records to a stable node owner so peers cannot overwrite or delete another node's pending journal. Replace the unbounded command queue with bounded coalesced state and revision-aware background saves. Keep legacy records isolated and enforce replay byte limits with conservative whole-bucket or unverified fallback. Add publication-stage diagnostics and storage regressions for ownership, clearing, retry, and restart behavior. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(scanner): collapse confirmed journal clear condition Use a let-chain to retain the generation fence before removing a confirmed bucket and updating its scope byte accounting. Satisfy collapsible_if without changing journal clearing behavior. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * fix(storage): retain typed owners across borrowed writes (#8329) * fix(storage): retain typed owners across borrowed writes * fix(storage): preserve Copy write completion policy * refactor(storage): remove redundant write owner references * fix(ecstore): hide rollback backup residue in prefix listings * test(connect): isolate short HTTP fixtures in nextest * fix(protocols): enforce S3 policies for file protocol operations (#8262) * test(ecstore): enforce main core regression gates (#8338) * test(ecstore): enforce main core regression gates * test(ecstore): pin external shards and completion regressions * fix(test): restore authenticated cluster fault scenarios (#8340) * fix(test): restore authenticated cluster fault scenarios * fix(test): use the E2E storage API boundary * ci: coerce the pool timeout input to a number timeout-minutes requires a numeric value, but inputs.pool_timeout_minutes is a string, so '${{ inputs.pool_timeout_minutes || 240 }}' evaluated to the string '240' and the runner rejected the template: 'An error occurred when attempting to determine the step timeout ... Unexpected value 240' (chain run 36505625402 job at 02:25:52, standalone run 36393453114 at 07:46:37). Both pool runs since #8155 merged have therefore executed without the intended step budget. Wrap the expression in fromJSON and update the policy test that pinned the broken literal. * ci: coerce the pool timeout input to a number timeout-minutes requires a numeric value, but inputs.pool_timeout_minutes is a string, so '${{ inputs.pool_timeout_minutes || 240 }}' evaluated to the string '240' and the runner rejected the template: 'An error occurred when attempting to determine the step timeout ... Unexpected value 240' (chain run 36505625402 job at 02:25:52, standalone run 36393453114 at 07:46:37). Both pool runs since #8155 merged have therefore executed without the intended step budget. Wrap the expression in fromJSON and update the policy test that pinned the broken literal. * fix(ecstore): own listing workers through cancellation (#8342) Consolidate the three page collectors, preserve completed worker failures, and abort nested merges when their owner is dropped. Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): route commit sync through guarded helper Use the Linux-capable guarded directory sync entry point for both commit sync sites. This keeps group commit and file-sync admission behavior while ensuring the already-open commit directory remains the sync target. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): expose rename directory guard in production The commit path now acquires the directory guard in production builds, not only tests, so destination metadata reads and durable sync share the guarded context. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> Co-authored-by: GatewayJ <835269233@qq.com> Co-authored-by: hector <42570491+majinghe@users.noreply.github.com> * fix(ecstore): remove unsafe metadata cache admission paths Keep distributed GET metadata on quorum reads and quota admission on authoritative storage. Preserve local cache invalidation, remove the experimental RPC surface, and gate diagnostic work. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(ecstore): cover cold degraded mutation admission Check first PUT and DELETE with one peer absent and no SDK retries, then verify that losing write quorum still rejects writes. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): retain guarded Linux destination metadata reads Use the existing commit directory guard when reading old xl.meta on Linux. Cover symlinked destination metadata for inline and non-inline renames without changing the split commit structure. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * chore(test): remove retired cache RPC import Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * merge: sync latest main into improve/base (#8349) * test: allow cleanup fixture CI headroom (#8336) * fix(s3): encode multipart listing response keys (#8347) --------- Co-authored-by: cxymds <cxymds@gmail.com> Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * merge: sync main auth and scanner updates (#8352) * test: allow cleanup fixture CI headroom (#8336) * fix(s3): encode multipart listing response keys (#8347) * perf(scanner): reduce scan and checkpoint peak allocations (#8346) * perf(scanner): reduce scan and checkpoint peak allocations * test(scanner): isolate partial commit recovery scenarios * fix(auth): return ExpiredToken for retained STS credentials (#8344) * fix(auth): return ExpiredToken for retained STS credentials Validate session identity independently of credential expiry in both IAM rejection and claims validation, and preserve typed S3 errors at the caller. Keep token binding, disabled-account precedence, and service-account behavior. Add exact expiry, IAM, and HTTP/XML regression coverage. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(ecstore): synchronize the metadata slowtail hedge regression Park the slow metadata task at a test-only fault checkpoint and release healthy reads after the fault is active. Assert the spare response and cancelled-task drain without imposing a 300 ms disk/scheduler deadline. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(ilm): synchronize copy compensation transition stages Observe automatic compensation reaching the mock tier PUT checkpoint before waiting for the final metadata commit. Assert forced enqueue failure, automatic backfill, one completed tier upload, and payload integrity. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: cxymds <cxymds@gmail.com> Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): reject overlapping cleanup schedulers Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): reject overlapping cleanup schedulers Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * docs(ecstore): explain cleanup scheduler conflict Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(metrics): separate GET request and timeout outcomes (#8354) Co-authored-by: zhi22915 <qiuzgang@gmail.com> * docs: refresh security advisory lessons (#8350) * fix(heal): bound durable MRF rescheduling (#8351) Reuse stable live-owner proof anchors to avoid duplicate in-flight admission. Pace unverified completions and unavailable attempts with bounded jitter while retaining checkpoints, exact proofs, and generation isolation. Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com> * fix(rustfs): route recovery errors through S3 gateway Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * chore(deps): update workspace dependencies Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(io-metrics): use is_multiple_of for sampling Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): satisfy strict Clippy pagination checks Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(heal): avoid cloning Copy purge identity Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * add ignore .mimosa * fix(zip): adapt archive readers to zip 9 APIs Upgrade zip and its required dependencies alongside fallible archive-name handling in log ingestion, report bundles, bucket metadata import, and archive tests. Preserve existing skip and error propagation behavior. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): reuse quorum metadata for body GETs Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(ci): align storage test stacks and peer reset classification Apply the existing bounded ECStore stack setup to the affected cross-crate storage fixtures. Validate conditional-only metadata publication separately from full body-layout cache reuse. Classify typed h2 shutdown resets as unreachable while retaining application, authentication, cancellation, and deadline distinctions. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * chore(deps): update workspace dependencies Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(ecstore): preserve omitted-disk GET recovery Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * chore: integrate published workspace dependency updates Preserve the concurrent dependency refresh and retain the typed h2 reset classification and bounded storage-test stack setup. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * test(ecstore): preserve snapshot size budget after fallback move * fix(test): use one barrier for compensation COPY Let the forced-enqueue helper own the PUT barrier and compensation assertions. Waiting on an outer barrier overwritten by the helper always timed out after the upload had completed. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * chore(ecstore): apply current lint fixes Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * chore(deps): update sha2 to 0.11.1 Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: zhi22915 <qiuzgang@gmail.com> Co-authored-by: GatewayJ <835269233@qq.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: hector <42570491+majinghe@users.noreply.github.com> Co-authored-by: cxymds <cxymds@gmail.com>
13 KiB
PUT and disk cleanup isolation
Cleanup can use process-wide admission budgets and a dedicated Tokio runtime. This is opt-in. It preserves cleanup receipts, eligible-data moves to trash, PUT temporary workspace cleanup, and periodic physical trash removal. It does not disable healing or skip cleanup.
Configuration
| Variable | Default | Meaning when isolation is enabled |
|---|---|---|
RUSTFS_CLEANUP_ISOLATE_ENABLE |
false |
Enable admission and execution isolation on this node. |
RUSTFS_PUT_RENAME_TAIL_CLEANUP_MAX_PENDING |
1024 |
Maximum admitted PUT ownership groups, including surviving commit and cleanup owners. |
RUSTFS_PUT_RENAME_TAIL_CLEANUP_DEFER_WORKERS |
2 |
Maximum active PUT cleanup continuations. Also controls existing deferred queue workers; does not determine OS thread count. |
RUSTFS_CLEANUP_ASYNC_THREADS |
up to 2 |
Dedicated async worker threads, limited by available parallelism and an explicit CPU mask by default. Independent of logical job concurrency. |
RUSTFS_CLEANUP_DISK_MAX_PENDING |
16 × disk workers (64) |
Maximum admitted local disk cleanup operations, including queued and running operations. Shared by local requests, incoming RPC cleanup and periodic GC steps. |
RUSTFS_CLEANUP_DISK_WORKERS |
blocking threads (4) |
Maximum active disk cleanup operations within that budget. |
RUSTFS_CLEANUP_GC_WORKERS |
1 |
Maximum periodic disk scans producing GC steps. |
RUSTFS_CLEANUP_BLOCKING_THREADS |
4 |
Maximum additional blocking threads on the cleanup runtime. |
RUSTFS_CLEANUP_CPUS |
1-2 on Linux; unbound elsewhere |
CPU affinity for cleanup async and blocking threads. Override with a CPU list such as 4-5,8, or none to disable affinity while keeping isolation. |
RUSTFS_PUT_RENAME_TAIL_CLEANUP_DEFER_ENABLE |
false |
Use the existing bounded deferred queue before coordinator execution. Isolation also works without this queue. |
RUSTFS_PUT_RENAME_TAIL_CLEANUP_DEFER_QUEUE_CAPACITY |
1024 |
Capacity of that existing queue; this is not the total admission budget. |
Counts must be positive. PUT workers must not exceed PUT pending capacity; GC workers must not exceed disk workers, which must not exceed disk pending capacity. Async and blocking thread counts must not exceed 1024. CPU IDs must be supported by the affinity implementation and belong to the initiating thread's allowed CPU set. Non-Linux hosts reject explicit CPU masks. Configuration is read once at first use, including disk cleanup or GC; changes require a restart. Invalid configuration rejects new cleanup admission. PUT checks configuration before staging data.
Isolation remains disabled by default. When enabling it on Linux, the default
mask requires logical CPU IDs 1 and 2 to be available inside the process/container
CPU set. On smaller or restricted CPU sets, configure permitted CPU IDs or none
before enabling isolation. An unavailable default mask is an error; it does not
silently select different CPUs. Configure each node separately.
Do not combine isolation with cleanup counterfactuals
RUSTFS_PUT_RENAME_TAIL_CLEANUP_COUNTERFACTUAL_SKIP,
RUSTFS_PUT_RENAME_TAIL_CLEANUP_DEFER_HOLD_WORKER, or
RUSTFS_PUT_RENAME_TAIL_CLEANUP_ZERO_TARGET_TMP_DELETE_SKIP. These combinations
are rejected because they deliberately suppress cleanup or stop queue progress.
Do not enable RUSTFS_PUT_RENAME_TAIL_CLEANUP_LANE_ENABLE together with
RUSTFS_CLEANUP_ISOLATE_ENABLE. The legacy per-disk lane adds a second queue
inside the isolated coordinator budget, so a slow lane can hold coordinator
capacity while unrelated cleanup waits. This combination is rejected before
PUT admission; either scheduler can still be enabled on its own.
Ownership and execution
PUT attempts admission before staging. Full admission returns SlowDown, so
clients should retry with backoff. Admission does not wait: COPY readers and
data-movement callers can already hold locks needed by admitted PUTs. No body
is ingested or tmp data staged by a rejected PUT.
The request, detached commit owner, and cleanup continuations share one reservation. An early ACK or cancelled waiter cannot release it while an owner still needs cleanup. After the existing rename-convergence and guard-release handoff, deferred workers, queue overflow and direct continuations all acquire the same coordinator execution semaphore. Queue overflow cannot bypass that limit. Awaited failure cleanup and no-tail tmp cleanup use the same budget. Full-tail receipt persistence remains awaited at its existing handoff point; old-data reclamation remains after the existing guard release. Incomplete rollback still preserves staging for recovery.
Each local disk operation has separate pending and active permits. The wrapper
below RPC authentication uses this gate for low-level delete, delete_data_dir
and old-data cleanup receipt write_all. Local PUT fan-out and incoming peer
RPCs therefore share the same node-local disk budget. Receipt classification
uses the internal receipt filename; ordinary writes retain their current path.
RPC schemas and authentication are unchanged. Enable isolation on every node
to isolate peer execution; settings and CPU masks are not transmitted by RPC.
Coordinator and receiver permits are deliberately distinct: a coordinator can
wait for peer RPCs without occupying the slots those peers need to service disk
cleanup. Full disk admission returns an I/O WouldBlock error through the
existing disk error path. A mutation carrying an external publication or
namespace owner also refuses a full active budget instead of waiting while it
holds a lock. Existing cleanup error handling and receipt recovery still apply.
Ordinary snapshot-lease release stays on the caller and only updates the in-memory registry. It does not allocate a cleanup task or wait for the cleanup scheduler. Invalid cleanup configuration also must not strand a reader token: release still updates the registry, while any deferred reclamation reports the initialization error and keeps both its intent and data. It does not fall back to unbudgeted I/O. If the last release triggers deferred old-data reclamation, that reclamation tries the same pending and active disk permits. Only this case dispatches an owned task to the cleanup runtime. Saturation leaves the token released and the cleanup intent/receipt available for retry; it does not strand a live reader token or bypass the budget. Quota fence release keeps its original receiver lifecycle and does not acquire disk execution permits, since it can wait for mutations that already own them.
An admitted request waits for disk execution capacity in the caller's existing future. Cancellation or timeout discards this unstarted operation and releases pending capacity; it cannot later start deleting a reused path. After execution capacity is acquired, the worker also checks whether its result receiver closed before its first poll. A job cancelled while waiting for the runtime to schedule it is discarded when the worker polls it, releasing its permits without starting I/O. Once the worker starts the operation, the owned job retains its permits and disk reference until completion. Cancelling or timing out its waiter does not free disk capacity while that started job still runs. This also retains the disk's mount ownership during detached execution. Cleanup waiter timeouts include queue delay and do not by themselves mark a disk faulty; the existing active disk-health probe retains its timeout policy.
Periodic trash and stale-tmp scans have a separate scan limit. Each scan acquires the shared disk budget to open its directory and then for each entry, releasing it between entries. The steps borrow the scan's iterator and paths and run in the same owned task, without a spawn/join or path clones per entry. A scan cannot occupy a disk execution slot while waiting for another scan slot. GC waits for admission without holding object locks; request admission is bounded without creating waiting producer tasks. A single trash subtree is still removed as one operation, so a very large subtree can hold a slot for its entire removal. Queued scans retain the original mount handle even when the corresponding disk object is dropped. Missed periodic ticks are skipped so a slow scan does not cause a burst of catch-up scans.
Cleanup runs on rustfs-cleanup threads. Nested Tokio tasks, Tokio filesystem
work and ordinary spawn_blocking use that runtime. Explicit fsync dispatch
also stays on its blocking pool. File/directory group-commit keys separate
cleanup and foreground producers, preventing batches from moving into the
other class's runtime. Fsync durability settings remain unchanged.
The blocking limit bounds simultaneous blocking execution, not Tokio's internal queue or kernel I/O. Disk admission bounds selected operations, not bytes, syscall count or device IOPS. Existing synchronous path probes and disk-full delete fallbacks can still run on the dedicated async workers, under the disk operation budget. All classes still share the filesystem, journal, page cache and devices with foreground work.
If CPU binding fails at thread startup, new admissions fail and the failure is logged; already-admitted cleanup can finish. Affinity does not reserve exclusive cores, move foreground threads elsewhere, change cgroups, or control kernel workers and interrupt affinity.
Scope and recovery
The limits are per process, not cluster-wide quotas. This is not a durable work queue. Existing cleanup receipts and tmp/trash recovery retain their role after process exit. The option does not bound all storage mutations: metadata/version DELETE transactions, heal/scanner coordination and multipart orchestration retain their scheduling. Calls from those paths that reach the selected disk wrapper entries do share the disk budget; internal LocalDisk operations that bypass those entries do not.
Measurement and tuning
On Linux, cargo test -p rustfs-ecstore --test cleanup_isolation_test exercises
the public disk facade, actual thread affinity, receipt/delete execution and
periodic physical trash removal with temporary disks. It does not benchmark
throughput.
With PUT stage metrics enabled, observe:
rustfs_cleanup_admitted_putsandrustfs_cleanup_admission_rejected_total;rustfs_cleanup_execution_wait_seconds;rustfs_cleanup_disk_pending,rustfs_cleanup_disk_active, andrustfs_cleanup_disk_admission_rejected_total;rustfs_cleanup_disk_execution_wait_secondsandrustfs_cleanup_gc_active;- existing deferred queue depth, age, overflow, completion and service time.
Compare isolation disabled, budget/pool isolation without CPU affinity, and isolation with affinity. Keep durability, object sizes, overwrite ratio, concurrency and cluster placement fixed. Measure PUT/GET throughput and p95/p99 latency, rejections, queue age, receipt/trash growth, per-disk I/O latency, CPU usage and context switches. Include a long steady-state interval covering physical GC, and verify backlog drains afterward.
Start with small worker and blocking-thread limits, then increase only when cleanup cannot keep up without harming foreground latency. Too few workers or CPUs can increase rejections and lower PUT throughput. Larger queues do not increase sustainable device throughput. Affinity helps only when CPU scheduling contention matters to the measured workload; no throughput gain is guaranteed.
The defaults protect a node; they are not a claim of optimal production throughput. The default disk queue permits sixteen service waves, rather than 256 waves with 1024 pending operations and four workers. For illustration, at a uniform 100 ms per operation, these bounds correspond to roughly 1.6 s versus 25.6 s to drain a full queue with no new arrivals. Real service times, stalls and disk fan-out vary; there is no queue latency guarantee. The ordinary disk waiter timeout defaults to 30 s and does not cancel already-admitted work.
Size pending capacity for measured arrival bursts and local/peer fan-out, not only for PUT request concurrency. For example, two coordinator jobs each touching 16 local disks can submit 32 disk operations before incoming peer requests and GC are counted. Sixty-four slots need not absorb every distributed burst. Increase the explicit limit when measured rejection rates require it, while keeping queue age comfortably below waiter deadlines. On many-disk nodes, measure whether four active operations underutilize devices before increasing disk/blocking workers. The 1024 PUT admission limit includes uploading requests and detached owners; it is an object-count bound, not a memory or byte budget. The older deferred queue remains optional and disabled by default; its overflow path cannot bypass the new coordinator or disk limits. No unbounded foreground fallback is introduced.