Compare commits

..

1898 Commits

Author SHA1 Message Date
唐小鸭 d69d682193 Refactor code structure for improved readability and maintainability 2026-07-24 14:01:40 +08:00
唐小鸭 5ea9a1fd8f fix(sse): separate SSE-S3 and KMS key providers 2026-07-22 21:05:23 +08:00
houseme a044d11443 chore(deps): refresh cargo dependencies (#5132)
Update selected workspace dependencies and lockfile entries.

Keep async-nats and rcgen on explicit feature sets while preserving the RustFS targets and TLS test surfaces.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 21:02:59 +08:00
houseme e1e6a8b020 fix(tier): gate exact remote version consumption (#5126)
* fix(tier): gate exact remote version consumption

Reject non-empty remote tier versions before transitioned GET and remote delete backend I/O when the tier backend does not support exact version operations.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(tier): split mock remote version validation fault

Separate one-shot mock remote version validation failures from persistent unsupported-backend behavior so cleanup durability tests can still verify exact-version recovery while #1358 fail-closed gate tests keep asserting no backend I/O.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 11:52:56 +00:00
houseme 5cfe4ccc7d fix(tier): include persisted refs in tier proof (#5128)
Extend tier mutation reference proof to cover persisted delete journal, transition transaction, and free-version references with one-pass target matching.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 11:31:03 +00:00
houseme df2db15ce8 fix(tier): persist unknown upload outcomes (#5127)
Advance transition transactions to UploadOutcomeUnknown before remote tier PUT so response-loss windows can be recovered through provider-authoritative probing.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 19:14:47 +08:00
houseme 0321e9350d fix(tier): retain prepared intents after abort failure (#5129)
Keep coordinator Prepared intents durable when peer abort recovery fails, then make reload retry abort or commit based on the persisted tier config digest.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 19:12:55 +08:00
houseme cd9a2eecb1 test(tier): fix uring test build fake peer (#5124)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 10:03:50 +00:00
houseme 8b09634e62 fix(tier): fan out coordinator mutations to peers (#5122)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 17:02:57 +08:00
houseme 1ede77b1c1 fix(ilm): recover unknown transition uploads by probing tier (#5120)
* fix(ilm): recover unknown transition uploads by probing tier

Co-Authored-By: heihutu <heihutu@gmail.com>

* style(ilm): format transition recovery assertions

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 17:02:54 +08:00
houseme 666e251b78 test(tier): cover prepared intent scan cleanup (#5121)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 17:02:44 +08:00
houseme 65ba138c27 test(tier): cover mixed-version committed replay (#5119)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 17:02:32 +08:00
Zhengchao An 92ae19b340 fix(targets): isolate invalid target instances instead of failing the whole subsystem (#5118)
A single instance with a malformed `enable` value (e.g. the typo `enable`
instead of `enabled`/`on`) made every configured notification/audit target
of every type fail to load. `create_targets_from_config_with_store_mode`
collected instance configs through the strict `try_collect_target_configs`,
whose `.collect::<Result<Vec, _>>()` short-circuits on the first error and
drops all siblings; the surrounding `?` then aborted the loop over every
plugin type.

This contradicted the function's own documented contract ("Creation is
fault-isolated per instance") and was inconsistent: target construction
failures were already isolated into the `failures` accumulator, only config
collection failures were fatal.

Add `collect_target_config_results[_from_env]`, which returns the enabled
`(id, config)` pairs plus a per-instance failure summary, and use it in the
create path. Per-instance collection errors now flow into the same
`failures`/`creation_failures` accumulator as construction failures: healthy
siblings and other target types still load, while the malformed instance is
surfaced so the notify lifecycle stays non-converged/retryable and an Admin
write cannot report a false success. Audit likewise keeps logging through
its valid targets instead of going fully Stopped.

Follow-up to #5088.
2026-07-22 16:32:16 +08:00
houseme 4607c3be53 fix(tier): recover coordinator mutation intents (#5114)
Persist coordinator tier mutation intents before config CAS, recover prepared coordinator intents whose candidate digest already matches the saved config, and clean finished coordinator records after local publish.

Refs rustfs/backlog#1357

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 07:35:29 +00:00
cxymds e0bac66941 fix(targets): unify runtime health snapshots (#5110)
* fix(targets): unify runtime health snapshots

* fix(targets): stabilize health snapshot merge

* fix(admin): import runtime health test type
2026-07-22 06:50:36 +00:00
houseme 31dc78eab0 feat(tier): probe transition candidates from providers (#5112)
* feat(tier): add transition candidate probe contract

Add a fail-closed WarmBackend probe contract for provider-authoritative transition candidate state. Default providers report Unsupported, while the shared mock backend can now model missing, unversioned, and exact-version candidates for follow-up recovery tests.

This is a forward-compatible foundation for #1352/#1358 recovery work and does not change production cleanup behavior.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(tier): probe transition candidates from providers

Implement provider-authoritative transition candidate probing for S3-family warm backends by querying ListObjectVersions with exact-key filtering and fail-closed classification for delete markers, multiple versions, truncation, and unknown versioning state.

This keeps non-S3 providers on the default Unsupported probe result and forwards MinIO, RustFS, and R2 through the S3 probe implementation.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 06:13:32 +00:00
Zhengchao An daca7294c7 fix(ecstore): stop logging not-found listing quorum miss at error (#5111)
A metacache listing that misses quorum purely because the volume or path
is absent on a quorum of drives (every drive reports VolumeNotFound or
FileNotFound) is a benign, expected outcome: list_path_raw already returns
VolumeNotFound/FileNotFound and lets the caller decide how to react.

The common trigger is a startup race where the system bucket (.rustfs.sys)
is not yet created on every drive when an early reader such as the IAM
config loader lists config/iam/. Logging that at error prints a scary
message during normal boot and, in #5076, misled a user into blaming it
for unrelated upload failures.

Add is_benign_not_found_listing_failure() and demote the pure not-found
case to debug (state = "quorum_not_found"), keeping error for listings
that failed for a real reason (I/O, timeout, corruption). Return value and
control flow are unchanged. Add a unit test for the classifier.

Refs: #5076
2026-07-22 05:26:09 +00:00
cxymds 1655f3192e fix(notify): unify runtime lifecycle coordination (#5088)
* fix(notify): unify runtime lifecycle coordination

* fix(notify): repair lifecycle convergence checks

* fix(admin): expose effective notify state (#5097)
2026-07-22 05:01:15 +00:00
houseme 0adb3c5ea1 fix(tier): gate inexact provider versions (#5109)
Bind provider exact GET and DELETE capability to tier operation leases and reject non-empty remote versions before committing transition metadata when the provider cannot address exact versions.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 12:24:05 +08:00
houseme 68e156a5c5 fix(tier): retain unproven transition candidates (#5108)
Keep LocalCommitStarted transition transaction records retained when the local source cannot prove the commit, so recovery does not classify the record as failed or delete the remote candidate without cleanup proof.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 04:03:33 +00:00
houseme 3f60cc743e fix(tier): replay committed mutation intents (#5107)
Keep recovered tier mutation blocks installed until the local publish transition has atomically established draining for the affected tiers, so old-generation leases cannot slip in after peer commit replay and before local publish.

Delay committed intent cleanup until local publish succeeds. If peer replay succeeds but local publish fails, the durable committed intent remains available for retry and the runtime block stays fail-closed.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 03:06:55 +00:00
houseme 35af4a611f fix(tier): fail closed legacy mutations without etag (#5106)
* fix: fail closed legacy tier mutations without etag

Reject non-add tier config mutations when the loaded durable snapshot has no current config ETag. This keeps legacy JSON/no-ETag paths from being saved as creation-style updates before coordinator intent state can bind an old config revision.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(tier): allow empty clear without config etag

Allow empty tier clears to initialize the binary config under the namespace coordinator lock while keeping non-empty legacy config mutations fail-closed when no current ETag exists.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 02:02:00 +00:00
houseme a8e7cce5e1 feat: expose list read-dir amplification metrics (#5103)
Record local read_dir entry counts and duration for live-walker ListObjects scans so wide root/prefix amplification can be measured below the cross-set merge layer.

Add a focused LocalDisk scan_dir test showing a page limit of one still observes the whole parent directory enumeration, plus metric helper coverage.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 08:15:41 +08:00
Jason Kossis 9469dfa5b8 fix(site-replication): delete replicated buckets 2026-07-22 01:22:41 +08:00
houseme f1d2af698c fix(tier): add mutation peer fanout helpers (#5102)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 17:19:07 +00:00
houseme d5f8c6c044 test(tier): cover zero reference proof matrix (#5100)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 17:17:38 +00:00
houseme f5303bad95 test(tier): cover peer mutation fail-closed statuses (#5101)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 17:15:17 +00:00
houseme cb0d4ffa76 perf: bound list merge prefetch (#5099)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 17:03:23 +00:00
houseme 5b61b030a4 fix(tier): narrow mutation proof targets (#5098)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-22 00:34:22 +08:00
houseme 0fbb5ba87b fix(tier): add peer mutation control client (#5096)
* fix(tier): lock tier config mutations

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(tier): add mutation RPC auth contract (#5082)

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): add peer mutation handler core (#5084)

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): add mutation control rpc service (#5087)

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): recover prepared mutation drains (#5093)

Recover prepared tier mutation intent records into the local tier runtime so a restarted peer fails closed before issuing new remote-tier operation leases or conflicting admin publishes.

Reconcile the recovered block map on each scan so committed, aborted, or removed intents clear stale local blocks instead of wedging the peer until process restart.

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): prove zero references before tier removal (#5092)

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): clear peer mutation runtime blocks (#5094)

Install prepared mutation runtime blocks when peer prepare requests are applied or replayed so followers fail closed immediately before restart recovery.

Clear the in-memory block once peer commit or abort reaches a durable terminal state, including delayed duplicate prepare requests that observe a committed or aborted record.

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): add peer mutation control client

Add signed tier mutation prepare, commit, and abort client calls for peer fanout while preserving the existing protobuf and RPC contract.

Verify response proofs before interpreting peer outcomes, fail closed on invalid states, and reject oversized payloads before dialing peers.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 15:52:46 +00:00
houseme f6e8ce4639 fix: preserve walk-dir internode metrics fallback (#5095)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 15:30:34 +00:00
houseme 937b311316 fix(tier): lock tier config mutations (#5080)
* fix(tier): lock tier config mutations

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(tier): add mutation RPC auth contract (#5082)

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): add peer mutation handler core (#5084)

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): add mutation control rpc service (#5087)

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): recover prepared mutation drains (#5093)

Recover prepared tier mutation intent records into the local tier runtime so a restarted peer fails closed before issuing new remote-tier operation leases or conflicting admin publishes.

Reconcile the recovered block map on each scan so committed, aborted, or removed intents clear stale local blocks instead of wedging the peer until process restart.

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): prove zero references before tier removal (#5092)

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>

* fix(tier): clear peer mutation runtime blocks (#5094)

Install prepared mutation runtime blocks when peer prepare requests are applied or replayed so followers fail closed immediately before restart recovery.

Clear the in-memory block once peer commit or abort reaches a durable terminal state, including delayed duplicate prepare requests that observe a committed or aborted record.

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 23:16:12 +08:00
houseme 62c2f81afd fix(admin): expose cluster diagnostic components (#5090)
Add machine-readable storage, listing, usage, and workload component diagnostics to the admin cluster snapshot response while keeping the external S3 API untouched.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 14:33:10 +00:00
Zhengchao An eed1e97967 fix: update cluster snapshot test for peer health local node semantics (#5089)
The peer health snapshot now reports CapabilityState::Supported for
local nodes (no probing needed), but the cluster_snapshot test still
expected Unknown. Update the assertion to match the new behavior
introduced in commit 7805cf5ae.
2026-07-21 13:02:25 +00:00
GatewayJ 97b618bc2b fix(iam): reject cross-identity access key collisions (#5085)
fix(iam): reject service account access key collisions
2026-07-21 20:36:25 +08:00
houseme 7805cf5ae6 fix(cluster): clarify peer health and listing timeouts (#5086)
* fix(cluster): surface observed peer health

Refs rustfs/backlog#1387

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(admin): distinguish unreported peer health

Refs rustfs/backlog#1388

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): decouple metacache peek timeout

Refs rustfs/backlog#1389

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(ops): diagnose metacache listing timeouts

Refs rustfs/backlog#1390

Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor(cluster): clarify observed peer health

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): route capability state through contract

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 11:11:24 +00:00
Henry Guo bb7bba3237 fix(obs): clarify cluster bucket usage metrics (#5081)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-21 17:23:49 +08:00
GatewayJ 17f0bd2637 fix(iam): report duplicate access keys clearly (#5066)
* fix(iam): report duplicate access keys clearly

* fix(iam): narrow duplicate access key handling
2026-07-21 16:14:06 +08:00
houseme 7f569b67cb fix(tier): add mutation intent CAS advance (#5078)
Add idempotent terminal transition handling for tier mutation intents and a compare-and-swap record update helper backed by config-object ETags. Cover commit and abort retries, conflicting terminal updates, stale ETag rejection, and exact-limit scan pagination.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 08:10:31 +00:00
唐小鸭 d13345dc65 fix(site-replication): preserve HTTPS peer join endpoints (#5045)
fix(site-replication): enhance TLS handling for peer joins and add tests
2026-07-21 16:04:12 +08:00
cxymds 79d745413e fix(admin): support pool listing on single-drive setups (#5071) 2026-07-21 15:32:40 +08:00
houseme 9f25858b05 fix(tier): add mutation intent record store (#5075)
Add canonical record-object naming and crate-private save, load, and idempotent delete helpers for tier mutation intents. Cover malformed record keys, mismatched persisted mutation IDs, and an ECStore config-object round trip under test-util.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 07:26:23 +00:00
houseme a4e7dd70a6 fix(tier): add mutation intent foundation (#5074)
Add an internal durable tier mutation intent model for the #1357 distributed fencing work. The new model validates schema, checksum, canonical targets, mutation-specific target identity shape, config ETags, expiry, and prepared-to-terminal state transitions without changing the production mutation path yet.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 06:24:57 +00:00
houseme d6d22afc6e fix(ilm): recover cleanup pending transactions (#5073)
Retry cleanup-pending transition transaction recovery after restart, keeping committed remote bodies when local metadata already references them and preserving records when remote cleanup fails.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 14:17:32 +08:00
houseme 75381d4ffe fix(ilm): accept null version transition sources (#5069)
* fix(ilm): accept null version transition sources

Treat nil/null source version IDs as null-version transition sources when building transition transaction source identity, while preserving fail-closed validation for truly missing IDs on versioned sources.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ilm): preserve multipart source versions

Propagate bucket versioning into CompleteMultipartUpload, assign a concrete version ID for versioned multipart completions, and classify unversioned FileInfo sources as null-version transition sources.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ilm): normalize multipart completion versions

Treat nil staged version IDs as missing for versioned multipart completion and clear staged version IDs when completion publishes a null-version object under suspended or unversioned semantics.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 13:18:45 +08:00
houseme f32597bdb0 fix(ilm): recover uploaded transition transactions (#5068)
* fix(ilm): recover uploaded transition transactions

Persist transition transaction records through production transition uploads, use transaction-scoped remote object names, and start a conservative recovery loop for uploaded candidates.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ilm): reconcile committed transition records

Drop LocalCommitStarted transaction records only after object metadata confirms that the local transition commit already points at the same tier object and version.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 03:17:24 +00:00
houseme 1fac7a5871 chore(deps): refresh workspace dependencies (#5067)
* chore(deps): refresh workspace dependencies

Refresh compatible workspace dependencies while preserving the requested
version pins. Update async-nats and Hyper, and replace the temporary Hyper
Git patch with the released crate.

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(deps): bump hotpath to 0.21.5

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-21 02:05:42 +00:00
cxymds 0e2e01d060 fix(site-replication): harden add finalization (#5064) 2026-07-21 00:12:08 +08:00
cxymds 4f133eb95f feat(tiering): add Wasabi lifecycle target support (#5057) 2026-07-21 00:11:53 +08:00
houseme 302dd42d38 fix(ilm): add transition transaction foundation (#5065)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 14:10:11 +00:00
GatewayJ 48b2f3d6e3 fix(s3select): preserve CSV input as strings (#5030)
* fix(s3select): preserve CSV input as strings

* fix(s3select): address CSV schema review findings
2026-07-20 21:02:54 +08:00
Henry Guo 376b90f61f fix(lifecycle): back off idle free-version recovery (#5025)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-20 21:02:29 +08:00
abdullahnah92 b6838b262f fix(site-repl): inject local site into replicate add when omitted (fixes web console setup) (#5023)
fix(site-replication): inject local deployment into replicate-add payload when omitted

The web console's "Set Up Site Replication" flow posts only the remote peer(s) to
/rustfs/admin/v3/site-replication/add and omits the local deployment. The add
preflight requires the local deployment to be present
(validate_add_preflight_topology), so the console's request failed with
"site replication add request must include the local deployment" and no
replication was configured from the web UI.

Inject the local site into the sites list when the payload does not already
include it (matched by endpoint identity), before validation. `mc admin
replicate add` always sends every site, so this is a no-op for the CLI; the
local site carries no credentials (validate_add_sites already skips credential
checks for it).

Adds unit tests for inject-when-missing and no-op-when-already-present.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:56:18 +08:00
cxymds 28fdcc87be fix(tiering): make rejected upload cleanup durable (#5059)
* fix(tiering): make rejected upload cleanup durable

* fix(tiering): close transition upload cancellation gap

* test(tiering): cover failed upload without candidate

* test(tiering): synchronize cancelled cleanup recovery

* test(tiering): stabilize cancelled cleanup recovery

Prefer cancellation when the tier delete journal recovery worker is racing an immediate tick, and build the cancelled-cleanup regression store with an already-cancelled token so production recovery cannot consume the test journal.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 20:54:32 +08:00
houseme 35f3599992 fix(ecstore): fence restore final commit by operation id (#5062)
Refs rustfs/backlog#1356

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 12:08:55 +00:00
houseme b44e82fef1 fix(notify): restore webhook HTTPS target initialization (#5060)
Restore the workspace reqwest default feature stack for RustFS outbound HTTPS clients, while keeping per-crate extra APIs such as json, stream, and multipart explicit. Lazily initialize the notification runtime from admin target access when RUSTFS_NOTIFY_ENABLE=true is already effective, and add regression coverage for HTTPS webhook custom CA handling and target-list visibility.

Fixes #5052.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 19:55:07 +08:00
abdullahnah92 7ddaae397b fix(site-repl): count replicated buckets as in-sync (base64 decode in status path) (#5022)
fix(site-replication): count replicated buckets as in-sync in replicate status

`mc admin replicate status` reported "0/N Buckets in sync" with a cross mark for
every bucket even when replication was healthy and objects had propagated.

build_sr_info stores each bucket's replication_config in the wire form as
base64-encoded XML (raw_config_to_base64), but site_replication_config_mismatch
XML-parsed that string directly without base64-decoding. The parse always
failed, so every bucket with a replication config was reported as a config
mismatch (replication_cfg_mismatch = true), which the status endpoint and mc
render as out-of-sync.

Decode the wire value (tolerant base64 decode, falling back to raw bytes) before
XML-parsing. Adds a regression test that feeds the base64 wire form used in
production; the existing tests only exercised raw XML, which masked the bug.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-20 10:49:51 +00:00
houseme 9e4c5e949f fix(ecstore): fence restore cleanup by operation id (#5058)
Refs rustfs/backlog#1356

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 10:32:02 +00:00
abdullahnah92 7cc211ae17 fix(site-replication): report peer sync state in replicate info and s… (#5020)
fix(site-replication): report peer sync state in replicate info and surface pre-existing back-fill failures

BUG 1 — blank/Unknown Sync in `mc admin replicate info` and the console:
The info endpoint (SiteReplicationInfoHandler) serializes the persisted peer
map, whose sync_state is constructed as Unknown and never updated; the health
derivation added earlier only runs in build_status_info (the status endpoint).
Persist sync_state = Enable for peers on the add and join enable paths
(promote only Unknown, never clobber an explicit Disable). build_status_info
still refines live health for the status endpoint, so info/status/console and
peer_states all report a real sync state for a healthy peer.

BUG 2 — pre-existing-bucket back-fill failures were silently swallowed:
backfill_existing_buckets_after_add returned () and the add handler reported
success with an initial_sync_error_message that only ever carried
metadata-bootstrap errors, so a pre-existing bucket that failed to propagate
was invisible. Return per-bucket failures (including the previously silent
Ok(false) runtime-unavailable no-op and the versioning-failure drop), fold
them into the add response via compose_initial_sync_error_message, and log
reverse-direction gaps on the join path.

Adds unit tests for sync_state promotion (and Disable preservation) and for
back-fill failure surfacing.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:47:31 +08:00
houseme a27fe2f56c test(ecstore): cover transition upload cancellation (#5056)
Refs rustfs/backlog#1353

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 09:47:19 +00:00
cxymds bd978bed2d fix(admin): stabilize cluster capacity usage reporting (#5053)
* fix(admin): stabilize cluster capacity usage reporting

* test(admin): strengthen capacity usage regressions
2026-07-20 17:42:03 +08:00
cxymds fe67af3524 fix(heal): coordinate cluster-wide control operations (#5003) 2026-07-20 09:40:46 +00:00
houseme 26573622bc test(ecstore): cover post-apply rollback error (#5054)
Refs rustfs/backlog#1355

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 17:05:53 +08:00
cxymds eeafc355d4 feat(heal): define fenced control wire envelopes (#4997)
* fix(rpc): bind internode auth to exact targets

* fix(heal): initialize the runtime atomically

* fix(heal): aggregate status across cluster nodes

* fix(heal): return canonical tokens for duplicate starts

* feat(heal): add authenticated control RPC contract

* feat(heal): gate control capability by cluster topology

* feat(heal): define fenced control wire envelopes

* fix(heal): return canonical tokens for duplicate starts (#4992)

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-20 16:54:37 +08:00
houseme 955577b66f test(ecstore): cover transition source dedupe (#5050)
Refs rustfs/backlog#1355

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 06:31:48 +00:00
houseme 1cff6f20c9 test(ecstore): cover transition partial rollback (#5048)
Refs rustfs/backlog#1355

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 06:17:48 +00:00
houseme 2abfdd8261 test(ecstore): cover transition lock-lost cleanup (#5047)
Refs rustfs/backlog#1355

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 13:47:42 +08:00
cxymds 908ca548bb feat(heal): gate control capability by cluster topology (#4994)
* fix(rpc): bind internode auth to exact targets

* fix(heal): initialize the runtime atomically

* fix(heal): aggregate status across cluster nodes

* fix(heal): return canonical tokens for duplicate starts

* feat(heal): add authenticated control RPC contract

* feat(heal): gate control capability by cluster topology

* fix(heal): return canonical tokens for duplicate starts (#4992)

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-20 05:04:30 +00:00
houseme c92e99ba95 chore(deps): refresh workspace dependencies (#5044)
Update compatible workspace dependencies and refresh the lockfile while
keeping ratelimit pinned.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 12:32:13 +08:00
cxymds a774bc07da feat(heal): add authenticated control RPC contract (#4993)
* fix(rpc): bind internode auth to exact targets

* fix(heal): initialize the runtime atomically

* fix(heal): aggregate status across cluster nodes

* fix(heal): return canonical tokens for duplicate starts

* feat(heal): add authenticated control RPC contract

* fix(heal): return canonical tokens for duplicate starts (#4992)

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-20 12:07:38 +08:00
houseme 69f543568b test(ecstore): cover transition cleanup failure (#5042)
Refs rustfs/backlog#1355

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 04:03:24 +00:00
houseme 2269896f5e test(ecstore): cover transition lock cleanup (#5040)
Refs rustfs/backlog#1353

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 02:51:29 +00:00
houseme 67c4e3e60e fix(ecstore): read provider tier version headers (#5041)
Refs rustfs/backlog#1358

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-20 02:46:29 +00:00
Zhengchao An 4f0be83ea5 ci(cla): upgrade CLA to v2 (#5046)
Point the cla-bot config and the PR template at cla/v2.md so new
contributions are checked against the v2 Individual CLA.

- .github/cla.yml: document.version v1 -> v2, url -> cla/v2.md
- .github/pull_request_template.md: CLA link -> cla/v2.md
2026-07-20 10:44:06 +08:00
Zhengchao An db3b08b612 fix(e2e): use non-default credentials in cluster test constructor (#5039)
Commit aec2ee9ec (#5005) enforced that RPC secrets cannot be derived
from the public default credentials. This broke all multi-node cluster
E2E tests that relied on the constructor's DEFAULT_ACCESS_KEY /
DEFAULT_SECRET_KEY defaults.

Move the non-default credential and RUSTFS_RPC_SECRET blanking into
the RustFSTestClusterEnvironment constructor so every cluster test
starts with a valid RPC secret derivation base. Remove the per-test
overrides in cluster_multidrive_pool_test that were added as a partial
fix in #5005.
2026-07-20 02:37:37 +00:00
houseme 998c3f561c test(ecstore): cover transition prepared combo (#5037)
Add a deterministic transition matrix case for a stale prepared metadata snapshot followed by a committed transition, duplicate transition, and late GET. The test proves the stale metadata generation is physically reclaimed, duplicate transition does not upload a second remote candidate, and the late GET reads the committed remote body byte-for-byte instead of reopening the released local source.

Refs rustfs/backlog#1355

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-19 17:13:22 +00:00
houseme f42fc54362 test(ecstore): cover real transition bitrot failures (#5036)
Refs rustfs/backlog#1353

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-19 16:09:57 +00:00
GatewayJ 8ebedddfa1 fix(s3select): reject truncated object streams (#5027)
* fix(s3select): reject truncated object streams

* fix(s3select): validate raw stream before conversion
2026-07-19 15:34:20 +00:00
Zhengchao An a73f4c345f test(rpc): cover tonic auth service binding (#5034) 2026-07-19 23:27:08 +08:00
Zhengchao An ebc0aa0365 docs: update security advisory lessons (#5032) 2026-07-19 23:26:56 +08:00
cxymds aec2ee9ec1 fix(credentials): enforce RPC fallback credential policy (#5005) 2026-07-19 23:26:42 +08:00
cxymds 4290f390dd fix(heal): aggregate status across cluster nodes (#4990)
* fix(rpc): bind internode auth to exact targets

* fix(heal): initialize the runtime atomically

* fix(heal): aggregate status across cluster nodes

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-19 15:25:52 +00:00
houseme 056ebcee38 fix(targets): avoid Pulsar producer name collisions (#5033)
Generate a unique Pulsar producer name for each producer instance while preserving the target type and target id context. This avoids reload-time collisions when an old producer is still connected while a new destination is initialized.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-19 15:15:32 +00:00
houseme 18f0c161dd fix(ecstore): harden tier reader and restore cleanup races (#5035)
* fix(tier): hold generation lease through readers

Refs rustfs/backlog#1354

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(restore): fence failed cleanup by source identity

Refs rustfs/backlog#1356

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-19 23:05:58 +08:00
cxymds 1ac0841f6f fix(heal): initialize the runtime atomically (#4989)
* fix(rpc): bind internode auth to exact targets

* fix(heal): initialize the runtime atomically
2026-07-19 14:20:37 +00:00
cxymds 133499c2d5 fix(rpc): bind internode auth to exact targets (#4988) 2026-07-19 21:52:57 +08:00
cxymds b0c6c4cbce fix(storage): resolve erasure parity per pool (#4977)
* fix(filemeta): add state-aware file info validation

* fix(filemeta): validate shard arithmetic and delete paths

* fix(ecstore): add fallible erasure construction

* fix(ecstore): resolve storage parity per pool

* fix(storage): report heterogeneous erasure layouts

* fix(admin): publish prepared storage config atomically

* fix(storage): harden per-pool parity boundaries

* fix(storage): address pre-PR validation findings

* test(ci): fix strict-topology validation fixtures

* fix(heal): preserve delete markers during repair

* refactor(filemeta): drop unused ValidatedFileInfo witness

ValidatedFileInfo wrapped an unread `_file_info` reference alongside an `Option<ValidatedErasureLayout>`, but only the layout was ever consumed. Return the layout directly from `FileInfo::validate` so the sole production consumer (`LocalDisk::check_parts`) and the two unit tests read it without the extra witness type and lifetime.

No behavior change.

* fix(filemeta): keep compressed and MinIO-migrated tiered objects readable

The new decode-path validation rejected several legitimate on-disk shapes that older RustFS and MinIO-migrated data carry, turning readable objects into FileCorrupt:

- Compressed objects written with an unknown upload size persist a negative per-part actual_size (the documented "unknown size" sentinel that ObjectInfo::get_actual_size already tolerates). validate_collection_contents rejected it via usize::try_from; now a negative actual_size skips shard validation and only real, non-negative sizes are checked.
- MinIO-migrated objects transitioned to a versioned remote tier store the tier version id as a UUID string, not 16 raw bytes. MetaObject::into_fileinfo returned FileCorrupt (main tolerated it as None), making all versions of the object unreadable; MetaDeleteMarker free-version records took a Some(nil) sentinel path with the same effect, which also breaks free-version expiry (remote-tier leak). Both now decode through a shared transitioned_version_id_from_meta_sys helper: 16 raw bytes or a UUID string are accepted, anything else is tolerated as None instead of failing the read.

Regression tests updated to assert the readable/compat behavior, with new tests covering MinIO string-form recovery.

* fix(scanner): build the delete-marker test fixture without erasure geometry

get_size_counts_delete_markers_separately_from_versions built its delete marker with `FileInfo::new(object, 1, 1)`, which attaches erasure geometry (data=1/parity=1/distribution). This PR classifies versions by shape via `is_storage_delete_marker()` (no geometry) rather than the raw `deleted` flag, so a geometry-bearing "delete marker" is correctly serialized as a purge-pending payload Object and counted as a version — CI saw summary.versions=3, expected 2.

Real delete markers carry no erasure geometry (delete paths build them as `FileInfo { deleted: true, ..Default::default() }`), so construct the fixture the same way. It then classifies as a storage delete marker and the counts (versions=2, delete_markers=1) hold. This keeps the PR's more-correct classification, which prevents a purge-pending object's geometry from being dropped when serialized as a bare delete marker.

* docs(changelog): note per-pool parity fix and storage-class startup upgrade caveat

Records the #4801 per-pool erasure parity fix under Fixed, and documents the upgrade behavior where a persisted storage class that a small or heterogeneous pool cannot satisfy now fails startup — with the RUSTFS_STORAGE_CLASS_STANDARD recovery steps. Docs-only; covers R4 from the on-disk compatibility audit.

* fix(heal): report parity from erasure geometry, not is_valid()

heal_object set HealResultItem.parity_blocks via `if lfi.is_valid()`, which was missed by the migration of the other quorum/metadata predicates. With the new `is_valid()` semantics (full payload validation; delete markers now return false), a delete marker or a geometry-bearing version with a benign collection quirk would misreport parity as the pool default instead of its own. Use `has_valid_erasure_geometry()` — the narrow "does this carry erasure geometry" predicate the rest of the migration uses — so reporting matches the object's actual layout. Reporting-only; no data-path change.

* fix(filemeta): do not silently serialize a non-canonical deleted FileInfo as an Object

`From<FileInfo> for FileMetaVersion` classifies by `is_storage_delete_marker()` (shape), which correctly routes canonical delete markers to Delete and purge-pending payloads (deleted=true with real erasure geometry) to Object. But a `deleted` FileInfo that is neither a canonical marker nor a valid erasure payload would silently serialize as a zero-geometry MetaObject that later fails `validate_for_metadata_read`. Write paths validate first (`validate_for_erasure_write` / `validate_for_metadata_read`), so this is a caller bug; `From` is infallible, so surface it with a structured `warn!` on the malformed branch instead of writing corrupt metadata silently. Legitimate purge-pending objects (valid geometry) are unaffected — the guard only fires for `deleted && !has_valid_erasure_geometry()`.

* test(filemeta): assert real historical xl.meta versions pass metadata-read validation

Empirical companion to the code-reasoned decode-tolerance invariants (docs/architecture/erasure-coding.md §11) and the rolling-upgrade / MinIO-migration compatibility concern: the tightened `validate_for_metadata_read` runs on every local disk read and peer-RPC-decoded FileInfo, so it must accept every version of real historically-written xl.meta, never reject it as FileCorrupt.

Loads five real fixtures — MinIO small-inline, MinIO versioned (two object versions + a delete marker), MinIO large multipart, a legacy V1 (xl.json-derived) object, and a legacy meta_ver 2 object — decodes every version with parts materialized, and asserts validate_for_metadata_read() is Ok for each. Reverting the tolerant handling (delete-marker shape, legacy per-part checksums, string/short transitioned-versionID, negative actual_size) turns this red.

* fix(ci): remove duplicate storage test re-exports

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-19 21:52:31 +08:00
houseme 21049401fa fix(ilm): harden tier transition failure boundaries (#5031)
* fix(tier): fence generation-scoped operations

Refs rustfs/backlog#1354

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ilm): verify transition upload streams

Refs rustfs/backlog#1353

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): expand transition fault matrix

Refs rustfs/backlog#1355

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-19 10:48:32 +00:00
houseme 83d73b34f3 chore(deps): update flake.lock (#5026) 2026-07-19 14:28:20 +08:00
GatewayJ f9e8440a04 refactor(iam): introduce federated identity boundary (#5018) 2026-07-19 14:28:08 +08:00
Zhengchao An 7f5873dac8 fix(ecstore): resolve erasure parity per pool (#4801) (#5015)
* fix(ecstore): add fallible erasure construction

(cherry picked from commit bd148b20f7)

* fix(ecstore): resolve storage parity per pool

(cherry picked from commit c05c2cb24b)

* fix(ecstore): keep carved per-pool parity core self-contained on main

Fixups so the cherry-picked fallible-erasure + per-pool-parity core builds standalone on current main without the excluded scope-creep commits:
- runtime/sources.rs: re-add backend_storage_class_parities (removed by the per-pool commit; its rebalance caller was updated in an unrelated reporting commit that was left out). Reimplemented over the snapshot API, behavior-identical.
- config/mod.rs: rename the storage-class publish test module (main independently added a mod tests, so the cherry-pick collided).
- rustfs storage_api.rs + startup_storage.rs: route the storage-class ENV consts through the startup storage facade and use a local const for the erasure-set-drive-count env name, satisfying the layer/facade guardrail (main's guardrail is stricter than when the core was authored).

* fix(ecstore): use struct-init in erasure test helper to satisfy clippy field_reassign_with_default

The cherry-picked fallible-erasure commit's `erasure_with_invalid_dimensions` test helper built `Erasure` via `default()` then reassigned fields, which trips `clippy::field_reassign_with_default` under `-D warnings` (only surfaced by `--all-targets`, which lints test code). #4977 fixed this in a later commit that was not part of the carved core. Use struct-init with `..Default::default()`, matching #4977's final form.

* fix(rustfs): gate the test-only storage-class ENV facade re-export behind cfg(test)

The ENV constants (INLINE_BLOCK_ENV/OPTIMIZE_ENV/RRS_ENV/STANDARD_ENV) re-exported through the startup storage facade are only consumed by a #[cfg(test)] test in startup_storage.rs, so in a non-test lib build the re-export is unused and trips -D unused-imports under clippy --all-targets. Gate it with #[cfg(test)], matching #4977's final form.

---------

Co-authored-by: cxymds <cxymds@gmail.com>
2026-07-19 03:06:46 +00:00
Zhengchao An 3ed682be42 feat: offline log fault-analysis system (rustfs diagnose) (#4876)
* feat(log-analyzer): add crate skeleton and unified event model

Implements LA-1 (rustfs/backlog#1282) of the log fault-analysis system
(rustfs/backlog#1281): new synchronous rustfs-log-analyzer crate with the
LogEvent/LogLevel/SourceRef/EventKind/ParseStats model shared by all
later stages. No tokio, no rustfs-* internal deps by design.

Note: thiserror listed in the issue is deferred until a stage actually
defines error types (LA-3/LA-4) to avoid an unused dependency.

* feat(log-analyzer): add line parsing layer

Implements LA-2 (rustfs/backlog#1283): four parse channels tried in order
per line — native tracing JSON, container-prefix stripping (K8s CRI /
docker compose / journald) with JSON retry, multi-line Rust panic block
folding (both pre- and post-1.65 formats, stderr has no JSON logger), and
a plain-text fallback that never fails. Parse accounting feeds the report
parse-ratio disclosure.

* feat(log-analyzer): add ingest layer for directories and archives

Implements LA-3 (rustfs/backlog#1284): expands customer inputs (files,
directories, zip/tar/tar.gz/.zst/.gz, stdin-like readers) into parsed
events. Magic-byte detection with extension fallback, recursive archive
walking with depth/entry/byte/memory caps (every capped input disclosed
in IngestReport.skipped), first-level directory names become node labels,
and nothing is ever extracted to disk so hostile entry paths are inert.

Adds tar 0.4 to workspace deps (sync; the async astral-tokio-tar used by
rustfs-zip does not fit this crate's no-tokio contract).

* feat(log-analyzer): add rule model, matching engine, and finding aggregation

Implements LA-4 (rustfs/backlog#1285): owned serde-round-trippable Rule/
Matcher/Severity types (external JSON rules deserialize into the same
types later), fail-fast RuleSet validation that reports every problem at
once, a linear-scan engine with regexes compiled once, and an
order-independent FindingsCollector (commutative aggregates only; the
test asserts byte-identical output across shuffled input orders).

* feat(log-analyzer): add built-in seed rule library (68 rules, 12 categories)

Implements LA-5 (rustfs/backlog#1286): the 2026-07 repository-wide
failure-log survey distilled into rules across disk health, erasure/
bitrot, quorum, network/RPC, distributed locks, heal, scanner, IAM,
startup/config/TLS, capacity, decommission/rebalance, and process panics.

Every anchor was verified verbatim against the source tree (94/94 hits,
zero corrections needed). Quorum rules pre-fill implies_root_cause for
the Phase-2 folding (rustfs/backlog#1290); client-side rules carry burst
thresholds (min_count) so isolated client mistakes don't clutter reports.
Tests: one realistic positive sample per rule (table-driven), exact-set
smoke samples including the intentional internode/client signature
double-hit, and negative cases.

* feat(log-analyzer): add analysis orchestration, report rendering, and redaction

Implements LA-6 (rustfs/backlog#1287): a single-pass Analyzer that does
rule matching, minute-bucket timelines (gap-filled, merged to <=60
buckets), unmatched WARN/ERROR template clustering (placeholders for
numbers/uuids/paths/addresses/quotes, 5000-template cap disclosed as
<overflow>), mixed-UTC-offset detection, and below-min_count demotion to
a low-confidence section. Renderers: pipe-friendly terminal text, stable
JSON (schema_version=1), and ticket-pasteable Markdown. --redact hashes
customer identifiers (stable h:sha256[..8]) in samples/evidence/messages
while keeping rule ids, targets, and panic locations intact.

* feat(rustfs): add 'rustfs diagnose' subcommand for offline log fault analysis

Implements LA-7 (rustfs/backlog#1288): wires rustfs-log-analyzer into the
main binary as a diagnose subcommand that short-circuits before
observability/storage init (same pattern as 'info' / 'tls inspect') so
the report on stdout is never wrapped by the JSON logger.

rustfs diagnose <paths>... [--format text|json|md] [--since 24h]
  [--until ...] [--min-level warn] [--redact] [--top N] [--samples N]
Accepts files, directories, archives (.zip/.tar/.tar.gz/.zst/.gz) and '-'
for stdin. Exit codes: 0 = diagnosis completed (findings never fail the
process), 2 = bad arguments / no readable input.

diagnose_e2e covers the six MVP acceptance scenarios from
rustfs/backlog#1281 (directory+zst archive, multi-node zip attribution,
CRI-prefixed kubectl logs, panic folding, stable JSON schema, CLI parsing
incl. the legacy 'rustfs <volume>' preprocessor regression); the
full-binary smoke test is #[ignore]d (run with -- --ignored).
Usage doc: docs/operations/log-diagnose.md.

* ci(log-analyzer): guard rule anchors against log-message drift

Implements LA-8 (rustfs/backlog#1289): every seed-rule anchor must exist
verbatim in the rustfs source tree, so changing a log message without
updating its rule fails the gate instead of silently killing the rule.

- la-dump-anchors bin emits 'rule_id<TAB>anchor' TSV;
- scripts/check_log_analyzer_rules.sh greps each anchor (fixed-string,
  *.rs only, excluding crates/log-analyzer itself to avoid self-matches);
- RuleSet::new now rejects anchors that are blank, contain tab/newline,
  or are shorter than 8 bytes (no discriminating power); the '[FATAL]'
  anchor gained its trailing space to meet the floor while still matching
  the emit_fatal_stderr format string;
- wired as log-analyzer-rules-check into the pre-pr gate (it compiles the
  crate, so it stays out of the fast pre-commit set).

Negative self-test: breaking an anchor makes the script exit 1 naming the
rule ('MISSING anchor for rule inconsistent-drive: zzz-not-exist-anchor').

* refactor(log-analyzer): use root-relative provenance for directory inputs

Binary smoke run showed report samples citing full absolute paths, which
drowns the useful part. Directory inputs now label sources as
"<root-name>/<relative-path>" (e.g. "smoke-logs/node1/rustfs.log");
archives and single files keep their existing provenance.

* chore(log-analyzer): reword comment to satisfy the typos gate

* fix(log-analyzer): declare chrono serde feature locally after workspace feature localization

* fix(log-analyzer): bound line reads so a newline-less input cannot bypass the byte cap

read_until grew the line buffer with the entire remaining stream before the
max_total_bytes check ran, so a single multi-GB line (decompression bomb or
corrupt file) could allocate unboundedly. Replace it with a capped reader that
enforces the remaining global budget chunk-by-chunk and adds a per-line cap
(IngestOptions::max_line_bytes, default 1 MiB); over-cap tails are discarded
but still charged, and truncation is disclosed once per file as the new
line_too_long skip reason. Flagged by Codex review on #4876.

* fix(log-analyzer): redact field-shaped identifiers inside message text and widen the hash to 64 bits

--redact only hashed IPv4 literals in unstructured message text, so
bucket/object/access-key values embedded in messages (access_key=AK123,
'bucket: media, object: private/a.bin') leaked into reports documented as
safe to forward. Apply the SENSITIVE_FIELDS list to key=value / key: value
shapes in message text with the same hash as structured fields, and extend
the hash from 8 to 16 hex chars so cross-identifier collisions stay
negligible. Flagged by Codex and Copilot review on #4876.

* fix(log-analyzer): strip collector prefixes before panic-block absorption

An open panic block tested continuation lines against absorbs() before their
CRI/compose/journald prefix was stripped, so containerized panics stored the
prefix in the payload and split into a truncated panic plus text noise as soon
as the note/backtrace lines arrived. Stripping now happens once at the top of
feed() and every channel judges the payload. Flagged by Codex review on #4876.

* fix(log-analyzer): remove two input-order dependencies in representative selection

The unmatched-cluster target stayed pinned to the first-seen event while the
representative sample could be replaced, so sample and target could come from
different events and vary with input order; the pair now updates together by
lexicographic (sample, target) min. Sample selection tie-broke on line number
alone, which is only unique within one file; the key now includes the source
file. Flagged by Copilot review on #4876.

* fix(rustfs): reject negative relative times in diagnose --since/--until

parse_time_arg accepted "-24h" and produced a future timestamp, contradicting
the documented 'counted back from now' semantics. The amount now parses as
unsigned, so a leading '-' fails with the usual invalid-time error. Flagged by
Copilot review on #4876.

* feat(log-analyzer): Phase 2 — causal folding, timeline anomalies, external rules (LA-9) (#4942)

* feat(log-analyzer): collapse cascade symptoms under their root-cause finding

Phase-2 sub-item A (rustfs/backlog#1290): a finding whose rule declares
implies_root_cause edges folds under a qualifying root — root.first_seen <=
symptom.first_seen + 5min and root.last_seen >= symptom.first_seen - 30min,
existence-based when either side has no timestamps (pure stderr panics).
Findings gain collapsed_into/caused; text/markdown render the root block with
an indented cascade line and stop listing collapsed symptoms flat, while JSON
keeps every finding. Roots are promoted to the most severe position among
their block so the report top still answers 'the most likely cause'.

* feat(log-analyzer): detect timeline/clock anomalies (schema v2)

Phase-2 sub-item B (rustfs/backlog#1290): three deterministic hints rendered
between the summary and findings — mixed UTC offsets (with a clock-skew note
when signature-mismatch findings coexist), per-node time ranges that do not
overlap at all (both nodes >100 timestamped events), and log gaps of at least
max(15min, 3x bucket width) after >=3 consecutive active minutes, upgraded to
restart evidence when a startup-class finding begins within 5min after the
gap. JSON gains timeline_anomalies and schema_version bumps to 2.

* feat(diagnose): load external rules with --rules <file.json>

Phase-2 sub-item C (rustfs/backlog#1290): an external JSON rule file
({schema_version: 1, rules: [Rule...]}, the exact serde shape of the built-in
rules) merges over the seed library, with same-id rules replacing built-ins so
the support team can hotfix a misfiring rule without a release. The merged set
validates as a whole and any problem (bad regex, duplicate id, empty matcher
group, wrong schema version) prints every error and exits 2 — analysis never
runs on a half-broken set. External anchors are exempt from the CI anchor
guard, documented as author-owned quality. Adds the custom-rules section to
docs/operations/log-diagnose.md.

* fix(log-analyzer): address PR #4876 review (redaction coverage, order-independence, guards)

Redaction (--redact) now honours its "forwardable" intent across every report surface instead of a 15-name field whitelist applied over a subset:
- redact_event scrubs the full fields map (sensitive names hashed whole, every other value run through redact_text) plus provenance, so the JSON/Markdown full-sample dump no longer leaks non-whitelisted fields (client_ip, url, user, ...).
- node labels are hashed once at ingestion, so summary.nodes, per-node timeline ranges, samples and timeline anomalies stay consistent and correlatable under one stable hash.
- evidence values, unmatched-cluster templates and skipped-input paths are now redacted; peer/disk/drive/volume/node/user added to the sensitive set; IPv6 literals are hashed (without touching `rust::paths` or HH:MM:SS clocks); provenance keeps the leaf filename and hashes the customer directory/archive prefix.
- redact.rs and docs/operations/log-diagnose.md reworded to "best-effort identifier scrubbing", not an anonymization guarantee.

Order-independence (the crate's headline contract):
- the evidence value cap keeps the lexicographically smallest N distinct values instead of the first-N-by-arrival (previously order-dependent).
- first_seen/last_seen break equal-instant ties on the offset, so the serialized RFC3339 offset no longer depends on input order.

Parsing:
- a new-format panic header no longer swallows the line immediately after it when that line is itself a JSON event or a second panic header (previously dropped an interleaved ERROR in merged stdout/stderr, or merged a panic-during-panic); trailing "note: ..." backtrace lines now fold into the block.

CLI:
- diagnose --since/--until reject absurd relative amounts via checked_sub_signed instead of panicking; the exit-code doc now matches actual behaviour.

CI:
- check_log_analyzer_rules.sh is wired into the ci.yml test-and-lint job (it was only in make pre-pr, so anchor drift from other PRs could merge green).

Markdown table cells escape '|' so customer log text cannot break the table structure.

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-18 15:00:34 +00:00
houseme 15f4e75870 fix(cache): harden object data cache coordination (#5004)
* fix(cache): enforce projected entry capacity

Refs: rustfs/backlog#1335

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): fence identity budget eviction by generation

Refs rustfs/backlog#1334.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): fence clear against concurrent fills

Refs rustfs/backlog#1333

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): linearize memory reservation claims

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): retain allocation memory claims

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): publish memory snapshots by epoch

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): coordinate cold object fills

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): fence metadata cache transition races

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-18 14:37:10 +00:00
Zhengchao An 4faea7fcbc ci: bump repo-visuals-action to v1.3.0 (#5014)
Backward-compatible feature release; existing chart-style/animate/contributors inputs are unchanged and continue to work.
2026-07-18 22:30:28 +08:00
Zhengchao An 9b197fc1c2 docs(architecture): correct erasure-coding spec statements that contradict main (#5012)
The normative erasure-coding spec stated several target behaviors and known baseline defects as active invariants, and cited two symbols that do not exist on main. Each correction below was verified against the code.

- §4.1 shard size: legacy even-padding is RustFS-legacy-only and is NOT MinIO's sizing. MinIO (and the modern GF(2⁸) path) use plain div_ceil; for 1 MiB / 6 data shards that is 174763 bytes versus the legacy even-padded 174764. MinIO-migrated data is decoded by the modern path, so the legacy formula never applies to it.
- §6.3 / §11 MTime: the MTime key is always written (a None mod_time encodes as UNIX_EPOCH = 0), not omitted. Round-trip safety to None is enforced on the decode side, not by write omission. Only the legacy StatInfo.ModTime field is omitted-when-None.
- §7 commit: rollback after a missed write quorum is best-effort — undo failures are counted and warn!-logged, never propagated or retried — so a partially-failed rollback can leave shards behind; softened "never left partially committed" accordingly.
- §7 data_dir: reduce_common_data_dir votes over each disk's old_data_dir (a GC input for reclaiming the superseded dir), not the newly committed data_dir.
- §7 convergence: classify_rename_convergence is consumed only by the multipart-complete path (convergence.needs_heal()); the regular put_object path discards it.
- §7 write layout: removed the WriteLayout / resolve_write_layout citation — neither symbol exists on main (the write layout is computed inline), which violated the spec's own cite-real-symbols rule.
- §6.6 inline: inline presence is read from the meta_sys[inline-data] body marker alone; the header InlineData flag is written but not consulted on read, and disagreement is tolerated (MinIO may write the flag unset with inline data present). Removed the false "must agree" invariant.
- §11 UUID tolerance: version_id / data_dir (ID/DDir) decode as a fixed 16-byte Uuid::from_bytes and fail closed on a present-but-non-16-byte value; only transitioned-versionID uses the tolerant Uuid::from_slice(...).ok().filter(!nil). Split the previously-conflated bullet.
- §11 decode blanket: narrowed "everything else must degrade to a tolerant default" to enumerate the legitimate structural/geometry/version/length fail-closed guards (header array length, unknown header_ver, version > max, versions_len / bin_len bounds, non-16-byte ID/DDir).
- §8 / §13 codec guard: has_valid_dimensions() is a &self method, so it runs after construction and reliably covers only block_size == 0; a data_blocks == 0 geometry with parity > 0 panics in the .expect constructor before the guard runs. Noted the fallible-constructor fix.

Refuted and intentionally left unchanged: the "accepts container major == 1 with any minor" statement is correct — the reader compares only major (rejects major > 1) and never gates on minor, so minor 4 is accepted.
2026-07-18 21:05:22 +08:00
Zhengchao An 53728a03d3 chore: self-host contributor wall (#5006) 2026-07-18 09:48:36 +00:00
Zhengchao An 04bfd48eb1 fix(ecstore): invalidate metadata cache after ILM transition persists (#4951)
A duplicate transition task admitted after the winner released its
in-flight claim (#4839) re-reads the version before uploading, but on
unversioned buckets that read could hit a stale pre-transition entry in
the 2s-TTL GET metadata cache: transition_object never invalidated the
cache after delete_object_version persisted transition_status=complete
and freed the local data. The stale hit defeated the TRANSITION_COMPLETE
early-return, so the duplicate streamed the already-deleted local data
to the remote tier (NotFound reader errors + rejected duplicate tier
PUT with UnexpectedContent).

Invalidate the cache right after the transitioned metadata is persisted,
matching the other metadata-mutating paths, and add a regression test
that runs a duplicate transition against an already-transitioned version
and asserts no second tier upload and unchanged remote object metadata.

Fixes #4827
2026-07-18 15:03:27 +08:00
Zhengchao An 2c113542f8 docs(architecture): normative erasure-coding algorithm and on-disk compatibility contract (#4999)
* docs(architecture): add normative erasure-coding algorithm and on-disk compatibility contract

Adds docs/architecture/erasure-coding.md as the source of truth for how RustFS erasure-codes, stores, reads, reconstructs, and heals user data, and the on-disk (xl.meta) / decode compatibility contract every future change must preserve. Grounded in the baseline (main) implementation with file:line anchors; cross-links (does not duplicate) the existing placement, MinIO-format-compat, layout-boundary, decommission, and tier-ILM docs, and the AGENTS.md cross-cutting invariants.

Covers: Reed-Solomon over GF(2^8) modern vs GF(2^16) legacy backends and how each is selected; pool/set/drive geometry with the 2..=16 set-size and per-pool parity invariants; the key-derived distribution permutation (1..=N); 1 MiB block size and the modern/legacy shard-size formulas; HighwayHash256S interleaved bitrot layout; the full xl.meta container/header/version-body schema and internal dual-key convention; write/read/heal quorum rules; and, newly codified as a first-class contract, the decode-tolerance invariants (nil-UUID/epoch-mod_time to None, skip unknown fields, hard-guard only length-critical arrays, tolerate the negative actual_size compressed sentinel, tolerate a malformed transitioned-versionID). Linked from the architecture README under "Contracts & invariants". Docs-only; check_doc_paths.sh passes.

* docs(architecture): anchor erasure spec by symbol name, not line numbers

Line numbers rot as code changes and are not validated by check_doc_paths.sh, so they would silently mislead the very code changes this normative spec is meant to guide. Replace all file:line citations with file-path + symbol-name references (functions/consts/types are greppable and rename only on deliberate changes; format byte offsets are kept). Add an explicit "references work here" note and a §13 rule that governed changes must update this spec in the same PR.

* docs(architecture): correct erasure spec after multi-expert adversarial review

Four independent adversarial reviewers fact-checked every claim against the code. Fixes:

- CRITICAL (found independently by two reviewers): §1 mislabeled the GF(2^16) reed-solomon-simd "legacy" backend as the reader for "older MinIO-lineage format". It is the opposite — that backend serves RustFS's own older main-branch (rmp_serde, uses_legacy_checksum) objects; MinIO-migrated data uses the same rs-vandermonde GF(2^8) scheme and is decoded by the modern backend. The old wording contradicted §1's own MinIO-interop line, §12, minio-file-format-compat.md, and the source comments, and would have misled the highest-stakes decode-routing decision.
- §2.1/§12: set size 2..=16 holds for multi-drive layouts; single-drive deployments run at N=1 (parity 0) outside is_valid_set_size.
- §2.2: validate_parity_inner enforces parity <= N/2 only for N > 2 (user storage-class parity flows through it); the standalone validate_parity is unconditional but only applied to the resolved default.
- §6.2/§12: header format is dispatched by header_ver; array length (4/5/7) is a per-version validation, not the discriminator.
- §6.3: the part-array length guard applies on the all_parts decode path.
- §6.5: get_bytes matches only the two canonical lowercase keys; only is_internal_key/get_str are case-insensitive.
- §6.5: transitioned-versionID — state the load-bearing invariant (non-16-byte decodes to None, never fatal); string-form recovery is optional, and transitioned-xl.meta interop is out of scope.
- §11: typo RustSF -> RustFS.

All other claims across §1-§14 were verified accurate against code (distribution formula, quorum formulas, on-disk key set/endianness/markers, decode-tolerance invariants, version anchors, standard references).
2026-07-18 14:44:04 +08:00
Zhengchao An 825bf0e2d8 ci: self-host star history chart (#5001)
* ci: self-host star history chart

* ci: use animated gradient star chart
2026-07-18 14:33:45 +08:00
Zhengchao An 0346108ae4 fix(s3): populate CopyObject response checksums and persist them (#4998)
fix(s3): populate CopyObject response checksums and persist them (#4996)

A CopyObject that requested ChecksumAlgorithm=SHA256 applied the copy but returned no CopyObjectResult.ChecksumSHA256, and a later checksum-mode HEAD/GET on the destination returned nothing: execute_copy_object never read the requested algorithm, never gave the destination write a checksum, and built CopyObjectResult with ..Default::default() (all checksum fields None).

Give the destination object a checksum on the copy path. When the caller requests an algorithm, compute it fresh over the copied plaintext (the hasher sits on the innermost reader so it digests plaintext, before compression/encryption wrap it) so it persists into the object's checksum and a checksum-mode HEAD/GET returns it. When no algorithm is requested, carry the source object's stored checksum over unchanged — the copy does not transform the plaintext, so re-hashing would be wasted work and would flatten a multipart composite value. Fill CopyObjectResult from the persisted destination checksum decoded exactly the way GetObject/HeadObject do, so the response value is identical to a later checksum-mode HEAD/GET.

Add e2e regression tests: requested SHA256 is computed, returned, and HEAD-consistent; a no-algorithm copy preserves and reports the source SHA256; and a requested CRC32 over a SHA256 source is computed fresh (equals a reference CRC32 PUT), overrides the source algorithm, and is not inherited.

Fixes #4996
2026-07-18 13:32:51 +08:00
houseme 79509aad2d chore(deps): refresh workspace dependencies (#5002)
Update direct dependency constraints and refresh the lockfile while keeping
ratelimit pinned.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-18 05:22:58 +00:00
Zhengchao An d7d880b37d test(1306): pin usage serialization, snapshot cache invalidation, and listing send classification (#5000)
test(1306): pin Some(0) usage serialization, snapshot cache invalidation, and listing send classification

Follow-up test hardening for the merged admin-usage-snapshot work
(#4979/#4980/#4981/#4982, rustfs/backlog#1306). Tests only; no production
behavior change.

- B-1 madmin: pin that a scanned-but-empty bucket (Some(0)) serializes usage
  stats as zeros, staying distinct from the no-snapshot (None) omitted case.
- B-2 gating: revert detector proving save_data_usage_in_backend invalidates
  the 30s snapshot cache so a fresh save is visible to the next cached read.
- A-1 list_objects: pin that a successful gather_results send is never
  misclassified as ConsumerGone (correct state + err sentinel delivered), and
  document the wrapper Err arm invariant. A full wrapper-level producer-error
  integration test is deferred as it needs the fake-disk list harness.
2026-07-18 04:30:53 +00:00
Zhengchao An cf9e9c6fd5 fix(ilm): implement expire_restored delete semantics for restore expiry (#4950)
DeleteRestoredAction is supposed to demote a restored object back to its
pure transitioned state: remove only the local restored copy, strip the
x-amz-restore headers, and leave the version (and the remote tier data)
untouched. expire_transitioned_object set
opts.transition.expire_restored accordingly, but no delete path ever
read the flag, so delete_object ran an ordinary delete: on unversioned
buckets the whole object vanished and the free-version record scheduled
remote tier cleanup (tier data loss); on versioned buckets the latest
version got a spurious delete marker that replication propagated.

Route expire_restored explicitly in SetDisks::delete_object before
delete-marker resolution and replication dispatch: target the found
version with FileInfo.expire_restored=true and return early. The
FileMeta::delete_version layer already implements the semantics (strip
restore headers, keep the version, hand back the local data dir); this
wires it up.

Also fix the action matching in expire_transitioned_object (extracted
into transitioned_object_delete_opts): DeleteRestoredVersionAction
previously fell through to the full transitioned-object delete, which
removed the remote tier data of a noncurrent restored version. It now
routes through the same restored-copy cleanup with the exact version id,
matching MinIO's Action.DeleteVersioned()/DeleteRestored() dispatch.

Re-enable test_restore_chain_local_read_expiry_keeps_remote_and_allows_
re_restore in the ILM Integration (serial) lane; add unit tests pinning
the event->options routing and the filemeta expire_restored branch.

Closes rustfs/backlog#1302
2026-07-18 02:55:29 +00:00
Henry Guo 361334ab08 chore(deps): sync merged s3s SigV4 fix (#4987)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-18 10:50:13 +08:00
Henry Guo 889a45ad4d fix(scanner): back off clean idle scans across erasure clusters (#4984)
* fix(scanner): back off clean single-disk cycles

* fix(scanner): extend idle backoff across erasure clusters

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-18 10:49:45 +08:00
darion-yaphet 5ec124bf23 docs(architecture): prevent workspace overview from drifting (#4983)
Replace stale dependency-depth and line-count snapshots with a domain overview based on the current Cargo workspace. This preserves the high-level architecture while avoiding references to removed crates and fragile size estimates.

Constraint: Workspace membership changes independently of architecture documentation updates
Rejected: Refresh the old numeric snapshots | they would immediately become stale again
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Cargo.toml as the source of truth for workspace membership
Tested: git diff --check; scripts/check_doc_paths.sh; cargo metadata --no-deps --format-version 1
Not-tested: make pre-commit (documentation-only change)
2026-07-18 10:49:05 +08:00
Henry Guo 906805568b feat(table-catalog): add durable backing state transfer (#4952)
* feat(table-catalog): add durable backing state transfer

* fix(table-catalog): reject orphaned migration entries

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-18 10:45:32 +08:00
Zhengchao An 230e5fc31a fix(auth): POST-object lock fields and signed metadata=true listing 403s (#4959)
* fix(sse): surface unconfigured managed SSE as 400, fix POST SSE-S3 e2e

Managed SSE (SSE-S3 / bucket-default) on a server without KMS and without RUSTFS_SSE_S3_MASTER_KEY fails closed by design since #3564, but surfaced as 500 InternalError. Map the misconfiguration to InvalidRequest (400) and seed the local SSE master key in the anonymous POST-object SSE e2e tests; align the missing-from-policy test with the MinIO-compatible SSE field exemption (s3s#608). Re-admit the tests to the e2e-full profile (rustfs#4844).

* fix(auth): POST-object lock fields and signed metadata=true listing 403s

Two authorization surfaces returned 403 for allowed requests (rustfs#4845): the PutObject access hook demanded PutObjectRetention/PutObjectLegalHold IAM actions for POST-object form uploads whose lock fields are governed by the validated POST policy, and the metadata=true listing route never resolved SigV4-verified credentials into ReqInfo so signed requests were evaluated as anonymous. Skip the PUT-header lock actions for POST form uploads and resolve credentials in the metadata route; re-admit the four e2e tests to the e2e-full profile.
2026-07-18 10:44:53 +08:00
Zhengchao An 40c089f31b feat(server): add opt-in global connection cap to the S3 accept loop (#4957)
Follow-up to backlog#1191 (optional sub-item). The main accept loop
spawned one task per socket with no global bound, so a connection flood
could exhaust file descriptors and memory and take existing traffic
down with it.

- New RUSTFS_API_MAX_CONNECTIONS (default 0 = unlimited, no semaphore
  constructed, accept loop unchanged).
- When set, the loop acquires an owned semaphore permit BEFORE
  accept(): at saturation it simply stops accepting and lets the
  kernel backlog absorb bursts (TCP-native backpressure) instead of
  accept-then-close churn. The permit moves into the connection task
  and is released by RAII on any exit path, including TLS handshake
  failures.
- Saturation is observable via the
  rustfs_http_server_connection_cap_saturated_total counter, a
  connection_cap_state startup event, and a per-wait debug event;
  shutdown stays responsive while parked on the semaphore.
- The cap covers everything on the main listener (S3, admin, console,
  internode gRPC); the constant docs carry sizing guidance.
- E2E coverage: ten sequential Connection-close requests against cap 2
  prove permits never leak; two stalled connections saturating cap 2
  leave a third unserved until their permits are released, after which
  the queued request is accepted and answered.
2026-07-18 10:41:51 +08:00
Zhengchao An 569fa3ec87 fix(ecstore): tolerate illumos/Solaris EEXIST for non-empty directory removal (#4995)
POSIX lets rmdir report a non-empty directory as either ENOTEMPTY or EEXIST. Linux/macOS/Windows use ENOTEMPTY (ErrorKind::DirectoryNotEmpty); illumos/Solaris return EEXIST (errno 17), which Rust surfaces as ErrorKind::AlreadyExists and which a DirectoryNotEmpty match never catches.

LocalDisk::delete_file removes xl.meta and then recurses to rmdir the object directory, tolerating only NotFound and DirectoryNotEmpty. Since #4300 (transactional delete rollback-staging, new in beta9) the object directory still holds the rollback backup dir when that rmdir runs — the caller removes it only after write quorum is confirmed — so the rmdir legitimately reports "not empty". On Linux that is tolerated; on Solaris it is EEXIST, which fell through to the catch-all arm and became FileAccessDeniedWithContext. That failed the delete commit, rolled the metadata back, and left the object undeletable, so the client retried indefinitely with a spurious FileAccessDenied and no EACCES anywhere (rustfs/rustfs#4978). The same Linux-errno assumption also broke non-force DeleteBucket on a populated bucket on Solaris.

Add a portable is_dir_not_empty_error classifier (DirectoryNotEmpty kind plus raw ENOTEMPTY/EEXIST), mirroring MinIO's isSysErrNotEmpty, and use it at the two directory-removal sites via is_benign_object_rmdir_error (delete_file) and classify_delete_volume_error (delete_volume). The classifier is applied only at rmdir/remove_dir_all sites, where EEXIST unambiguously means "not empty", so EEXIST keeps its normal meaning everywhere else. rmdir never returns EEXIST on Linux/macOS/Windows, so the new raw-errno branch is unreachable there and the change is a strict no-op off illumos/Solaris.

Adds unit tests for the classifier (DirectoryNotEmpty/ENOTEMPTY/EEXIST match, EACCES/ENOENT reject, real non-empty rmdir against the host errno) and call-site decision tests that make a Solaris EEXIST regression detectable on Linux CI.

Fixes #4978
2026-07-18 02:37:43 +00:00
Zhengchao An edfb3c134f fix(s3): return x-amz-copy-source-version-id for versioned CopyObject source (#4985)
For a CopyObject whose source carries versioning, the response must echo the exact source version copied via x-amz-copy-source-version-id (SDK CopySourceVersionId), kept distinct from the newly created destination x-amz-version-id. RustFS set the destination version_id but left copy_source_version_id unset, so AWS SDK clients could not prove which source version was copied — the same field UploadPartCopy already populates.

Populate CopyObjectOutput.copy_source_version_id from the version actually read from the source, gated on the source bucket carrying versioning (enabled or suspended) and rendering the null version as "null", mirroring the GET/HEAD convention. Add an e2e regression test that copies a non-latest source version and asserts the source-version header equals the requested version, the destination header is a distinct new version, the destination bytes/size match the copied version, and the source version remains present.

Fixes #4976
2026-07-18 07:33:35 +08:00
Zhengchao An 314c17205e fix(replication): recover targets after outage (#4986) 2026-07-18 07:03:23 +08:00
Zhengchao An 814682d6bb fix(ecstore): accept illumos non-empty rmdir errors (#4991) 2026-07-18 06:01:25 +08:00
Zhengchao An 87128682b0 fix(ecstore): recover data usage snapshots safely (#4979) 2026-07-17 19:02:16 +00:00
Zhengchao An 4589148f48 fix(admin): serve data usage endpoints from scanner snapshot instead of live listing (#4980) 2026-07-18 00:09:02 +08:00
Zhengchao An accd312465 fix(ecstore): classify listing consumer disconnect as non-error completion (#4981) 2026-07-17 16:05:21 +00:00
Zhengchao An 627c396649 fix(scanner): allow usage snapshot save when existing timestamp is future-dated beyond clock tolerance (#4982) 2026-07-17 16:04:43 +00:00
Zhengchao An c818177b54 test(ilm): re-enable test_transition_and_restore_flows; fix test-util disk-open and restore error-path lock (#4945)
test(ilm): re-enable test_transition_and_restore_flows; fix test-util disk-open and restore error-path lock (rustfs/backlog#1303)

The excluded test's 'missing xl.meta ... on disk2' was NOT an EC
metadata-distribution issue: after a transition all four shard disks hold
a fully consistent xl.meta (verified by decoding each shard). The panic
came from the tier test util's open_disk, which hardcoded disk_index 0
for every disk path; LocalDisk::new validates the endpoint's
(set_idx, disk_idx) against the disk's own format.json and rejected every
non-slot-0 disk with InconsistentDisk, which read_transition_meta
collapsed into 'missing xl.meta'. Derive the real indices from
format.json instead. This also un-breaks free_version_count /
wait_for_free_version_absence for non-first disks (silently 0 before).

With that fixed, the test advanced to the #4877 restore self-deadlock,
whose main paths #4886 already fixed. Complete that fix on the one path
it missed: update_restore_metadata (the restore-failure metadata
rewrite) still rebuilt copy_object options with no_lock=false and would
re-acquire the object write lock the restore handler already holds.
Propagate the caller's no_lock there too.

Remove the test from the serial-lane exclusion list; the four remaining
exclusions are unrelated known issues and stay.
2026-07-17 11:33:28 +00:00
cxymds ec47c20ced fix(lifecycle): expire sole delete markers by days (#4974) 2026-07-17 19:03:33 +08:00
cxymds 1e14c05cf0 fix(tiering): support UTF-8 metadata signing (#4969)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-17 19:03:13 +08:00
Zhengchao An 7b2cc1f427 fix(sse): surface unconfigured managed SSE as 400 and fix anonymous POST SSE-S3 e2e (#4958)
fix(sse): surface unconfigured managed SSE as 400, fix POST SSE-S3 e2e

Managed SSE (SSE-S3 / bucket-default) on a server without KMS and without RUSTFS_SSE_S3_MASTER_KEY fails closed by design since #3564, but surfaced as 500 InternalError. Map the misconfiguration to InvalidRequest (400) and seed the local SSE master key in the anonymous POST-object SSE e2e tests; align the missing-from-policy test with the MinIO-compatible SSE field exemption (s3s#608). Re-admit the tests to the e2e-full profile (rustfs#4844).
2026-07-17 19:02:21 +08:00
Zhengchao An 97edb2e5cf feat(api): add opt-in per-bucket dimension to the S3 API rate limiter (#4949)
Follow-up to #4895 (backlog#1191 deferred sub-item). The client-IP
dimension gives per-client fairness; this adds a collective per-bucket
budget so one hot bucket cannot monopolize the server regardless of how
many client IPs the traffic is spread across.

- Generalize RateLimiter over its key type with a Borrow-based check()
  so &str lookups against String bucket keys stay allocation-free on
  the hit path; client-IP call sites are unchanged in behavior.
- RateLimitLayer now carries optional client and bucket limiters; a
  request must pass every configured dimension, and rejections report
  which one tripped via a new 'dimension' metric label.
- Bucket extraction mirrors s3s host routing: virtual-hosted-style
  resolves the Host/authority prefix against the same expanded domain
  set (with port variants) the s3s router uses; otherwise the first
  path segment. Admin and table-catalog namespaces are never buckets.
- New env vars RUSTFS_API_RATE_LIMIT_BUCKET_RPM/_BURST (default 0 =
  dimension off) under the existing enable switch; bucket-only
  configurations (client RPM 0) are supported.
- Bucket names are attacker-chosen, so the bounded-shards design
  (100k keys, lossless idle sweeps, most-idle eviction) is the memory
  defense; a test floods 10k random names and asserts the cap holds.
- Unit tests for extraction, shared bucket budgets across IPs, both-
  dimensions interaction, and the extended env matrix; e2e test proves
  bucket-only throttling on the real server with an unrelated bucket
  unaffected.
2026-07-17 19:01:18 +08:00
Zhengchao An e1fc4b12ea fix(api): descriptive InvalidArgument reason for Windows-unsupported object keys (#4947)
fix(api): return descriptive InvalidArgument reason for Windows-unsupported object keys

On Windows hosts object keys containing NTFS-reserved characters or
Win32-unaddressable path segments are rejected up front, but the client
only saw a bare "Invalid argument" (issue #3299). Attach an explicit
reason to these rejections and surface non-empty InvalidArgument reasons
through the S3 error message.
2026-07-17 19:00:52 +08:00
Zhengchao An e279a4f48a fix(extract): treat tar mtime=0 as unset to fix unreadable entries (#4948)
An extracted entry whose tar header mtime is 0 was stored with
mod_time = UNIX_EPOCH, which xl.meta encodes as 0 nanos (= no
mod_time). On read-back the version failed valid() and parsing fell
into the legacy rmp_serde fallback, so every read of the object
returned 500 (invalid type: integer 0, expected an OffsetDateTime).

Treat mtime 0 as unset (tar convention) and fall back to the upload
time. Also fix two test-side issues uncovered behind the 500: the pax
fixture must use a ustar header for its XHeader entry, and the SSE-S3
extract tests must provision RUSTFS_SSE_S3_MASTER_KEY. Re-admit the 19
quarantined tests to the e2e-full merge gate.

Fixes #4842
2026-07-17 19:00:31 +08:00
Zhengchao An 8ace340694 docs(agents): add anti-bloat guidelines and simplicity adversary role (#4975)
Add Reuse Before You Write and Necessary Code Only sections to AGENTS.md,
promote quality probes into a dedicated seventh adversarial-validation
role (simplicity adversary), extend rust-code-quality checks, and dedupe
code-change-verification's restated Rust checklist into a pointer.
2026-07-17 18:59:59 +08:00
Zhengchao An 701c3eee5b fix(ilm): replace whole-copy-back restore lock with accept-path CAS (#4956)
fix(ilm): serialize RestoreObject accepts with a CAS guard, not the whole copy-back

Implements the backlog#1304 decision: replace #4877's object write lock
held across the entire tier copy-back with an atomic compare-and-set on
the restore ongoing flag.

- ecstore: add ECStore::acquire_restore_accept_guard + RestoreAcceptGuard
  (opaque, purpose-scoped object write lock with an is_lock_lost fence);
  handle_restore_transitioned_object no longer locks the copy-back, so
  HEAD/get_object_info stay non-blocking during a restore and the inner
  put_object/complete_multipart_upload commit locks no longer self-deadlock.
- API: execute_restore_object holds the guard across the restore-status
  read-check-write (no_lock inside the scope), drops it before spawning
  the copy-back; SELECT-type restores keep the plain read-locked path;
  the copy-back pins the resolved version so a concurrent PUT cannot
  strand the flagged version at ongoing=true; concurrent restores are
  rejected with 409 RestoreAlreadyInProgress (was a retryable 500) and
  guard contention maps to 503 SlowDown.
- tests: drop the #4877 entry-blocking unit test (semantics deliberately
  reversed), pin accept-guard mutual exclusion, update the ilm-8 restore
  integration test to the final semantics, and add a concurrent
  double-POST test asserting exactly one acceptance and one tier GET.
2026-07-17 17:56:45 +08:00
Zhengchao An a9e3613cfd fix(quota): only require decoded length for actually framed aws-chunked bodies (#4968)
PUT admission since #4928 rejected any request whose Content-Encoding declares
aws-chunked but lacks x-amz-decoded-content-length with 400 UnexpectedContent.
Whether the body is actually chunk-framed is signalled by a STREAMING-*
x-amz-content-sha256, not by the declared encoding: the s3s auth layer only
de-frames streaming payloads and already requires the decoded length for them,
so a declared-only aws-chunked request (issue #1857 clients) carries an
unframed body whose wire Content-Length is the authoritative object size.
Admit it against that length; keep failing closed for genuinely framed bodies
without a decoded length, and use the decoded length for streaming payloads
even when Content-Encoding is absent.

Refs: https://github.com/rustfs/backlog/issues/1336
2026-07-17 16:23:00 +08:00
Zhengchao An f67e8a6cdc chore(release): prepare 1.0.0-beta.10 (#4946) 2026-07-17 10:57:58 +08:00
Zhengchao An fedc37c834 docs(operations): add drive timeout tuning guide for slow storage (#4944)
Issue #4810's production incident showed the drive timeout knobs are
undiscoverable from symptoms: a listing that outruns the walk budget
gives operators no signal pointing at RUSTFS_DRIVE_* tuning. The knobs
existed only as code constants in crates/config/src/constants/drive.rs
with no operator-facing documentation.

Add docs/operations/drive-timeout-tuning.md covering the per-operation
drive timeout knobs, resolution precedence, the high_latency profile,
and a dedicated section on listing truncation and the walk stall
budget - including the correction that since the stall-budget rework
the foreground listing path is governed by
RUSTFS_DRIVE_WALKDIR_STALL_TIMEOUT_SECS, not the total-timeout knob
the issue suggested. Link the guide from the README.

Ref #4810
2026-07-17 10:34:16 +08:00
Zhengchao An e55fdd275a docs(skills): single-commit release flow with preview and final tags on the same commit (#4943)
docs(skills): single-commit release flow — preview and final tags share the validated commit

Version files are bumped once, directly to the final target version; the -preview.N suffix now exists only in tag names. The binary self-reports build::TAG and build.yml derives artifact naming and prerelease classification from the tag, so the preview tag and the final tag can point at the exact same commit — eliminating the post-validation version-bump commit that previously separated the validated hash from the released tag.

rustfs-release-version-bump gains a guard rejecting any -preview. target version.
2026-07-17 10:26:54 +08:00
Zhengchao An 3b1bed7009 test(e2e): socket-level network fault-injection proxy (backlog#1325) (#4938)
test(e2e): add socket-level network fault-injection proxy (backlog#1325)

Add `FaultProxy`, an in-process async TCP proxy for black-box cluster E2E tests, under `crates/e2e_test/src/fault_proxy.rs`. It binds a random local port, forwards every accepted connection to a fixed target, and lets a test flip the wire behaviour at runtime.

Fault modes: `Pass` (transparent), `Latency(Duration)` (delay each forwarded chunk), `Blackhole` (accept but forward nothing either way and never respond), and `Partition(Direction)` (block exactly one direction while the other keeps flowing). Modes are switchable at runtime via `set_mode`, and `shutdown` stops the accept loop and drops the listener so the bound port is released.

This is the black-box counterpart to the in-process white-box hooks (rename barrier, disk call counters), which cannot reach across the process boundary into a spawned RustFS server. It serves the lock-plane one-way partition and accept-then-blackhole-peer acceptance for #1312/#1319 and the cross-process replay/tamper seam for #1327. Wiring it into the cluster harness (expose a node port through a proxy) plus the 5GiB / nightly CI-lane budget are follow-ups, since the harness multi-drive / 2-pool work lands separately (rustfs#4937); this block stays independent and self-tested.

Self-tests run against a loopback echo server that records received bytes, covering: pass round-trip identity, latency lower-bound delay, blackhole no-response-no-panic, one-way partition in both directions (request-path vs return-path), runtime mode switching on a live connection, and port release after shutdown. Timing assertions use loose lower bounds and bounded read timeouts only, never fixed-sleep absolute-value checks, to stay non-flaky. No product code changes; the module is `#[cfg(test)]`-gated.
2026-07-17 01:04:32 +00:00
Zhengchao An b0b5ffb8e2 test(e2e): cluster harness drivesPerNode + 2-pool topology (backlog #1325) (#4937)
test(e2e): add drivesPerNode and 2-pool cluster harness topology (backlog #1325)

Extend the e2e cluster harness so tests can declare per-node multiple drives (drivesPerNode) and a two-pool topology, alongside a smoke suite that boots such a cluster and round-trips a PUT/GET.

A new `ClusterTopology` describes node_count, drives_per_node, and pool membership, and `RustFSTestClusterEnvironment::with_topology` builds the matching on-disk drive directories and `RUSTFS_VOLUMES` string. `new(node_count)` now delegates to a single-pool single-drive topology, so existing single-drive cluster tests are byte-for-byte unchanged. `ClusterNode` gains `data_dirs` (all drives, `data_dirs[0] == data_dir`) and `pool_idx`; multi-drive/multi-pool clusters automatically add `RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true` because their drives share the same temp filesystem.

The `RUSTFS_VOLUMES` assembly matches the two forms the server parser accepts on a single localhost machine (verified against ecstore `DisksLayout::from_volumes`): a single pool is the explicit enumeration of every `(node, drive)` endpoint (no ellipses, one legacy DistErasure pool), while multiple pools each contribute one ellipses argument `http://<addr><node-base>/drive{0...N-1}`. A pool argument is a single URL template, so it cannot enumerate multiple distinct-port hosts; a pool striped across several localhost nodes would need a host ellipses that forces a shared on-disk path and collides physically. The topology validator therefore requires every pool in a multi-pool layout to own exactly one node and requires drives_per_node >= 2 (the parser rejects a single-drive `drive{0...0}` ellipses pool). Genuine multi-node pools need real multi-host infrastructure and are deferred to the nightly cluster lane (backlog #1313/#1314).

Unit tests assert the volumes-string layout for single-pool single-drive (backward compatible), single-pool multi-drive (8 explicit endpoints), and two-pool (one ellipses arg per pool), plus the validator rejections. The smoke suite `cluster_multidrive_pool_test` boots a 4-node x 2-drive single pool and a 2-node/2-pool cluster, asserts the volumes layout, and round-trips a PUT/GET using the harness readiness handshake (no fixed sleeps). It joins the six existing RustFSTestClusterEnvironment suites excluded from the merge-gate `e2e-full` profile so it runs in the nightly 4-node lane.

Network fault injection (toxiproxy / socket proxy) and 5GiB large-object budgets remain out of scope for this block.
2026-07-17 01:02:38 +00:00
Zhengchao An be2e454d9d test(ecstore): rename/commit fan-out pause barrier + background-task introspection (backlog#1325 block 2) (#4936)
test(ecstore): add rename/commit fan-out pause barrier and background-task introspection

Second white-box test-infra block for https://github.com/rustfs/backlog/issues/1325 (the first block landed the per-disk call counters in PR#4914). Adds a `#[cfg(test)]` awaitable pause barrier plus in-flight background-task introspection to the rename/commit fan-out in `crates/ecstore/src/set_disk/core/io_primitives.rs`, following the same dual-cfg seam style as the existing `disk_call_counters` and `cleanup_fault_injection` seams.

A test arms a barrier for `(object, disk_index, phase)`; the matching spawned fan-out task parks at its checkpoint until the test releases it, and the test awaits the pause through a deterministic `tokio::sync::Notify` handshake (no sleeps). A separate object-keyed task tracker reports how many rename/cleanup background disk tasks are still in flight, so a test can assert "a background disk write is still running" while paused and "no background disk write remains" once the fan-out drains. Both mechanisms live in one process-global registry keyed by object name, so concurrent tests using distinct object names stay isolated. Barriers are placed on the real `rename_data` fan-out (phase `rename`) and the `commit_rename_data_dir` old-data-dir cleanup fan-out (phase `cleanup`).

In production the barrier compiles to an immediately-ready `#[inline(always)]` no-op future and the task guard to `()`, so the fan-out control-flow shape and behavior are unchanged; only the `#[cfg(test)]` variants touch the registry. Coordinator lock-holding is asserted by the test at the store/coordinator layer via the guard it already holds; io_primitives has no handle to that namespace lock. Cross-process/black-box fault injection (toxiproxy, blackhole peers, 2-pool) remains a later cluster-harness block.

Serves the barrier-style white-box acceptances of #1312 (commit fencing: abort at the first-disk rename barrier, assert no background disk write remains after release), #1319, and #1313. Three demo tests drive the real fan-out functions and double as regression guards: neutralizing the barrier seam makes the pause await time out, and neutralizing the task guard pins the in-flight count at zero, so reverting either seam fails the demos.
2026-07-17 08:31:41 +08:00
Zhengchao An 4ff7775ecb fix(admin): map service account validation errors (#4932) 2026-07-17 08:31:17 +08:00
Zhengchao An 78469aa63b fix(get): bound GET disk-read admission with a hard cap instead of unbounded permit bypass (#4935)
Refs https://github.com/rustfs/backlog/issues/1317

Previously, when the primary disk-read permit pool stayed saturated past `RUSTFS_OBJECT_DISK_PERMIT_WAIT_TIMEOUT` (default 5s) — which a handful of slow clients can cause because a permit is held for the whole body transfer — the GET path set `disk_permit = None` and continued reading with no admission token at all. That made the disk-read concurrency limit unbounded under exactly the overload it is meant to protect against: any number of GETs could pile onto the disks simultaneously.

This replaces the permit-less bypass with a bounded overflow lane and a hard cap:

- A new bounded degraded semaphore (`RUSTFS_OBJECT_DISK_DEGRADED_READ_CAP`, default mirrors the primary cap) is consulted only after the primary wait times out. A GET takes one degraded permit without blocking, or is rejected with `SlowDown`/503 once that lane is also full. The total number of GETs performing disk-active reads is therefore hard-capped at `primary_cap + degraded_cap`, and no GET ever reads without holding an admission token.
- Admission is centralized in `ConcurrencyManager::admit_disk_read`, returning `Primary`/`Degraded`/`Unbounded`/`Rejected`. `Degraded` and `Rejected` are counted in metrics (`rustfs.get_object.disk_permit.degraded.total`, `rustfs.get_object.disk_permit.hard_reject.total`), replacing the removed `rustfs.get_object.disk_permit.bypass.total`.
- A primary cap of `0` (disk-read throttling disabled) is preserved as the only intentional permit-less path via `Unbounded`, so that degenerate-but-served configuration is not turned into all-503. The `primary_wait == 0` wait-forever opt-out is likewise unchanged.

Healthy GETs are unaffected: concurrency at or below the primary cap is admitted immediately from the primary pool exactly as before, with no new latency or rejection. The rejection is surfaced before response headers are constructed, so it is a clean pre-header 503, never a post-header 504 masquerade. Degraded-lane GETs use the identical streaming reader and forward-progress stall timeout as primary GETs, so a slow but progressing large download is not killed. Owned permits (primary or degraded) are held by `DiskReadPermitReader` and released on body EOF or client drop/cancel, so tokens are always returned.

Body stall timeout already resets on forward progress and post-header failures already surface as body errors, so no timeout-split changes were needed here.

Scope: this PR implements the minimal safe correctness core (eliminate unbounded bypass + hard cap + SlowDown). The two-level weighted-fair + aging scheduler (small setup cap feeding a size-aware data-producer fair queue) and the strictly-bounded producer/client buffer with cancellation propagation from the issue plan remain follow-ups. The black-box slow-client soak is deferred to the unbuilt facility in https://github.com/rustfs/backlog/issues/1325 rather than faked.

Tests (white-box, virtual clock via `start_paused`): degrade-then-hard-reject with token release; 100 concurrent GETs against primary=1/degraded=1 never exceed 2 simultaneous admissions with every request either admitted or explicitly rejected; disabled cap serves unbounded; zero-wait blocks on the primary lane. Reverting the hard cap makes the concurrency invariant fail.
2026-07-17 08:30:39 +08:00
Zhengchao An a2a336aec3 fix(replication): compute the PUT replication decision exactly once (#4934)
The PutObject usecase computed `must_replicate_object` twice with the same inputs: once before commit to persist the pending replication metadata, and again after commit to drive `schedule_object_replication`. Besides repeating the versioning/config/target traversal on the hot path, the two computations read the replication configuration independently, so a config hot update landing between them could split the two phases into a pending-without-schedule or schedule-without-pending divergence.

Reuse the single immutable `ReplicateDecision` computed before commit for both the pending metadata and the post-commit schedule. The two former computations were already equivalent for a stable config (`must_replicate` reads only `opts.replication_request` from the options, which the pending-suffix insertion does not touch), so this preserves replication semantics while removing the redundant traversal and closing the config-race window. The decision carries only stable target/rule/status identifiers (arn, replicate, synchronous, id) and no secrets.

Add a white-box regression test that drives a real PutObject through the usecase and asserts, via a test-only invocation counter on `must_replicate_object`, that a single PUT computes the decision exactly once; reverting to the pre-commit + post-commit recompute makes the counter observe 2 and fails the test.

Refs: https://github.com/rustfs/backlog/issues/1320
2026-07-17 08:30:03 +08:00
Zhengchao An 4d22ed4465 perf(capacity): drop per-PUT global lock and per-disk allocation from write dirty-scope (#4933)
perf(capacity): remove per-PUT global lock and per-disk allocation from write dirty-scope

Every successful write recorded its capacity dirty scope by allocating an endpoint/path String per online disk, deduplicating through a HashSet, entering the global dirty-scope Mutex, and — in the app response path — taking a global async RwLock to record the write frequency. Under small-object high concurrency this created a global serialization point and O(disks) allocation on the hot path (https://github.com/rustfs/backlog/issues/1315).

This change makes the steady-state write path allocation-free and lock-free without altering capacity accounting semantics:

- Memoize the per-set dirty scope. Each set resolves its disks' immutable endpoint/path identity lazily into a slot-indexed cache and reuses a shared `Arc<CapacityScope>`; steady-state writes clone the Arc under a read lock instead of rebuilding String/HashSet. The heal path keeps an ad-hoc scope builder because it passes disks in erasure-distribution order rather than physical-slot order.
- Add a monotonic generation to the global dirty-scope registry, advanced only when a non-empty drain removes disks. A set upgrades the global registry mutex only on the first write of each generation and then skips it while the generation is unchanged; the observed generation is read under the registry lock so a concurrent drain forces a re-mark, preventing lost updates. The write commits its bytes before recording the scope, so any drain that could remove the mark is ordered after the commit and the following refresh reads the committed bytes.
- Replace the write-frequency `RwLock<WriteRecord>` with lock-free atomics: per-second CAS buckets, an atomic last-write timestamp, and an atomic total counter. The frequency window and debounce semantics the refresh scheduler relies on are unchanged.

Capacity marking remains a conservative superset of the disks actually written, so admin/scan totals are byte-for-byte identical: extra dirty marks only trigger a re-read of a disk whose usage is unchanged. White-box tests assert the memoized scope equals the previous ad-hoc construction, that the global registry is upgraded exactly once per generation and re-marked after a drain, and that the lock-free write record is exact under concurrent contention.

Ref: https://github.com/rustfs/backlog/issues/1315
2026-07-17 08:29:38 +08:00
Zhengchao An fc7d46b6cf fix(quota): admit hard quota against authoritative decoded size and fail closed on checker faults (#4928)
fix(quota): admit bucket hard quota against authoritative decoded size and fail closed on checker faults

Bucket-quota admission for PUT/POST previously ran before the authoritative object length was known and used the raw wire Content-Length: for an aws-chunked upload that length counts chunk framing (overcounting) and can be absent entirely, in which case the check was silently skipped. Separately, any quota-checker fault (bucket-config read, config parse, usage lookup) degraded to allow, which silently bypasses a configured hard quota.

Resolve the authoritative decoded/plain object length first — rejecting negative and unknown lengths, and requiring x-amz-decoded-content-length for aws-chunked instead of falling back to the framed wire length — then run quota admission exactly once against that size. This is the same basis the settle phase records via ObjectInfo.size, so admission and accounting agree. When no quota is configured the QuotaChecker keeps its zero-extra-I/O fast path; once a hard quota is set, checker faults now fail closed with a retryable ServiceUnavailable, increment rustfs_bucket_quota_check_failed_total, and keep the client-facing message generic so internal config/usage details are not leaked.

Size resolution and quota-outcome mapping are extracted into pure functions (resolve_put_object_authoritative_size, map_quota_check_outcome) with unit tests covering aws-chunked decoded-vs-wire, missing/negative/unknown lengths, plain PUT, the exact/over-limit admission split, and fail-closed on checker error. QuotaCheckResult is re-exported through the ecstore api::bucket::quota surface for the app layer. Cross-node reservation and overwrite-delta accounting remain out of scope (sibling issue). Also corrects one stale doc path (set_disk/core/local.rs -> disk/local.rs) flagged by the doc-paths guard.

Refs: https://github.com/rustfs/backlog/issues/1311
2026-07-16 18:10:52 +00:00
Zhengchao An b41bbe2db4 fix(ecstore): split rename_data signature from heal-convergence decision (#4926)
CompleteMultipartUpload enqueued a normal-priority heal whenever
`rename_data` returned a `Some(versions)` signature. But the per-disk
signature is produced for every object with <=10 versions, and a healthy
quorum reduces to `Some` as well, so the `Option<Vec<u8>>` return value
conflated two distinct facts — "a version signature exists" and "the
committed replicas need heal". The result: nearly every healthy MPU
completion self-enqueued a heal, while >10-version objects (signature
`None`) did not — an algorithmic heal amplification on the healthy path
(rustfs/backlog#1321).

Replace the overloaded `Option<Vec<u8>>` second element of
`SetDisks::rename_data` with an explicit `RenameConvergence` classification
computed after the write-quorum gate:

- AllSuccessIdentical — every attempted disk committed with an identical,
  known signature (no heal).
- PartialCommit — write quorum met but a disk failed/offline; a committed
  replica is missing or stale (heal).
- SignatureDivergent — all committed but signatures diverge, or mix signed
  (<=10-version) with unsigned (>10-version) disks (heal).
- Unknown — all committed, no signature produced (>10 versions); latent
  divergence is left to the scanner backstop, not self-enqueued.

`RenameConvergence::needs_heal()` is the single decision point. The version
signature is now only comparison material; it no longer doubles as a heal
flag. The old `select_rename_data_versions` / `reduce_common_versions` /
`rename_data_versions_key` machinery that carried the conflation is removed.

The heal submission in `complete_multipart_upload` moves off the ACK
critical path into a detached task: it runs after the object lock is
dropped and after the durable `rename_data` commit, survives cancellation
of the completion future, and coalesces through the existing bounded /
deduplicated / observable heal-channel admission (one submit per degraded
completion, at most). A completion cancelled in the narrow window between
the durable commit and reaching the enqueue is scanner-backstopped, as is
the Unknown (>10-version) case.

The PUT path (`object.rs`) binds the second element as `_` and is
unchanged. The change is orthogonal to and composes with the #1312 commit
fence on the same `rename_data` path (epoch rejection is a commit-gate
failure surfaced through `Result::Err`, convergence is a post-commit
signal); documented in docs/architecture/unified-object-generation.md.

Tests: `classify_rename_convergence` white-box cases cover the full
acceptance matrix (healthy 4/4 and 8/8, 3-same-1-divergent, failed/offline
disk, no-common-quorum split, >10-version all-success and with-failure,
mixed signed/unsigned) and fail on revert to the old "signature exists =>
heal" semantics. The decision function is tested directly rather than
through the process-global heal channel, whose receiver is owned
exclusively by the blackbox serial test (init_heal_channel is once per
binary).

Refs: https://github.com/rustfs/backlog/issues/1321
2026-07-17 01:38:15 +08:00
Zhengchao An 6559248f55 fix(ecstore): make legacy stripe prefetch cancel-safe on emit termination (#4930)
The legacy erasure-decode overlap path drove the speculative next-stripe read and the current-stripe emit with `tokio::join!`, which runs both futures to completion. When the current stripe's emit terminated the loop — a client disconnect or any emit error — the join still waited for the prefetch read, so a `Stop` could stall for a full shard-read deadline on a slow or wedged remote shard before the GET could fail.

Drive the two futures with a biased `select!` instead and, the moment emit reports `Stop`, drop the in-flight read future. Because the entire read pipeline is structured async (a `FuturesUnordered` of `read_shard` futures inside `ParallelReader::read`/`read_lockstep`, with no `tokio::spawn`), dropping the read future is a real cancellation: it drops every in-flight shard read and propagates cancellation down to the RemoteDisk/HTTP reader, leaving no background read behind. The `select!` is scoped so both pinned futures drop before `reader`/`shards` are reused, which is what performs the cancellation in the `Stop` case.

This only affects the overlap-enabled path. The default remains OFF (`prefetch_count == 1` and bitrot-decode overlap disabled), and the strictly-serial read -> reconstruct -> emit default branch is untouched and byte-for-byte identical. The `Continue` path preserves offset, short-tail, buffer recycle, and bitrot/reconstruction error ordering exactly as before.

Scope: cancel-safety only. The rollout decision (whether to enable overlap by default) still requires the Linux multi-node high-RTT three-size A/B from https://github.com/rustfs/backlog/issues/1310 and is deferred; this change does not flip the default or introduce any behavior that A/B must adjudicate.

White-box test `test_legacy_prefetch_cancels_next_read_on_emit_failure` drives the real `Erasure::decode` path with overlap enabled, a writer that fails emit, and shards that serve the first stripe then stall the next-stripe read far beyond the assertion window. Under the paused clock the read future is dropped and decode returns at virtual t~=0; reverting cancel-safety makes it wait out the shard-read timeout, so the test fails closed.

Refs: https://github.com/rustfs/backlog/issues/1310
2026-07-17 01:37:44 +08:00
Zhengchao An caf42018b1 fix: pin tokio to 1.52.3 and fix stale doc path reference (#4931)
fix(deps): pin tokio to 1.52.3 to fix dial9-tokio-telemetry build

tokio 1.52.4 made  private, breaking
dial9-tokio-telemetry 0.3.14 which references it as public.
Pin tokio back to 1.52.3 until the upstream dependency fixes
compatibility.

Also fix a stale doc path reference in unified-object-generation.md
where  is a planned file that does not exist yet.
2026-07-16 17:36:37 +00:00
Zhengchao An 7cc92ac93c test(replication): add programmable fake S3 target (#4929) 2026-07-17 01:20:09 +08:00
Zhengchao An 3674f5f56e fix(ecstore): bound remote shard writers with a progress deadline so one black-hole peer cannot pin write quorum (#4925)
A PUT that fans out erasure shards to remote peers awaited every shard writer to completion on both the per-block write and the final shutdown, and the remote HttpWriter had no progress deadline. A peer that accepts the TCP connection but never drains the request body (or never sends a response) therefore wedges the writer forever once the bounded buffers fill, pinning an otherwise-healthy write quorum indefinitely — a cluster-level write-availability hazard triggered by a single bad peer (rustfs/backlog#1319, https://github.com/rustfs/backlog/issues/1319).

MultiWriter now wraps each shard write and each shard-writer shutdown in a forward-progress deadline. The budget is re-armed on every block, so it bounds a stall rather than the total transfer time of a large object: a slow-but-honest writer that keeps completing shards is never killed, while a writer that makes no progress within the budget is failed and its disk dropped before commit. An optional absolute per-object cap (disabled by default) backstops a slow-drip peer that dribbles just enough progress to reset the per-block timer without ever converging; it is off by default so a legitimate large upload over a slow link is not killed on total time alone. Both knobs come from RUSTFS_OBJECT_DISK_WRITE_STALL_TIMEOUT (default 30s) and RUSTFS_OBJECT_DISK_WRITE_ABSOLUTE_CAP (default 0 = disabled); setting the stall timeout to 0 restores the previous wait-forever behavior for a conservative rollback.

The deadline enforcement lives in MultiWriter (writer-agnostic), so it covers local and remote writers alike and keeps the existing control-flow shape: a timed-out shard is marked failed (Error::Timeout, which is not an ignored error) and excluded from the write quorum exactly like any other shard write failure, and the unchanged nil_count/quorum check then continues on quorum or fails cleanly. This deliberately stays out of the MultiWriter lifecycle / commit-coordinator territory owned by rustfs/backlog#1312.

When a stalled writer is dropped to fail its shard, the remote HttpWriter must stop holding the connection and its buffered body. HttpWriter previously left its spawned request task running on drop; it now aborts that background task in Drop (it is no longer pin-projected, since every field is Unpin and the AsyncWrite impl already used get_mut). Bytes already handed to the transport cannot be unsent, but they land only in this upload's unique tmp path and are reclaimed by tmp GC — they never touch a committed object.

Tests, all on a paused virtual clock so they are deterministic and non-flaky:
- one black-hole writer still meets a 3/4 write quorum without hanging; two black holes fail the quorum cleanly (both for the per-block write and the shutdown paths).
- a slow-but-honest writer that keeps making progress within the stall budget is never failed across many blocks.
- the absolute cap bounds a slow-drip writer within a finite budget while the healthy writers keep quorum.
- the default policy is armed by default and honors 0 as disabled.
- HttpWriter aborts its background request task on drop against a hanging peer.

The toxiproxy/black-hole 4x4 end-to-end acceptance depends on black-box test facilities from rustfs/backlog#1325, which are not built yet; that acceptance is deferred to #1325 and intentionally not faked here.
2026-07-16 16:41:38 +00:00
houseme 4f04d5f883 chore(docker): update Alpine and Ubuntu base images (#4924)
chore(docker): update base images

Upgrade Alpine images to 3.24.1 and Ubuntu runtime images to 26.04.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 16:36:36 +00:00
Zhengchao An 84a34890ef docs(architecture): pin JSON→msgpack migration interaction for generation transport (#4913) 2026-07-17 00:12:24 +08:00
Zhengchao An da0c2d3730 test(ecstore): per-disk call-counter registry for metadata fan-out (backlog#1325 block 1) (#4914)
test(ecstore): add per-disk call-counter registry for metadata fan-out

First landable piece of the backlog#1325 test-infrastructure work: a test-only, per-disk call-counter registry that can observe `read_version` RPC counts recorded inside `tokio::spawn` tasks. This unblocks the RPC-count assertions in backlog #1309 / #1314 / #1315, which the thread-local `CapturingRecorder` cannot serve because it is blind to metrics emitted from spawned tasks.

The new `#[cfg(test)]` module `disk_call_counters` (modeled on the existing `cleanup_fault_injection` seam) is a process-global registry keyed by object name; an RAII `observe(object)` scope collects per-disk counts and clears only its own on drop, so parallel tests using distinct object names stay isolated. A dual-`cfg` `SetDisks::record_read_version_call` seam records from inside both metadata-fanout `read_version` spawn sites for online disks only; the `#[cfg(not(test))]` variant is an empty `#[inline(always)]` fn, so production runtime behavior is unchanged.

Two demo/regression tests prove the facility works across worker threads and is revert-detecting (neutralizing the recorder makes them fail).

Refs: https://github.com/rustfs/backlog/issues/1325
2026-07-17 00:02:58 +08:00
Zhengchao An 73d75428a8 fix(get): enforce strict exact-length materialization for in-memory GET bodies (#1324) (#4922)
* fix(get): enforce strict exact-length materialization for in-memory GET bodies (#1324)

The streaming GET path already fails a short read with UnexpectedEof, but several in-memory materialization branches only WARNed on a length mismatch and then served the body anyway: the encrypted-buffer path, the direct-memory/cache buffered-body path, and the seek-support buffer. Most dangerously, the seek-support path fell through to streaming the *same* reader after read_to_end had already drained K bytes on error, shipping a body missing its prefix (prefix-misaligned data — the closest this code came to returning wrong bytes rather than merely a short body).

Because the HTTP response commits to Content-Length == expected before the body is produced, any other body length is an unrecoverable, broken response. This change gives every in-memory source the same exact-length contract that the ODC materialize-fill path already had.

- Add strict_materialize_object_body, a shared helper that bounds the read to expected+1 (so an over-long stream is detected without buffering it unbounded), requires bytes_read == expected, and on any read error returns without reusing the partially consumed reader. The encrypted, seek, and ODC materialize branches now all route through it.
- The buffered-body branch hard-fails a length mismatch before headers instead of WARN-and-serve.
- MemoryTrackedBytesStream gains a defense-in-depth guard: a buffer whose length disagrees with the declared content length yields a stream error on first poll instead of a clean short body or a silently truncated over-long body. This backstops the cache-hit paths that hand bytes straight to the blob.

The compatibility boundary is preserved: expected is derived from get_actual_size(), the same value used for the committed Content-Length, so a legitimate object (including a legacy/backfilled-size object) whose decoded bytes equal its declared size still serves cleanly — only genuine short, over-long, or errored reads fail. This matches the streaming path, which already hard-fails short reads, so no new large-object behavior is introduced.

Tests: each source (encrypted/seek via the shared helper, cache via build_get_object_body_with_cache, buffered/memory via MemoryTrackedBytesStream) is exercised with expected N and actual N-1 / N / N+1 plus a read-K-then-error injection; only the exact-length read succeeds. Reversal is guarded: restoring WARN-and-serve or a partial fallback flips the short/over-long/error assertions from Err to Ok. Existing streaming UnexpectedEof tests are unchanged.

Refs: https://github.com/rustfs/backlog/issues/1324

* fix(app): reword WARNed comment to satisfy typos check
2026-07-16 16:01:10 +00:00
Zhengchao An 082061f18b fix(object): losslessly convert suffix Range from u64 to i64 (#4921)
s3s parses a `Range` suffix length as `u64`, but the GET and HEAD handlers cast it straight to `i64` and bypass any satisfiability check. This truncates deterministically: `bytes=-18446744073709551615` wraps to `-1` and is then read as "last 1 byte", and `bytes=-0` produces a 0-length 206 instead of a 416.

Both handlers now share a single `range_to_http_range_spec` conversion that rejects a zero-length suffix with `InvalidRange` (416), clamps any suffix above `i64::MAX` to `i64::MAX` (such a suffix always covers the whole object, which `HTTPRangeSpec::get_length` then clamps to the real size), and keeps the int branch as a checked cast (s3s already caps `first`/`last` at `i64::MAX`). The scattered `as i64` casts are removed.

Note on ordering: a zero-length suffix is now rejected at conversion time, so `bytes=-0` on a missing object returns 416 rather than 404. This matches the handler's existing behavior of validating range shape (range + partNumber -> 400) before object existence.

Adds a table-driven unit test covering suffix `0/1/size/size+1/i64::MAX/i64::MAX+1/u64::MAX` over empty, 1-byte, and normal objects, asserting the InvalidRange (416) mapping, full-object return for over-size suffixes, and no regression of int first-last / open-ended ranges.

Refs: https://github.com/rustfs/backlog/issues/1322
2026-07-17 00:00:31 +08:00
Zhengchao An cbf1c69234 docs(architecture): fix object commit path reference (#4920) 2026-07-16 23:21:47 +08:00
Zhengchao An 2ae2081753 docs(skill): unwrap prose lines and add semver.org references (#4918)
Remove hard line wrapping from rustfs-release-publish prose (one logical line per sentence/paragraph, soft wrap for display) and link SemVer 2.0.0 spec including spec item 11 for prerelease precedence.
2026-07-16 22:54:28 +08:00
Zhengchao An 1305f7590d docs(skill): add semver confirmation gate to rustfs-release-publish (#4916)
Require explicit target-version confirmation (AskUserQuestion with concrete semver candidates derived from the latest tag) before any release phase runs, and document SemVer 2.0.0 precedence and bump-type rules.
2026-07-16 22:50:29 +08:00
Zhengchao An f17f0470b0 docs(skill): add rustfs-release-publish preview-validated release pipeline (#4915)
Adds a project skill that orchestrates the full release flow: preview tag first, CI/artifact verification, local run with console checks, rc client command validation, then final version bump and tag cut from the validated preview hash instead of latest main.
2026-07-16 22:46:48 +08:00
Zhengchao An f40abbb9f2 docs(architecture): unify per-object generation authority (#4912)
Add the shared design/contract document for backlog #1326: a single
per-object generation authority (the #1312 fencing epoch) that spans
commit fencing, read lease, prepared pool read, quota reservation, and
old-dir GC. Pins the five cross-cutting constraints once - RPC signature
binding with server-side nonce enforcement, xl.meta encoding contract
(no meta_ver bump, no positional FileInfo field, internal metadata map
under the dual-key contract), proto3 optional presence, mixed-version
fallback direction, and cluster-level capability negotiation - so the
implementation sub-issues follow them rather than each re-deciding.

No product code changes.
2026-07-16 22:29:07 +08:00
Zhengchao An dbfe1c9bae fix(docker): upgrade base-image packages in runtime stages to clear Trivy CVE alerts (#4909)
fix(docker): upgrade base-image packages in runtime stages

Trivy code scanning reports 40 open alerts against the published
container images, all from OS packages frozen at the base-image tag:

- musl image (alpine:3.23.4): openssl libssl3/libcrypto3 3.5.6-r0,
  including HIGH CVE-2026-45447; fixed in 3.5.7-r0
- glibc image (ubuntu:24.04): tar, gzip, perl-base, ncurses CVEs with
  fixes already published in the Ubuntu archive

The runtime stages only ever installed packages and never upgraded the
ones shipped with the base image, so distro security fixes could not
reach released images until the base tag itself moved. Run
`apk upgrade` / `apt-get upgrade -y` in the runtime stages of
Dockerfile, Dockerfile.glibc and Dockerfile.source so each build picks
up current security fixes.

Verified against the exact base tags: after upgrade, alpine 3.23.4
resolves libssl3/libcrypto3 3.5.7-r0 and ubuntu 24.04 resolves
tar 1.35+dfsg-3ubuntu0.2, gzip 1.12-1ubuntu3.2,
perl-base 5.38.2-3.2ubuntu0.3, ncurses 6.4+20240113-1ubuntu2.1 —
matching every fixed version demanded by the open alerts.
2026-07-16 22:15:29 +08:00
Zhengchao An dc4b85eb2d ci: log console version and completion after asset download (#4910) 2026-07-16 22:11:59 +08:00
houseme 75c3403dcc chore(release): prepare 1.0.0-beta.10-preview.4 (#4908)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 14:08:34 +00:00
Zhengchao An 381639fbbc fix(release): require embedded console assets (#4907) 2026-07-16 21:23:48 +08:00
houseme 1badff3923 fix: resolve moved-value diagnostics (#4905)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 18:46:56 +08:00
Zhengchao An d5baaa67b8 ci: restore latest.json updates for prerelease tags (#4903)
PR #4582 gated the update-latest-version job to stable release tags so prereleases could not overwrite the stable version pointer. But the project currently ships prerelease tags only (beta previews), so the job never runs and latest.json has been stale since. Run the job for every release tag again, and keep the honest half of the #4582 fix by writing release_type from the actual build type instead of hardcoding "stable".
2026-07-16 17:39:56 +08:00
Henry Guo f5e715a8fd fix(table-catalog): allow table recreation after drop (#4900)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-16 17:25:47 +08:00
Zhengchao An e2f394a897 feat(helm): flexible drivesPerNode topology with backward-compatible per-pool defaults (#4901)
* helm chart: one data drive per node - fix1

* refactor(helm): prevent negative or 0 replicaCount

Co-authored-by: Copilot <copilot@github.com>

* refactor(helm): remove env REPLICA_COUNT

* feat(helm): default no logs directory to force stdout

Co-authored-by: Copilot <copilot@github.com>

* feat(helm): add drivesPerNode

* feat(helm): correct default parity with 4 nodes /  1 drive per node

* conditional render of RUSTFS_OBS_LOG_DIRECTORY

* feat(chart): add table doc for parity

* fix(chart): handle invalid annotation objects

* fix(chart): move logging and obsevability options together; default for kubernetes output to stdout

* feat(chart): better table doc for parity

* fix(chart): leave RUSTFS_OBS_LOG_DIRECTORY empty, or the defaults will attempt to write to readonly fs

* fix(chart): chart defaults as the previous version: 4 nodes with 4 drives per node

* feat(chart): render RUSTFS_STORAGE_CLASS_STANDARD in configmap

* minor fix for pvcAnnotations

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Cristian Chiru <cristi.chiru@gmail.com>

* fix(chart): better drivesPerNode handling

* fix(chart): obs_log_directory default non empty again to preserve previous deployments compatibility

* fix(chart): proper drivesPerNode impl

* fix(chart): values clarification for empty vs unset `obs_log_directory`

* feat(chart): add service externalIPs

* feat(helm): add optional service labels

* fix(helm): merge service labels

* fix(helm): storageclass pvcAnnotations

* fix(chart): move logging and obsevability options together; default for kubernetes output to stdout

* fix(helm): remove duplicate keys

* fix(helm): default drivesPerNode null, keep legacy chart behavior

* feat(helm): add template test for externalIPs

* fix(helm): per-pool drivesPerNode inference, restore regression tests

Repairs the drivesPerNode feature from #2693 so it renders and stays
upgrade-safe:

- define the missing drives inference (rustfs.poolDrives) and compute it
  per pool inside rustfs.pools, so mixed 4x4 + 16x1 pool deployments keep
  their exact legacy volumeClaimTemplates when drivesPerNode is unset
- restore the $poolsEnabled definition dropped in the rebase (chart failed
  to render at all)
- fix .Values references inside the pool range (dot is the pool there) and
  keep per-pool storageclass pvcAnnotations overrides working
- make rustfs.volumes derive the drive range from pool drives instead of
  the pod count, and relax the pools.list 4-or-16 restriction to >= 2
- reject drivesPerNode=0 explicitly instead of silently inferring
- keep default rendering identical to main: storage_class_standard stays
  unrendered by default, obs_endpoint.use_stdout stays false, clusterDomain
  value restored
- restore the clusterDomain/mTLS SAN/explicit-volumes regression tests that
  the branch deleted, keeping the new topology tests

---------

Signed-off-by: Cristian Chiru <cristi.chiru@gmail.com>
Co-authored-by: Cristian Chiru <cristi.chiru@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 09:02:04 +00:00
houseme 8a126bb176 ci: drop macOS x86_64 release target (#4899)
Remove the Intel macOS entry from the Build and Release platform matrix so official release builds only publish the Apple Silicon macOS binary.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 16:03:20 +08:00
Zhengchao An ae15f5804d test(ilm): fix restore integration test object key to match transition filter (#4886)
* test(ilm): fix restore test object key to match transition filter

restore_object_usecase_reports_ongoing_conflict_and_completion used the
object key "restore/api-object.bin", but the shared
set_bucket_lifecycle_transition_with_tier helper only transitions objects
under the "test/" prefix. enqueue_transition_for_existing_objects therefore
matched nothing and wait_for_transition timed out at 15s, failing the test
deterministically.

The test was added in #4860 but its ILM Integration (serial) lane is
skipped on regular PRs, so it merged red and has failed on every main run
since. Move the object under the test/ prefix like every passing sibling
test in this file.

* ci(ilm): exclude broken RestoreObject API test from serial lane

restore_object_usecase_reports_ongoing_conflict_and_completion exposes a
real regression, not a test bug: the RestoreObject copy-back
(handle_restore_transitioned_object) now holds the object write lock added
in #4877 across the entire tier read-back, so it never releases in time and
the test's concurrent get_object_info times out with Lock(Timeout, 5s). The
failure is deterministic and independent of the mock tier's injected latency.

This is the same class of known-broken restore/transition failure already
tracked under backlog#1148 (three sibling scanner tests are excluded here by
name for the same reason), so exclude this one the same way until the restore
copy-back path is fixed or the #4877 lock scope is revisited. The prior commit
keeps its correct fix (the object key must live under the test/ transition
prefix); that was masking this deeper issue by never letting the object
transition in the first place.

Restore copy-back deadlock/hang under the #4877 lock is escalated separately
for a product-level decision (fix the copy-back vs. narrow/revert #4877).

* test(ilm): fix scanner restore test object keys to match transition filter

test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore and
test_multipart_restore_preserves_parts_and_etag (both added in #4860) keyed
their objects under restore/ instead of the test/ prefix that
set_bucket_lifecycle_transition_with_tier filters on, so the objects never
transitioned and wait_for_transition timed out at 15s.

These surfaced only after the prior commit excluded the rustfs-side restore
API test: nextest runs -j1 fail-fast, so that earlier failure stopped the run
before these scanner tests executed. Unlike the excluded API test, both call
restore_transitioned_object().await sequentially and only read afterwards, so
they don't hit the concurrent-read-vs-#4877-write-lock timeout; the key
prefix was their only problem.

* ci(ilm): exclude the two remaining #4877-broken restore tests

test_multipart_restore_preserves_parts_and_etag and
test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore both
call restore_transitioned_object().await, which since #4877 acquires the
object write lock and deterministically times out (Lock Timeout, 5s) against
an already-held lock, so restore never completes. They surfaced one at a time
because nextest runs -j1 fail-fast. The earlier prefix fix was necessary but
only advanced them from the transition wait to this restore-lock timeout.

Exclude both by name alongside their already-excluded sibling
test_transition_and_restore_flows (same root cause, tracked under
backlog#1148) so the ILM Integration (serial) lane goes green. The #4877 lock
scope still needs a product fix before any of these re-enable.

* docs(ilm): describe the excluded restore tests' symptom as a lock timeout, not a deadlock

The #4877 write lock is held across the tier read-back and outlives the 5s
lock timeout; nothing proves a true deadlock. Wording flagged by Copilot
review.

* fix(ecstore): stop restore copy-back self-deadlocking on the #4877 write lock

#4877 made handle_restore_transitioned_object hold the object write lock for
the whole restore and forward no_lock=true so the set layer would not
reacquire it. But the set-level copy-back rebuilds its own options
(put_restore_opts -> ropts, and the complete_multipart_upload opts) that
default no_lock=false, so the inner put_object / new_multipart_upload /
complete_multipart_upload each re-acquire this object's write lock in their
commit phase and block on the lock the restore already holds -> Lock(Timeout,
5s), and restore never completes.

Confirmed via RUSTFS_OBJECT_LOCK_DIAG_ENABLE: restore_transitioned_object
acquires the write lock, then holds it ~10.5s across two nested 5s acquire
timeouts before failing. This is a real product deadlock: a RestoreObject on
any transitioned object (multipart especially) hangs, not just the tests.

Propagate no_lock into the copy-back options so the inner writes inherit the
already-held lock. Use opts.no_lock (not a hardcoded true) so a caller that
restores without the outer lock still locks correctly. put_object_part is left
as-is: it locks the multipart upload-id resource, not the object key, so it
does not conflict. Verified test_multipart_restore_preserves_parts_and_etag
now passes (3.6s, was a 15s+ hang).

* ci(ilm): re-enable multipart restore test; scope remaining exclusions

The prior commit fixes the #4877 restore self-deadlock, so
test_multipart_restore_preserves_parts_and_etag passes again - drop it from
the serial-lane exclusion list and remove its 'currently excluded' note.

The other restore/transition tests still fail, but each on a DIFFERENT,
independent issue unrelated to the (now-fixed) lock, verified locally:
  - test_restore_chain_...: DeleteRestoredAction sets expire_restored but no
    delete path reads it, so cleanup deletes the whole object (unimplemented
    semantics), not the local restored copy only.
  - test_transition_and_restore_flows: transition xl.meta missing on one drive
    (EC metadata distribution), not restore.
  - restore_object_usecase_reports_ongoing_conflict_and_completion: asserts a
    concurrent mid-restore ongoing=true read that #4877's read-vs-restore
    serialization rules out (backlog#1148 ilm-8 criterion 1, an API-semantics
    decision).
Comments and #[ignore] reasons updated to reflect each real cause. All remain
tracked under backlog#1148.
2026-07-16 07:43:57 +00:00
houseme 07e643cac2 chore(release): prepare 1.0.0-beta.10-preview.1 (#4898)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 15:24:30 +08:00
Zhengchao An 61dbaf60d3 ci: install zip and aws CLI on self-hosted runners (#4897) 2026-07-16 15:23:51 +08:00
Zhengchao An c82ee6be58 feat(api): wire opt-in per-client S3 API rate limiting (429 + Retry-After) (#4895)
feat(api): wire opt-in per-client S3 API rate limiting (backlog#1191)

RustFS shipped three rate-limiter implementations and none was wired to
any request path: the tower layer never returned 429 (its over-limit
branch passed requests through) and was never instantiated, the console
env switches only logged, and the Swift token bucket was never called.

Replace them with one working, default-off implementation:

- Rewrite rustfs/src/server/rate_limit.rs as a sharded per-client-IP
  token-bucket limiter (32 mutex shards instead of one global RwLock
  write per request), bounded at 100k tracked IPs with lossless
  refilled-idle sweeps, returning 429 + Retry-After + x-ratelimit-*
  headers and an S3-style XML body.
- Key on trusted-proxy-validated ClientInfo.real_ip, else the socket
  peer address; never read spoofable X-Forwarded-For/X-Real-IP headers.
  Requests without a resolvable identity fail open. The echoed request
  id is charset-gated to prevent reflected XML injection.
- Wire the layer once at startup via option_layer between
  CatchPanicLayer and ReadinessGateLayer (external stack only), gated by
  new RUSTFS_API_RATE_LIMIT_ENABLE/_RPM/_BURST constants; health and
  profiling probes, internode RPC/gRPC, and the console are exempt.
- Make RUSTFS_CONSOLE_RATE_LIMIT_ENABLE/_RPM actually enforce by
  reusing the same limiter core through an axum middleware.
- Delete the dead Swift ratelimit module, its isolated tests, and the
  stale logging-guardrail entry; keep the live SwiftError 429 mapping.
- Add unit tests (exhaustion/recovery with injected time, concurrency,
  cap eviction, spoofed-header and fail-open behavior, env matrix) and
  e2e tests proving 429 + Retry-After on the real server and zero
  behavior change with default configuration.
2026-07-16 07:09:42 +00:00
houseme 48b328d0d2 chore(deps): tighten crate dependency features (#4896)
* chore(deps): tighten crate dependency features

Narrow Tokio and dependency feature declarations for protocols, TLS runtime, utils, targets, and replication based on direct crate usage.

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(deps): trim hyper-rustls features

Keep direct hyper-rustls features aligned with the actual RustFS call sites. rustfs-targets only needs native root loading and the rustls provider/TLS policy features for MQTT TLS config construction, while rustfs-ecstore needs the HTTP connector protocol features but not webpki roots.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 06:55:52 +00:00
Zhengchao An 7b3d9e0c04 ci: install C build tools before compiling s3s-e2e on custom runners (#4894) 2026-07-16 06:31:28 +00:00
Henry Guo 73f603b625 fix(table-catalog): return absolute Iceberg metadata paths (#4893)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-16 14:27:43 +08:00
houseme 3a4937367a chore(deps): remove redundant obs tracing feature (#4892)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 14:22:06 +08:00
houseme 513e5f1018 test(ilm): scope restore e2e object under the rule's test/ prefix (#4868)
`restore_object_usecase_reports_ongoing_conflict_and_completion` failed
deterministically in the "ILM Integration (serial)" lane, panicking at
its setup step ("object should transition before the restore API runs"):
the object never reached transition status `complete` within the 15s
wait, before the restore logic under test even ran.

Root cause: the test object key was `restore/api-object.bin`, but
`set_bucket_lifecycle_transition_with_tier` scopes the transition rule to
`<Filter><Prefix>test/</Prefix>`. An object outside that prefix never
matches the rule, so `enqueue_transition_for_existing_objects` never
enqueues it (confirmed: the mock tier saw zero puts and the object's
transitioned status stayed empty) and `wait_for_transition` times out.
Every other transition test in this file already keys its objects under
`test/`. The break shipped in #4860 because that PR's ILM serial lane was
skipped on the merge run, so the test never actually executed in CI.

Fix: key the object as `test/restore/api-object.bin` so it matches the
rule. The restore API, tier copy-back, and local-GET assertions are
unchanged (they address the object by bucket+key; the remote tier name is
generated internally).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 05:50:52 +00:00
houseme 56179210ab chore(deps): simplify dependency features (#4890)
* chore(deps): remove redundant dependency features

Remove manifest feature entries that are implied by other requested features in the same dependency declaration.

Verified that the resolved Cargo feature graph is unchanged after the cleanup.

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(deps): narrow tokio and reqwest features

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 05:20:43 +00:00
houseme f3a7a4b0da chore(deps): localize workspace dependency features (#4888)
Move workspace-level dependency feature lists into the member crates that consume each dependency while keeping required default-features flags at the workspace root.

Also refresh starshard to 2.2.2 via cargo update and cargo upgrade --exclude ratelimit.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 03:55:27 +00:00
houseme 0fa6dc5946 ci: switch Linux builds to custom runners (#4884)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-16 00:47:31 +00:00
Zhengchao An b3a781bce0 test(replication): pin SSE replication contracts (#4883) 2026-07-16 04:34:18 +08:00
Zhengchao An 49e04bf343 test(replication): fix nightly site peer setup (#4882) 2026-07-16 03:03:09 +08:00
Zhengchao An 299b739f9f fix(replication): stop active-active replay loops (#4878) 2026-07-16 01:59:43 +08:00
Zhengchao An 53270054e2 fix(ecstore): serialize transitioned object restores (#4877) 2026-07-15 16:52:16 +00:00
houseme 978ac13eb5 fix(rustfs): gate mimalloc JSON helpers off Windows (#4875)
Avoid compiling the mimalloc JSON parsing helpers on non-test Windows builds so the platform-specific dead_code warnings disappear while tests keep coverage through #[cfg(test)].

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-15 16:26:08 +00:00
houseme 35c5693486 ci: switch linux builds to ubicloud runners (#4873) 2026-07-15 14:17:10 +00:00
houseme 8f15dd2cef ci: add zip packaging fallback (#4872)
ci: add python zip packaging fallback
2026-07-15 21:34:34 +08:00
houseme 5ef2731a6b ci: tune build workflow runners (#4871)
* chore: extend build timeout and trim dev deps

* ci: switch linux builds to ubicloud runners

* ci: use larger linux build runners
2026-07-15 20:58:37 +08:00
houseme 081c10f073 chore(deps): trim s3select datafusion features (#4869) 2026-07-15 12:17:26 +00:00
houseme 28ee1e5e1a test(heal): tolerate by-design "retry scheduled" deferral in resume e2e (#4867)
`test_heal_resume_across_page_boundary_e2e` panicked whenever the
erasure-set healer deferred a single object version to a later heal
cycle. Under load a per-version `heal_object` can hit a transient error;
`ErasureSetHealer::heal_bucket_with_resume` then persists its
resume/checkpoint state and returns a terminal `Failed { .. "retry
scheduled" }`, expecting a fresh heal run to finish the job. In
production the background scanner is that next run — the e2e had no such
follow-up, so the first task's `Failed` state failed the test. This is a
pre-existing rare flake (the wiped-disk heal is otherwise correct); it is
unrelated to any policy/proptest work that happened to surface it in CI.

Drive the heal to a genuine `Completed` instead: on a `Failed` carrying
the `retry scheduled` marker (retry budget still remaining) re-submit the
idempotent heal (`force_start`), mirroring the production scanner, up to a
small bound. A `Failed` without that marker (e.g. `exhausted retries`) or
any other non-`Completed` terminal state still fails the test, and the
strict per-version data-restoration assertions still run only after a
real `Completed`. `wait_for_task` is refactored onto the shared
`await_terminal_status` poller; its panic-on-failure semantics for the
unversioned-bucket heal are unchanged.

Test-only change; no production heal code is touched.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-15 19:53:35 +08:00
Zhengchao An a7827ed91b test(policy): add property tests for the policy evaluation algebra (#4840)
crates/policy had zero property tests: the wildcard Action/Resource matching
and Deny-first evaluation in Policy::is_allowed — the algebra authorization
safety rests on — were guarded only by example-based tests.

Add crates/policy/tests/policy_eval_proptest.rs with three generated-input
invariants (pure evaluation, no IO or global state, parallel-safe):

- explicit_deny_anywhere_denies: a Deny matching the request denies it no
  matter how many broad Allow statements co-exist or at which index the Deny
  sits; a built-in sanity assertion first proves the Allows alone would permit,
  so the Deny is what flips the decision.
- wildcard_superset_implies_concrete_match: any request allowed by an
  exact-action/exact-resource policy is also allowed after widening to s3:* on
  bucket/* and to * on arn:aws:s3:::*, checked both on the built grant and on
  random probe requests (implication form).
- empty_policy_denies_everything: a statement-less policy and Policy::default()
  deny every non-owner request.

Statements are built from JSON exactly as production policies arrive via
PutPolicy. Generated names avoid wildcard metacharacters so the only wildcards
in play are the ones the properties introduce deliberately. proptest joins
crates/policy dev-deps following the filemeta/ecstore precedent.

Refs: backlog#1151 (sec-9)
2026-07-15 10:48:01 +00:00
houseme 6d528414ee chore(deps): update workspace dependencies (#4865)
Run `cargo update` followed by `cargo upgrade --exclude ratelimit` on the
latest main to pull the newest compatible versions.

Manifest bumps (Cargo.toml):
- redis 1.3.0 -> 1.4.0
- xxhash-rust 0.8.16 -> 0.8.17

Lockfile-only bumps: simd-adler32 0.3.10, syn 2.0.119, and
toml_edit 0.25.13. `ratelimit` is held at 0.10.1 as requested.

Verification: cargo check --workspace --all-targets, cargo clippy
--all-targets -D warnings (plus the rio-v2 feature variant), cargo nextest
run --all --exclude e2e_test (8964 passed), and cargo deny check
advisories/sources/bans/licenses all pass.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-15 10:40:31 +00:00
Zhengchao An 7dc987298c test(ilm): add restore full-chain integration coverage (#4860)
* test(ilm): add restore full-chain integration coverage

* test(ilm): fix restore integration types

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-15 18:18:14 +08:00
Zhengchao An 609ccb06af test(cache): assert visible state after clear (#4864) 2026-07-15 09:45:33 +00:00
Zhengchao An c7926a7956 test(e2e): admin IAM user/policy/service-account CRUD lifecycle (#4855)
First systematic HTTP e2e batch for the admin management surface
(backlog#1154 peri-2): full user -> canned-policy -> attach ->
service-account lifecycle with data-plane effect assertions (the
credential really gains and loses S3 access), plus a non-admin 403
probe per management endpoint targeting a different principal so
deny_only self-access semantics do not mask the gate. Wired into the
e2e-smoke nextest profile (ci-4 mechanism).
2026-07-15 09:37:54 +00:00
Henry Guo 1e7fb8438a fix(table-catalog): accept inherited snapshot manifests (#4833)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-15 09:33:42 +00:00
cxymds aa11ee4341 fix(rio): defer HTTP writes until first use (#4838) 2026-07-15 17:16:52 +08:00
Zhengchao An e9903f7501 test(replication): add MRF failure-recovery e2e trio (target outage, source restart, delete-marker mtime) (#4858)
test(replication): add MRF failure-recovery e2e trio for target outage, source restart, and delete-marker mtime
2026-07-15 17:15:27 +08:00
Zhengchao An cc0c9f6565 test(e2e): add console listener over-the-wire smoke net (#4851)
Pin the console listener's wire contract with a real binary on a random
port (backlog#1154 peri-4): unauthenticated version/license endpoints
answer complete JSON without credential material, the SPA prefix always
dispatches to the console static handler (never the S3 API), the admin
API and S3 root on the console listener stay 403 for anonymous callers,
and a console-disabled listener serves no console surface. Wired into
the e2e-smoke nextest profile (ci-4 mechanism).
2026-07-15 17:02:13 +08:00
Zhengchao An 795c74bdf4 test(ilm): add hermetic RustFS-to-RustFS transition main-path e2e (#4818) 2026-07-15 08:47:22 +00:00
Zhengchao An f78f146c35 test(ecstore): extend crash-point injection to multipart complete and xl.meta update paths (#4853) 2026-07-15 16:10:14 +08:00
Zhengchao An f05a69d51b test(utils): add rustfs-test-utils crate and shared ECStore bootstrap (#4850)
* test(utils): add rustfs-test-utils crate, absorb heal/iam ECStore bootstrap

backlog#1153 infra-1. The ~50-line "build a real temp-disk ECStore"
bootstrap was copy-pasted (and drifting) across the heal and iam
integration tests. This adds crates/test-utils (rustfs-test-utils, a
dev-dependency-only crate) owning that bootstrap and converts the four
copies into thin wrappers:

- TestECStoreEnvBuilder: disk_count (default 4), prefix (uuid-suffixed
  /tmp dir), base_dir (caller-owned dir, e.g. tempfile::TempDir),
  init_bucket_metadata (default true; the iam bootstrap test opts out
  to preserve its historical semantics). TestECStoreEnv exposes
  temp_root/disk_paths/ecstore plus a versioned-bucket helper, and
  init_tracing() replaces the per-file Once blocks.
- All rustfs_ecstore imports stay behind src/ecstore_test_compat.rs,
  the sanctioned test-compat boundary pattern (mirrors
  crates/iam/tests/ecstore_test_compat).
- heal: heal_integration_test / heal_b5_versioned_regression_test /
  heal_b920_subquorum_union_test drop their setup_test_env{,_n} copies
  for heal_env{,_n} wrappers; the tests/storage_api.rs integration
  surface shrinks to what test bodies still touch.
- iam: iam_bootstrap_no_lock_test drops build_local_ecstore; its
  ecstore_test_compat fixture shrinks to SetupType +
  update_erasure_type.

rg 'async fn setup_test_env' crates/heal crates/iam now returns 0.
Scanner's lifecycle tests are deliberately NOT absorbed (gated on
ilm-1; 14 of 15 are #[ignore]d today). Net -230 lines.

* fix(heal): drop tokio::fs import orphaned by the b920 bootstrap move

* fix(heal): drop tokio::fs import orphaned by the b5 bootstrap move
2026-07-15 16:08:30 +08:00
Zhengchao An 602a742a1b test(e2e): TLS certificate hot-reload live-listener regression net (#4861)
Prove the swap-certificates-without-restart promise over real TLS
handshakes (backlog#1154 peri-5): new connections pick up the rotated
certificate within the reload interval, a session opened under the old
certificate survives the swap, and garbage material is fail-safe (the
old certificate keeps serving, the failure leaves a tls_reload_failed
log event, the process stays up). Wired into the e2e-smoke nextest
profile (ci-4 mechanism).
2026-07-15 08:05:06 +00:00
Zhengchao An 242424b0fc ci(perf): fit the baseline cache build to the post-LTO profile and self-heal nightly misses (#4841)
* ci(perf): fit the baseline cache build to the post-LTO profile and self-heal nightly misses

Every build-baseline-cache run on 2026-07-15 died on its 60min ceiling
('exceeded the maximum execution time of 1h0m0s'): #4806 put thin LTO +
codegen-units=1 on [profile.release], pushing a single release build past
60min on sm-standard-2, so the baseline cache never populated. The measured
binary must keep the production profile, so raise the job budget to 100min
instead of weakening the profile.

Also make the A/B job self-heal a same-commit cache miss: when the candidate
commit equals the baseline commit (nightly/dispatch on main) and the restore
misses, build the binary once, reuse it for both phases, and save it back to
the cache under rustfs-baseline-<sha> — previously that miss made the rig
build the identical commit twice, which no job budget fits post-#4806. The
PR-context miss (candidate != baseline, opt-in gate) keeps the double-build
fallback and now documents that it will overrun and alert.

Refs rustfs/backlog#1152 (perf-3 follow-up).

* ci(perf): latest-wins concurrency for the baseline cache build

Consumers only restore the binary for the current origin/main tip, so a
superseded push build's output is dead weight; cancel it instead of stacking
~65min jobs on the shared sm-standard-2 pool when main merges quickly. A
skipped intermediate SHA at most costs one same-commit self-heal in the A/B
job.
2026-07-15 07:58:37 +00:00
houseme 4f3f2afd0d refactor(ecstore): make transition-client base64 standard everywhere (#4857)
refactor(ecstore): make client base64 standard everywhere, drop the split

Self-review of the transition-client fixes: the Content-MD5 fix had only
converted the two transition call sites to a parallel base64_encode_standard,
leaving the same URL-safe, unpadded base64 bug on every other outbound value —
the SigV2/no-length multipart Content-MD5, the x-amz-checksum-* headers on the
parallel and no-length paths, api_remove's multi-delete Content-MD5, and the
checksum encode/decode helpers. Every base64 value this client emits or parses
is S3 wire format, which is standard base64; none is ever URL-safe.

Encode and decode both use base64_simd::STANDARD now, and the parallel
base64_encode_standard is gone. This fixes those seven latent call sites at the
root and removes the duplicate function. The transition path is functionally
unchanged (it already produced standard base64 via the helper), so the
end-to-end byte-for-byte result is preserved.

Also drop a stray Vec::with_capacity(part_size) that was allocated (up to
128 MiB) and immediately overwritten by read_multipart_part's own buffer.

Refs: rustfs/rustfs#4811, rustfs/backlog#1267

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-15 07:56:50 +00:00
Zhengchao An d3669ed637 test(e2e): quarantine deterministically failing webhook redelivery test (#4856) 2026-07-15 15:54:51 +08:00
Zhengchao An f29455b32b ci: add e2e-full merge-gate job running the full single-node e2e suite (#4847)
* ci: add e2e-full merge-gate job (single-node full e2e via nextest)

Adds the [profile.e2e-full] nextest profile and a matching e2e-full job in
ci.yml (push main + merge_group + workflow_dispatch) that runs the
never-automated user-visible e2e suites (KMS, object_lock, multipart_auth,
quota, checksum, encryption, security-boundary, ...) the fast PR e2e-smoke
subset skips.

The filter is the whole e2e_test crate minus the sets other lanes own:
protocols:: (ci-6/ci-7), the 6 RustFSTestClusterEnvironment cluster suites
(ci-7 nightly), replication_extension_test (repl-1's smoke + repl-nightly
lanes), and #[ignore]d tests (ci-13). The 4-disk reliability tests are
serialized, mirroring the ci profile. Reuses the downloaded debug binary and
uploads junit. backlog#1149 ci-5.

* ci: exclude characterized known-failures from e2e-full with tracked issues

First-ever automated run of the full suite (dispatch 29381309848: 341 ran /
32 failed / 460s) surfaced five real product-bug families, each now tracked:
rustfs#4842 (extract 500s, mtime=0 OffsetDateTime), rustfs#4843 (path-limit
vs ignore-errors), rustfs#4844 (anonymous POST SSE-S3 500), rustfs#4845
(object-lock POST + list metadata=true 403), rustfs#4846 (lock quorum
misclassified as timeout under load). Deterministic failures cannot be
retried away, so each family is excluded with its OPEN issue cited and the
fixing PR contract to delete the exclusion — passing negative-path siblings
stay in as regression guards.
2026-07-15 07:46:31 +00:00
Zhengchao An 8eba7bb9be test(filemeta): add version-graph marshal/load roundtrip properties (#4849)
xl.meta roundtrip coverage was one fixed example test plus byte-level no-panic
fuzz properties; nothing generated STRUCTURED version graphs, so a lossy
encoding bug in version headers, ordering, delete markers, or the dual
internal-metadata-key handling would only surface if the fixed example happened
to hit it.

Add crates/filemeta/tests/version_graph_roundtrip_proptest.rs with two
generated-input properties over multi-version graphs (1-7 versions, ~30% delete
markers, deliberately colliding mod_times to exercise tie-break ordering, user
metadata, and the AGENTS.md dual x-rustfs-internal-*/x-minio-internal-* keys
written via the real insert_bytes helper):

- version_graph_marshal_load_roundtrip: marshal -> load preserves version
  count, meta_ver, the exact (id, type) sequence, full headers, fully decoded
  per-version content, delete-marker payload shape, and both internal metadata
  key forms with identical values.
- version_graph_marshal_is_idempotent: marshal(load(marshal(x))) is
  byte-identical to marshal(x), catching encoders that mutate or reorder state
  on the way out.

Complements the existing byte-level no-panic properties: those prove hostile
bytes cannot crash the decoder; these prove honest graphs are encoded
losslessly.

Refs: backlog#1151 (sec-10)
2026-07-15 15:33:06 +08:00
Zhengchao An f41489a187 test(e2e): deflake webhook redelivery target registration (#4848)
Register the webhook target while its endpoint is reachable, then drop
the listener before the PUT: registering against a dead endpoint stalls
behind the reachability probe timeout and flaked the ARN wait on the
rustfs#4821 merge run. Also widen the registration wait to 20s.
2026-07-15 15:27:59 +08:00
houseme 47c5a3ab35 fix(ecstore): deduplicate concurrent ILM transition enqueues (#4839)
A single PUT can enqueue the same object for transition twice — immediately
(enqueue_transition_after_write) and again from the startup/lifecycle
compensation backfill. transition_object's own namespace lock is commented out,
so the two attempts are not serialized as same-object work: the winner
transitions the object and removes its local data, and the loser then reads that
already-removed data via get_object_fileinfo(read_data = true) and logs a
spurious "get_object_with_fileinfo err ... No such file" /
lifecycle_tier_operation_failed. For a large (multipart) object both attempts
can also race the source read, corrupting the transferred copy.

Guard the transition queue with an in-flight set keyed by
(bucket, object, version). queue_transition_task claims the key before sending;
a duplicate enqueue is reported handled without queueing a second task. The
worker releases the claim once the transition finishes, so a later lifecycle
pass can still re-transition the object, and a failed enqueue releases it
immediately. This is the sole path into the transition channel, so every
enqueue is covered.

Surfaced while validating the >128 MiB transition fix end-to-end in Docker
(rustfs/rustfs#4811); tracked as its own defect since it is unrelated to the
checksum/multipart-client bugs.

Tests: same-version enqueues dedupe (direct reserve and via queue_transition_task
with spare capacity), a distinct object still hits the full-queue path, and a
released claim can be re-acquired.

Refs: rustfs/backlog#1268

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-15 15:23:57 +08:00
cxymds ea04c94204 fix(ecstore): skip deferred readers without sources (#4836) 2026-07-15 15:22:54 +08:00
Zhengchao An 6248690bd1 fix(ecstore): read full transitioned multipart objects (#4837)
Co-authored-by: houseme <housemecn@gmail.com>
2026-07-15 15:22:25 +08:00
cxymds 23fa008ed8 fix(heal): retry failed objects within bucket scans (#4834) 2026-07-15 14:32:07 +08:00
houseme be6859be55 fix(ecstore): handle ChecksumNone in >128 MiB ILM transitions (#4831)
* fix(ecstore): treat ChecksumNone as unset so >128 MiB ILM transitions succeed

ILM transition of any object larger than 128 MiB to a RustFS-native tier
(rustfs/minio/aliyun/tencent/r2/azure/huaweicloud/s3 backends that use the
built-in TransitionClient) failed with "unsupported checksum type", while
objects <=128 MiB transitioned fine.

Root cause: `ChecksumMode::is_set()` reported `ChecksumNone` as a configured
checksum. `ChecksumNone` is the zeroth enum variant, so it occupies bit 0 of
the EnumSet repr and the `len() == 1` check treated "no checksum" as set. The
128 MiB boundary is the warm backend's `MIN_PART_SIZE`, which selects a single
PUT (<=128 MiB) versus a multipart PUT (>128 MiB). On the multipart path,
`put_object_multipart_stream_optional_checksum` saw `checksum.is_set() == true`,
disabled the Content-MD5 branch, and called `ChecksumNone.hasher()`, which
returns the "unsupported checksum type" error. The single-PUT path hit the same
misjudgement but never calls `hasher()`, so it silently succeeded (without a
checksum), which is why only >128 MiB objects failed.

Fix:
- `is_set()` returns false for `ChecksumNone` (and the bare `ChecksumFullObject`
  flag, which has no base algorithm). This is the sole callers' intended
  meaning: a concrete algorithm with a real hasher is selected.
- Defense in depth: guard the multipart checksum branch on
  `auto_checksum.is_set()` so an unset mode uploads the part without a per-part
  checksum header instead of hard-failing in `hasher()`.

Only the TransitionClient consumes this `ChecksumMode::is_set()`; the
server-side data path uses the unrelated `rustfs_rio::ChecksumType`.

Tests: is_set()/set_default semantics, hasher parity for every set mode, and a
`build_transition_put_options` invariant (checksum unset + Content-MD5 on).

Refs: rustfs/rustfs#4811, rustfs/backlog#1267

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): read exactly one part per multipart chunk in transition uploads

Second defect behind the >128 MiB ILM transition failure (rustfs/rustfs#4811),
uncovered while verifying the checksum fix.

`put_object_multipart_stream_optional_checksum` read each part with
`read_all()` / `to_vec()`, which drained the entire source into the first part
and left every later part empty. Any multipart upload of a streamed
(`ObjectBody`) source was therefore malformed. Objects <=128 MiB take the
single-part path and were unaffected; a 128 MiB + 1 byte object splits into a
128 MiB part plus a 1 byte part, so the first part received the whole object and
its declared Content-Length (part_size) did not match the body.

Verified empirically: `optimal_part_info(128 MiB + 1, 128 MiB)` yields 2 parts,
and `GetObjectReader::read_all()` on part 1 returns the full 134217729 bytes,
leaving 0 for part 2.

Fix:
- Add `read_multipart_part`, which reads exactly the requested part size (or
  less at EOF) and advances the reader, for both `Body` (in-memory) and
  `ObjectBody` (streamed) sources.
- Upload each part with the bytes actually read (`length`) as its size, and
  account uploaded size by actual bytes, so a short read is detected instead of
  masked.

The concurrent (`put_object_multipart_stream_parallel`) and SigV2
(`put_object_multipart`) paths share the same `read_all()` pattern but are not
exercised by transition; left untouched here and noted for follow-up.

Tests: `read_multipart_part` splits a 250-byte source into [100, 100, 50] for
both streamed and in-memory bodies, consumes the source fully, and stops at EOF
without overrun.

Refs: rustfs/rustfs#4811, rustfs/backlog#1267

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): complete the >128 MiB ILM transition multipart client

Docker end-to-end reproduction of rustfs/rustfs#4811 (two RustFS tiers, a
128 MiB + 1 byte object, zero-day transition) surfaced four more defects on the
multipart transition path, each masked by the previous one. With the checksum
and part-splitting fixes in place the transition now failed later and later,
and finally produced a 0-byte object with no error at all. Fixed together:

- initiate_multipart_upload discarded the CreateMultipartUpload response and
  returned an empty UploadId, so the first UploadPart failed with "UploadID
  cannot be empty". Parse the response XML (InitiateMultipartUploadResult now
  derives Deserialize with PascalCase).
- Content-MD5 / x-amz-checksum-* were encoded with URL-safe, unpadded base64,
  which the remote rejected as "Invalid content MD5: Base64Error". Add
  base64_encode_standard and use it for those outbound header values.
- PutObjectOptions::default() set legalhold to OFF, so header() attached
  x-amz-object-lock-legal-hold to every request and CompleteMultipartUpload was
  rejected with "does not accept object lock or governance bypass headers".
  Default to an empty (unset) status.
- CompleteMultipartUpload / CompletePart had no serde renames, so the request
  body used Rust field names (<parts>/<part_num>/<etag>). The remote parsed
  zero <Part> elements and completed a 0-byte object while returning 200. Emit
  S3 element names (<Part>/<PartNumber>/<ETag>) and skip empty checksum fields.

Verified end-to-end: a 128 MiB + 1 byte object now transitions to the remote
tier and reads back (transparently restored) byte-for-byte identical
(sha256 match), with none of the four prior errors in the logs.

Refs: rustfs/rustfs#4811, rustfs/backlog#1267

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-15 06:31:37 +00:00
Zhengchao An 52ef30f167 test(e2e): add S3 event-notification webhook delivery regression net (#4821)
Cover the previously untested configure-target -> put-notification-config
-> object operation -> webhook delivery chain (backlog#1154 peri-1):
PUT/multipart-complete/DELETE event fields, prefix/suffix filter
negatives, and store-queue redelivery after target recovery. Wire both
tests into the e2e-smoke nextest profile (ci-4 mechanism).
2026-07-15 05:01:20 +00:00
Zhengchao An 776f7ee83f test(security): add bucket-policy x IAM priority conflict matrix (#4825)
The individual policy layers were tested in isolation, but the cross-layer
resolution — which layer wins when IAM and bucket policy disagree — had no
coverage. A priority error there is a data-exposure or data-lockout bug.

Add crates/policy/tests/bucket_iam_authz_matrix.rs, a pure table-driven test
(no globals, no IAM store, no server) that drives the real Policy::is_allowed
and BucketPolicy::is_allowed through a helper modeling the request-layer
orchestration in rustfs::storage::access::authorize_request (bucket explicit-Deny
gate -> IAM allow -> bucket allow fallback; anonymous = bucket policy alone). It
pins all four Allow/Deny quadrants plus the anonymous case, and adds intra-policy
Deny-beats-Allow checks for both layers.

The matrix surfaces the resolution invariant explicitly: a BUCKET explicit Deny
is a hard gate and always wins, but an IAM explicit Deny is SOFT — a bucket Allow
fallback overrides it, which diverges from AWS "an explicit Deny in any policy
always wins". The test characterizes the current (MinIO-lineage) behavior; if IAM
Deny is later hardened into a gate, iam_deny_x_bucket_allow flips to false and
must be updated deliberately.

Refs: backlog#1151 (sec-8)

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-15 04:06:26 +00:00
Zhengchao An 6ea6832aef test(lifecycle): add property-based coverage for rule evaluation (#4824)
Co-authored-by: houseme <housemecn@gmail.com>
2026-07-15 03:52:17 +00:00
Zhengchao An 468dcaef69 test(security): pin GHSA-m77q STS root-secret token signing (#4823)
test(security): pin GHSA-m77q STS root-secret token signing (sec-7)

GHSA-m77q-r63m-pj89 (intentionally UNFIXED) is that STS session tokens are
signed with the shared root secret: crates/iam/src/root_credentials.rs
token_signing_key() returns the root secret_key, so anyone holding the root
secret can forge STS session tokens. No test named the advisory, and the
existing test_created_sts_credentials_authorize_with_session_token_claims uses
token_signing_key() for both signing and verifying, so it pins "same key signs
and verifies" but not the m77q-specific "signing key IS the root secret" — a
future fix that decouples the STS key from the root secret would pass it
silently.

Add a flow-level pin, test_ghsa_m77q_sts_session_token_signed_with_root_secret,
that captures the advisory's exact signature: (1) token_signing_key() == the
root secret; (2) an AssumeRole-style session token issued with that key decodes
with the root secret and NOT with any other secret; (3) it authorizes through
the STS path. All three assert CURRENT (by-design-vulnerable) behavior, so a
real m77q fix (a dedicated STS signing key) turns them red and forces a
red -> green regression update.

Also GHSA-name the existing characterization test and token_signing_key() with
doc comments and the advisory URL, and update the m77q row in
docs/testing/security-regressions.md. No production behavior changes.

Refs: backlog#1151 (sec-7)

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-15 03:43:56 +00:00
Zhengchao An eb392f24d6 chore(scripts): add scripts index and archive one-shot scripts (#4822)
chore(scripts): index scripts/ and archive 29 one-shot scripts

backlog#1153 infra-13. scripts/ had 80+ unlabelled top-level entries
mixing CI gates with finished one-shot issue-validation scripts.

- scripts/README.md — one index row per entry with status
  (ci-gate / dev-tool / archived), purpose, and wiring; subdirectories
  get one row each. run_scanner_benchmarks.sh is annotated
  "disposition owned by backlog perf-10" and deliberately untouched.
- git mv 29 confirmed-stale one-shot entries to scripts/archive/:
  11 issue-scoped validation/perf-capture scripts, the 5-script
  backlog#706 large-PUT breakdown family, the 4-file GET-optimization
  stress suite, 2 gt1g one-shots, and 7 other orphaned one-shots.
  Evidence: a whole-tree boundary-aware reference census showed zero
  references from CI/Makefiles/docs/code for every moved entry (or
  references only from other scripts inside the same archived set);
  re-run after the move shows zero dangling references.
- docs/testing/README.md links the index.

Moves only — no script content changed.
2026-07-15 03:43:26 +00:00
Zhengchao An 5294f36669 perf(rustfs): remove the fake s3_operations benchmark (#4817)
bench(rustfs): remove the fake s3_operations benchmark

rustfs/benches/s3_operations.rs never measured S3: all three groups
(put_object/get_object/list_objects) only black_box(data.len())/black_box(count)
with a 'In a real benchmark, this would call the actual S3 client' comment. It
gave a false 'S3 operations have benchmark coverage' signal and was dead weight
on rustfs/Cargo.toml.

Delete the file and its [[bench]] entry. S3-face performance is covered solely
by the warp A/B gate (performance-ab.yml); the runbook scope note now says so
explicitly. Micro-benchmarks stay at the function level (perf-8).

Refs rustfs/backlog#1152 (perf-9).
2026-07-15 11:10:48 +08:00
Zhengchao An 57a9fc6ac8 chore(scripts): retire the broken run_scanner_benchmarks.sh (#4819)
The script cannot run on any machine: WORKSPACE_ROOT is hardcoded to a personal
path (/home/dandan/code/rust/rustfs) and it targets the rustfs-ahm crate, which
no longer exists in the workspace (scanner is crates/scanner, heal is
crates/heal, neither ships benches/). It gave a false impression that scanner
performance automation existed.

There is no current scanner-bench requirement, so retire it (option a). Nothing
references the script anywhere else in the repo.

Refs rustfs/backlog#1152 (perf-10).
2026-07-15 11:10:31 +08:00
Zhengchao An 5fd2e8b6a1 ci(coverage): add weekly cargo-llvm-cov baseline workflow (#4820)
ci(coverage): weekly cargo-llvm-cov workspace baseline (non-blocking)

Add the weekly line-coverage report (backlog#1153 infra-5):

- .github/workflows/coverage.yml — Sunday 07:00 UTC + workflow_dispatch;
  runs `cargo llvm-cov nextest --workspace --exclude e2e_test` under
  NEXTEST_PROFILE=ci (same scope and profile as the ci.yml test gate),
  writes a per-crate line-coverage table to the job summary, uploads
  lcov + JSON as a 90-day artifact, and routes scheduled failures
  through the shared schedule-failure-issue action (ci-8). Runs only on
  schedule/dispatch, so it can never become a required PR check.
- scripts/coverage_per_crate.py — stdlib-only aggregation of the
  llvm-cov JSON export into the per-crate markdown table (worst-first,
  TOTAL row), shared by the workflow and the local target.
- make coverage (.config/make/coverage.mak) — local equivalent with the
  same command sequence; fails with install hints when cargo-llvm-cov
  or cargo-nextest are missing. Listed in make help.
- docs/testing/README.md — Coverage section: cadence, where the table
  and artifacts live, the trend-comparison method, and what is not
  measured (doctests, e2e_test).
2026-07-15 11:10:22 +08:00
houseme 04616e32c8 ci: route build jobs through matrix runner labels (#4830) 2026-07-15 11:03:44 +08:00
Zhengchao An d73c8a783a ci(perf): cache the main baseline binary to cut the nightly double build (#4816)
Every push to main now builds the release binary once and stores it in the
actions cache as rustfs-baseline-<sha> (build-baseline-cache job). The A/B job
restores it for origin/main and passes --baseline-bin; on the nightly, where
the candidate commit equals the baseline, one cached binary serves both phases
with --skip-build and the run does zero source builds. A cache miss falls back
to the source double-build via --baseline-ref origin/main.

This removes the ~32min-per-side double build that pushed the 24-cell nightly
past its 120min ceiling (2026-07-11..07-14 all cancelled on timeout).

Also:
- alert-on-failure now fires on cancelled/timed-out, not just failure, so a
  timed-out nightly is no longer silent (the composite action already reports
  cancelled jobs); removed the stale perf-2 TODO now that the alert job exists.
- run_hotpath_warp_ab.sh appends a Provenance section to gate.md (baseline and
  candidate SHAs + binary source, runner, warp version, matrix params) via a
  repeatable --provenance-note; the gate exit code is preserved.

Refs rustfs/backlog#1152 (perf-3).
2026-07-15 09:33:56 +08:00
Zhengchao An 0e7b8ea16b test(security): wire negative-auth suites into e2e-smoke with a count-floor guard (#4815)
The header-SigV4 (sec-1), presigned-URL (sec-2), and admin-gate (sec-4) negative
auth-rejection e2e suites merged earlier but only presigned_negative was actually
selected by any CI profile; negative_sigv4_test and admin_auth_test compiled and
sat unrun. Add both to the e2e-smoke default-filter so all three attacker-facing
S3 auth-rejection suites execute on every PR. They already meet the smoke
admission criteria (RustFSTestEnvironment, random ports, parallel-safe, no
#[ignore], no feature gates), so this is a pure filterset change — the single
e2e-in-CI wiring mechanism (backlog#1149 ci-4), not a new job.

Because the filter selects by module name, a rename or deletion could silently
drop a suite out of the security gate with no CI signal. Add
scripts/check_security_smoke_count.sh (infra-12 count-floor mechanism): it lists
what the e2e-smoke profile selects and fails if the count of security
auth-rejection tests drops below the committed floor in
.config/security-smoke-floor.txt (16). Invoked from the e2e-tests job, before the
smoke run, so the nextest list compiles the binaries the run reuses.

The GHSA-3p3x FTPS/WebDAV constant-time e2e (protocols::test_protocol_core_suite)
stays out by topology: it binds fixed ports, needs the ftps,webdav features, and
is #[serial], so it cannot join the random-port default-feature smoke profile as
a filterset change (global ruling G5). Its GHSA-r5qv sibling is a unit test that
already runs in the default CI pass. docs/testing/security-regressions.md now
carries the full CI-execution map and flags the GHSA-3p3x e2e CI-lane gap as a
ci-domain follow-up.

Refs: backlog#1151 (sec-5)
2026-07-15 09:33:30 +08:00
Henry Guo a6a0e29282 fix(table-catalog): support Spark REST commits (#4788)
* fix(table-catalog): support Spark REST commits

* chore(deps): update s3s SigV4 revision

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-15 09:32:40 +08:00
Zhengchao An c111d25a6c ci(mint): set RUSTFS_UNSAFE_BYPASS_DISK_CHECK so the mint container boots (#4814)
ci(mint): bypass local physical-disk-independence guard so mint boots

The mint container maps four RUSTFS_VOLUMES data dirs onto a single
runner device, so the startup physical-disk-independence guard aborts
with a FATAL before mint can run. The scheduled run 29183544431
(2026-07-12) crashed at 'Wait for RustFS ready' with 'local erasure
endpoints must use distinct physical disks'.

Set RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true on the container, the CI use
the guard explicitly sanctions -- mirroring the e2e-s3tests harness
fixed in #4768. Completes the ci-2 acceptance (a mint run that actually
produces log.json and the per-suite summary).
2026-07-15 09:32:15 +08:00
Zhengchao An cd51d66321 docs(testing): populate the testing pyramid overview (backlog#1153 infra-11) (#4813)
docs(testing): populate testing pyramid, naming, serial/nextest rules

Fill the docs/testing/README.md skeleton (backlog#1153 infra-11):

- Test taxonomy table for all eight layers (unit / ecstore black-box /
  e2e / s3s-e2e / S3 compatibility / chaos / fuzz / bench) with a
  verified entry command and a qualitative "when it runs" per layer; the
  event x timeout x required-status matrix stays owned by
  docs/testing/ci-gates.md (ci-15), linked not duplicated.
- Naming conventions section with the migration-gate reserved substrings
  (data_movement / rebalance / decommission / source_cleanup /
  delete_marker) linking the infra-12 count-floor guard, closing that
  task's docs cross-link.
- Serial execution & nextest rules: nextest as a hard dependency with the
  RUSTFS_ALLOW_CARGO_TEST_FALLBACK escape hatch and the runner-semantics
  difference (folds the infra-14 README half), why #[serial] is a no-op
  under nextest, and the default/ci/e2e-smoke/e2e-repl-nightly profiles.
- A time-control placeholder for infra-4 to fill.

The pre-existing flake-policy section (ci-10) is preserved verbatim.
Add pointers from CLAUDE.md and CONTRIBUTING.md.
2026-07-15 09:07:34 +08:00
houseme 83fe12d6aa chore(release): prepare 1.0.0-beta.9 (#4807)
* chore(release): prepare 1.0.0-beta.9

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(release): align release assets for 1.0.0-beta.9

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-15 09:03:21 +08:00
houseme ea2e24ac13 test/ci(ecstore): fix MinIO SSE interop size assertion + nightly dockerized interop check (#4809)
* test(ecstore): assert decrypted_size for MinIO SSE interop round-trip

The ignored MinIO interop round-trip tests asserted `ObjectInfo.size`
against the plaintext length. For SSE objects `size` is the on-disk
DARE-encrypted size (plaintext + 32 bytes per 64 KiB block), so the
assertion can never hold once real fixtures are present — the two
`#[ignore]` tests failed the moment a real MinIO-written fixture was fed
in, even though the decoded data was byte-identical.

The client-visible object size comes from `decrypted_size()` /
`get_actual_size()`, which correctly reads MinIO's
`x-*-internal-actual-size` metadata (verified: both SSE-S3 and SSE-KMS
8 MiB multipart fixtures now report 8388608). Assert against that
instead and keep the plaintext length and SHA-256 data checks.

With real 4-drive MinIO fixtures (RELEASE.2025-09-07) all four tests
pass, confirming RustFS reads MinIO erasure-coded SSE objects with
byte-identical data and correct logical size.

Co-Authored-By: heihutu <heihutu@gmail.com>

* ci(ecstore): nightly MinIO interop check + dockerized fixture capture

Wire the ignored MinIO on-disk interop reader tests into a nightly,
non-required CI job, and make their fixtures reproducible without a host
MinIO install.

- Dockerfile + capture_via_docker.sh: build a throwaway image carrying
  the official MinIO server binary (pinned RELEASE.2025-09-07) plus the
  fixture lab on a small Python base, then run `lab.py capture-matrix` to
  write the SSE-S3 / SSE-KMS multipart fixtures the tests consume. lab.py
  drives MinIO's S3 API directly, so no `mc` is needed.
- .github/workflows/minio-interop.yml: nightly + manual workflow on
  GitHub-hosted ubuntu-latest (reliable Docker + Python, unlike the
  self-hosted fleet — see e2e-s3tests.yml infra note). Regenerates the
  gitignored fixtures each run and executes the #[ignore] reader tests.
  Not a PR gate.
- README: document the Docker capture path.

Validated end to end: the script builds the image, captures the two
multipart cases, and `cargo nextest run --run-ignored ignored-only`
passes all four interop tests.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-14 15:08:14 +00:00
houseme c53e34f13b test(replication): lock outbound checksum consistency for XXHash/SHA-512/MD5 (#4808)
test(replication): lock outbound checksum consistency for new algorithms (T3)

Adds a consistency test at the replication put-options boundary confirming that
the AWS 2026-04 additional checksum algorithms (XXHash3/64/128, SHA-512, MD5) are
forwarded into replication user_metadata identically to the classic five. The
outbound replication path routes a stored object checksum through the
algorithm-agnostic decrypt_checksums -> user_metadata flow, so the new algorithms
(already covered by rustfs-rio read_checksums) need no new-algorithm-specific
handling. This locks that behavior against regressions.

Investigation summary (no code change needed on the outbound side): the
per-algorithm ChecksumMode selection path is dormant (opts.checksum is never set
to a specific algorithm; tiering uses Content-MD5; the add_crc bool is dead
code), so extending ChecksumMode was unnecessary.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-14 12:58:29 +00:00
houseme 750e5d15eb feat(checksums): add native S3 additional checksum support (#4805)
* feat(rio): wire XXHash3/64/128 and SHA-512 into ChecksumType (S2)

Add the AWS 2026-04 additional checksum algorithms as base types in
rustfs-rio's ChecksumType, covering every dispatch site (key, raw_byte_len,
hasher, Display, from_string_with_obj_type, BASE_CHECKSUM_TYPES) so no path
silently strips them. Derive BASE_TYPE_MASK from BASE_CHECKSUM_TYPES as the
single source of truth, allocate the new base-type bits append-only above
bit 9 to preserve the on-disk varint format, and add streaming hashers whose
digest uses the S3 canonical big-endian encoding (seed 0).

The new algorithms are COMPOSITE-only: an explicit FULL_OBJECT request is
rejected and they are never routed through add_part()/can_merge(). A
round-trip guardrail test asserts every base type survives all dispatch
sites, failing loudly if a future algorithm is added but a match arm or the
mask is forgotten.

Refs rustfs/backlog#1254 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* test(rio): pin XXHash/SHA-512 digests to official vectors, big-endian (S3)

Lock the byte order and seed of the new algorithms against the OFFICIAL
upstream xxHash / SHA-512 empty-input test vectors (XXH3-64, XXH64, XXH3-128,
SHA-512), in big-endian, so the stored and echoed checksum is byte-for-byte
identical to what AWS SDKs (awscrt) compute — the interop correctness this
feature hinges on. Add a non-empty regression lock (official "fox" vectors)
that also asserts the encoded field is the standard-base64 of the raw digest.

Refs rustfs/backlog#1255 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* test(rio): lock on-disk checksum round-trip and forward-compat degrade (S8)

Cover the xl.meta varint (de)serialization for the new algorithms:
to_bytes() -> read_checksums() must recover the value under the Display key
for XXHASH3/64/128 and SHA512. Pin the rolling-upgrade contract that a node
reading a future, unknown base-type bit degrades safely — skips the entry and
returns without panicking or mis-decoding a length. Combined with the
append-only bit allocation from S2, this protects mixed-version clusters.

Refs rustfs/backlog#1260 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(head): echo XXHash/SHA-512 additional checksums on HeadObject (S5)

HeadObject with x-amz-checksum-mode: ENABLED now returns the XXHash3/64/128
and SHA-512 checksums that S3 stored, closing the head_object gap in #4800.
s3s HeadObjectOutput has no typed field for these, so they are emitted as raw
response headers via response.headers (the same mechanism RustFS already uses
for tagging-count), keyed by ChecksumType::key(). The existing five typed
algorithms are unchanged. Also carries the Cargo.lock update for the
xxhash-rust dependency introduced in S2.

Refs rustfs/backlog#1257 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(checksums): fail-closed on unknown checksum algorithm (S7)

A. Harden unknown/unsupported checksum algorithms to fail closed instead of
   panicking. ChecksumMode::base() in the outbound S3 client
   (crates/ecstore/src/client/checksum.rs) previously did
   `panic!("enum err.")` for any mode without a concrete base algorithm (e.g.
   a bare ChecksumFullObject flag); it now falls back to ChecksumNone. Added
   unit tests proving base() never panics and hasher() returns Err for
   unsupported modes. rustfs-checksums FromStr already returns Err on unknown
   names; added a regression test asserting garbage/unknown names fail closed.

B. Extend rustfs-checksums ChecksumAlgorithm with the AWS 2026-04 additional
   algorithms Sha512/Xxhash3/Xxhash64/Xxhash128. Updated FromStr, as_str,
   into_impl, name constants, the x-amz-checksum-* header constants and the
   HttpChecksum impls. Byte order/seed matches the server-side rustfs-rio
   spec: xxh3/xxh64 as u64 big-endian (8 bytes, seed 0), xxh128 as u128
   big-endian (16 bytes), sha512 via sha2::Sha512. Added tests validating each
   digest against a direct library computation. MD5 stays intentionally
   rejected (PR #4513) and is left untouched.

C. crates/ecstore/src/client/checksum.rs ChecksumMode is enumset repr="u8"
   with 7 variants already consuming 7 bits; adding the 4 new algorithms would
   overflow u8 and require a breaking repr change, so ChecksumMode is left
   unchanged. The new algorithms are available through the rustfs-checksums
   ChecksumAlgorithm path.

Refs rustfs/backlog#1259 rustfs/backlog#1252

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(get,put): echo XXHash/SHA-512 checksums on GetObject and PutObject (S5-GET, S4)

Complete the additional-checksum round-trip so AWS SDKs can verify integrity on
download and confirm it on upload:

- GetObject with x-amz-checksum-mode: ENABLED now returns XXHash3/64/128 and
  SHA-512 checksums (the download-side path SDKs auto-verify). The values flow
  from build_get_object_checksums through GetObjectOutputContext into
  finalize_get_object_response and are emitted after wrap_response_with_cors.
- PutObject echoes the server-computed additional checksum on its response,
  captured at the want_checksum set points before opts is moved.

Both reuse a single centralized helper, inject_additional_checksum_headers,
which HeadObject now also uses. This is the ONLY place that emits these headers,
so when s3s gains typed fields for these algorithms the migration is one spot
(fill the typed field, drop the insert) with no risk of duplicate headers.

The five s3s-typed algorithms are unchanged. Trailing-checksum PUT echo (value
lands after the body) is left for e2e coverage in S10.

Refs rustfs/backlog#1257 rustfs/backlog#1256 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(multipart): support XXHash/SHA-512 composite multipart checksums (S9)

Make multipart uploads work end-to-end for the composite-only algorithms
(XXHash3/64/128, SHA-512):

- complete_part_checksum previously returned the outer None for any algorithm
  outside the five typed ones, which failed CompleteMultipartUpload with
  InvalidPart. It now accepts any valid base type with no double-check value
  (Some(None)) — mirroring the missing-value path of the typed algorithms —
  since s3s CompletePart has no field to carry a client-supplied per-part
  value and the part was already verified server-side at UploadPart. Genuinely
  unset/invalid types are still rejected.
- The existing COMPOSITE assembly (Checksum::new_from_data over the
  concatenated per-part raw digests; full_object_requested() is false so
  add_part() is correctly bypassed) already works for these algorithms via the
  S2 wiring. A rio test locks the assembly and that add_part refuses them.
- UploadPart and CompleteMultipartUpload echo the new-algorithm checksum on
  their responses via the shared inject_additional_checksum_headers helper
  (now pub(crate)), since s3s has no typed output field.

Refs rustfs/backlog#1261 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(rio): add MD5 as an additional checksum (x-amz-checksum-md5) (S6)

Wire MD5 into ChecksumType as an additional (flexible) checksum, distinct from
the legacy Content-MD5 / ETag path: header x-amz-checksum-md5, 16-byte digest,
COMPOSITE-only, md-5 hasher. Pinned to the official empty-input MD5 vector.

Thanks to the single-source-of-truth wiring from S2, every dispatch site
(GetObject/HeadObject/PutObject echo, multipart complete_part_checksum and the
COMPOSITE assembly) picks MD5 up automatically via base()/key()/the catch-all
arm — no handler changes needed. Tests are extended to cover MD5 across them.

Coordination with #4513: that PR made the OUTBOUND rustfs-checksums client
reject "md5" so it could never silently fall back to CRC32. This change is on
the server-side rio path and never falls back — it implements MD5 correctly
rather than substituting another algorithm — so the #4513 intent is preserved,
and the outbound client keeps rejecting md5 (S7).

Refs rustfs/backlog#1258 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(rio): drop per-request to_uppercase alloc in checksum parsing (S11)

from_string_with_obj_type ran alg.to_uppercase() on every checksummed request,
allocating a String just to compare against a fixed set of algorithm names.
Replace it with eq_ignore_ascii_case, which is allocation-free and, for the
ASCII algorithm names involved, exactly equivalent. A test locks that
case-insensitivity, the CRC64NVME full-object assumption, composite-only
FULL_OBJECT rejection, and unknown/empty handling are all unchanged.

The other S11 notes are intentionally not acted on: the Phase-0 header scan is
N/A (we chose full support over rejection, so there is no reject guard), and
parallelizing the serialized hash passes is deferred pending a measured need.

Refs rustfs/backlog#1263 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor(checksums): collapse 5 duplicated response-checksum loops into one

Review of the accumulated commits found the same "iterate decrypted checksums,
match five typed algorithms, drop the rest" loop copy-pasted across five
response paths (GetObject, HeadObject, GetObjectAttributes object-level and
part-level, CompleteMultipartUpload). That was patch-on-patch duplication.

Collapse it into a single source of truth:
- rustfs-rio gains ChecksumType::is_s3s_typed() — the one place that defines the
  five-typed vs additional-algorithm split.
- object_usecase gains ResponseChecksums + classify_response_checksums(), which
  performs the typed/extra split once. All five call sites now destructure its
  result; additional_checksum_echo_pairs() also uses is_s3s_typed() instead of a
  hand-rolled five-way comparison.

Behaviour is unchanged (GetObjectAttributes still cannot surface the additional
algorithms — an s3s XML-body limitation, now documented in one spot). One pass
over the map; extra pairs pushed only when a new-algorithm checksum is present.

Refs rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* test(checksums): unit tests for classifier/echo helpers + fix unused import

Add direct unit tests for the refactored single-source-of-truth helpers:
- rio ChecksumType::is_s3s_typed() — exhaustive typed-vs-additional split, and
  that flags (FULL_OBJECT/MULTIPART) on a base type don't change classification.
- object_usecase classify_response_checksums() — typed fields vs `extra` headers,
  the checksum-type marker, and empty input.
- additional_checksum_echo_pairs() — echo pair only for additional algorithms,
  none for the five typed ones, none for None.
- inject_additional_checksum_headers() — writes all pairs; empty is a no-op.

Also drop the now-unused AMZ_CHECKSUM_TYPE import in multipart_usecase.rs left
by the classifier refactor (would fail the -D warnings gate).

Refs rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* style(rio): fix typo flagged by CI (mis-decoding -> decoding a wrong length)

The Typos CI check flagged "mis-decoding" (it reads "mis" as a word). Reword
the S8 forward-compat comment; no code change.

Refs rustfs/backlog#1260
Co-Authored-By: heihutu <heihutu@gmail.com>

* test(e2e): integration test for XXHash/SHA-512/MD5 additional checksums (S10)

Permanent verify-on-write integration test in the e2e suite for the AWS 2026-04
additional algorithms. aws_sdk_s3 has no typed builder for these, so the
x-amz-checksum-<algo> header is injected via mutate_request (value from
rustfs-rio, byte-for-byte identical to awscrt). Uses a client with automatic
checksum calculation disabled (request_checksum_calculation=WhenRequired) so the
injected header is the only checksum on the wire. For each of XXHash3/64/128,
SHA-512 and MD5: a correct value is accepted and the object stored intact; a
mismatched value is rejected with BadDigest and nothing is stored.

Verified passing locally (1 passed) alongside a boto3+awscrt round-trip that
additionally confirms the HEAD/GET header echo (14/14).

Refs rustfs/backlog#1262 rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

* style(get): allow too_many_arguments on finalize_get_object_response

The classifier refactor added an extra_checksum_headers parameter, pushing
finalize_get_object_response to 8 args and tripping clippy::too_many_arguments
under CI's `-D warnings`. Add the same #[allow] the sibling GET helpers already
carry; no behavior change.

Refs rustfs/backlog#1252
Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-14 12:03:59 +00:00
houseme af5ca7c3f9 build: optimize release profile (#4806) 2026-07-14 10:56:15 +00:00
houseme 24e7f8f19c chore(deps): refresh workspace dependencies (#4804) 2026-07-14 08:11:16 +00:00
escapecode a80699b6dd feat: add an opt-in NATS JetStream publish path for the notify and audit targets (#4634)
feat(targets): add an opt-in NATS JetStream publish path for the notify and audit targets

The NATS notify and audit targets publish through NATS Core, which returns
before the server has durably accepted the message. A broker restart or a
connection drop between the publish and the flush loses the event, even though
the send queue has already cleared it, and no acknowledgement gates that clear.

An opt-in JetStream publish path clears a queued event only after the server
returns a durable PublishAck, so delivery is at-least-once across a broker
restart or a reconnect. It applies to both the notify and audit NATS targets, is
off by default, and is byte-identical to the NATS Core path when disabled.

The path includes durable store-and-forward, a stable dedup id sent as the
Nats-Msg-Id header so a replayed event is collapsed by the stream duplicate
window, pre-flight stream validation, and a bounded failed-events store for
terminally-failed and retry-exhausted events. Three configuration keys per
target select it: JETSTREAM_ENABLE, JETSTREAM_STREAM_NAME, and
JETSTREAM_ACK_TIMEOUT_SECS, under the RUSTFS_NOTIFY_NATS_ and RUSTFS_AUDIT_NATS_
prefixes.

The on-disk batch filename separator changes from colon to underscore so
batch names are valid on Windows filesystems, with transparent read-back
of files written under the previous separator. The migration affects the
shared queue store for every target type and lands with this feature
because the store gains its first Windows-exercised paths here.

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-14 15:36:14 +08:00
cxymds 25f81f812c feat(site-replication): support custom TLS peers (#4802)
* feat(madmin): add site replication TLS settings

* feat(site-replication): support custom TLS peers

* test(site-replication): remove redundant clones

* test(site-replication): avoid needless resolver collection
2026-07-14 15:33:00 +08:00
Zhengchao An e9a0200a72 fix(ecstore): hedge slow shard reads in lockstep GET to cut the large-object first-byte tail (#4799) 2026-07-14 11:13:03 +08:00
houseme 27a7cc739e fix(targets): keep pulsar target online after restart without TLS (#4798)
The pulsar config validation rejected any non-`pulsar+ssl` broker whenever
`tls_allow_insecure` was set or `tls_hostname_verification` was disabled.
Both toggles are inert on a plaintext `pulsar://` broker — the Pulsar client
only applies them for `pulsar+ssl` — so treating a non-default value as fatal
is wrong.

This surfaced as issue #4796: the console persists `tls_hostname_verification`
as `false` (the checkbox defaults to unchecked while the server default is
`on`). At creation time the value is not present in the unmerged request and
falls back to the safe default, so the connectivity check passes and the
target comes online. On restart the persisted config is merged and validated,
the `false` is read back, and the target is rejected — permanently offline
even though Pulsar is reachable.

Relax both the loader-side (`validate_pulsar_broker_config`) and runtime-side
(`PulsarArgs::validate`) checks so only a `tls_ca` bundle — real TLS trust
material that is never defaulted to a non-empty value — requires a
`pulsar+ssl` scheme. The inert toggles no longer fail the target. This also
heals configs already persisted with the bad value. Add regression coverage
for both paths.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-13 16:07:08 +00:00
Zhengchao An 0f83a27f6a fix(deps): pin hyper to flush-before-shutdown fix for large-GET unexpected EOF (#4797)
* fix(deps): pin hyper to flush-before-shutdown fix for large-GET unexpected EOF

hyper <= 1.10.1 can call poll_shutdown() on an HTTP/1 socket while response
bytes are still buffered (a prior poll_flush() returned Poll::Pending and the
result was discarded), so a backpressured/slow-reading peer receives a graceful
FIN before the full Content-Length body is flushed. Standard S3 clients
(minio-go/warp) report this as sporadic `unexpected EOF` on large-object GET
under load (rustfs/backlog#1232). This is the transport-layer bug from
Cloudflare's "hyper-bug" writeup — distinct from the EC-reconstruct-desync EOF
already fixed via lockstep decode; here the app layer delivers the full body and
truncation is purely hyper->socket.

Fixed upstream in hyperium/hyper#4018 (commit 72046cc7, "fix(http1): flush
buffered data before shutdown"), which is not in any crates.io release yet as of
hyper 1.10.1. Pin hyper via [patch.crates-io] to git rev ccc1e850 (a descendant
of the fix that still declares version 1.10.1).

Use [patch.crates-io], not a git+rev on the workspace `hyper` dependency: the
server connection is driven by the transitive hyper-util (conn::auto /
GracefulShutdown), and a git source would not unify with the crates.io hyper
hyper-util resolves, leaving two hyper copies with the server path still on the
buggy one. The patch rewrites the crates.io source globally, so the lockfile
holds a single git-sourced hyper.

Add rustfs/tests/hyper_h1_shutdown_flush_regression.rs, a deterministic guard
(mirrors hyper's own h1_shutdown_while_buffered) that fails if the pin is dropped
or hyper is downgraded below the fix. Its load-bearing assertion is a
synchronously-set flag, so a slow runner can only under-detect, never false-red.

Closes rustfs/backlog#1232.

* build(deny): allow the hyperium/hyper git source for the flush-before-shutdown pin

cargo-deny's [sources] check denies unknown git sources. The hyper
[patch.crates-io] pin added for rustfs/backlog#1232 uses a github.com/hyperium
git source, so add it to allow-git with an owner/review note and a removal
condition (drop once a released hyper > 1.10.1 carries commit 72046cc7).
2026-07-13 15:36:38 +00:00
houseme 0ac7f0d0cf chore: refresh erasure codec and rust toolchains (#4795) 2026-07-13 12:16:25 +00:00
cxymds 7ece747eab fix(ecstore): suppress missing rollback rename warnings (#4792) 2026-07-13 19:05:09 +08:00
cxymds 724d3ea0bc fix(error): map StorageError::NotModified correctly (#4793)
fix(error): map not modified storage errors
2026-07-13 19:04:42 +08:00
Zhengchao An f710f51687 fix(logging): rate limit the GetObject I/O queue congestion WARN (#4790) 2026-07-13 12:41:11 +08:00
Zhengchao An 6096bb189d fix(ecstore): demote reliable_rename NotFound WARN to debug (#4789) 2026-07-13 12:07:49 +08:00
Zhengchao An 535d672b1f fix(admin): report heal runtime state (#4786) 2026-07-13 11:02:13 +08:00
houseme 2e85709634 chore: refresh workspace lockfile (#4785)
* chore: refresh workspace lockfile

* chore: bump uuid to 1.23.5

* chore: bump pollster and path-absolutize
2026-07-13 01:03:01 +00:00
houseme 1553dc3f62 Address P2 follow-ups from the 2026-07-10..12 merged-PR review (backlog#1210-1220) (#4783)
* fix(obs): open cleaner compression source with O_NOFOLLOW

The compressor opened the source log via File::open, which follows a
symlink at the final path component. Between the scanner selecting a
regular file and this open, an attacker with write access to the log
directory could swap the entry for a symlink (TOCTOU) pointing at, say,
/etc/shadow, whose contents would then be copied into an archive. Open
the source with O_NOFOLLOW on Unix so such a swap fails with ELOOP; the
temp/archive path already refused symlinks, this closes the source side.

Refs rustfs/backlog#1210
Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs): recompress instead of trusting leftover cleaner archives

archive_header_ok only checked the first 2-4 magic bytes before treating
an existing .gz/.zst as a completed prior result and letting the caller
delete the source log. A file with valid magic but a truncated or forged
body passes that check, so an attacker with write access to the log
directory (or a crashed prior run) could plant such a stub and make the
cleaner delete the real log without ever producing a usable archive —
silent audit-data loss.

Chosen fix: stop trusting cross-process leftovers entirely and always
recompress the source in this pass, rather than fully decoding every
leftover to validate it. Full-decode validation would add real CPU cost
and decode-bug surface for a rare crash-recovery case; the existing
atomic create_new+rename already overwrites whatever sits at the archive
path (a planted symlink is replaced, never followed) with a freshly
written, fsync'd archive, so a partial/forged leftover can never gate
source deletion. This is the lowest-regression option.

Refs rustfs/backlog#1211
Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): cap memory-gate reservation at cache growth headroom

The memory gate subtracts `admitted_since_refresh` from the snapshot's
available bytes so a burst arriving faster than the 5 s refresh cannot
over-allocate. That counter is GROSS: it only rolls over on the refresh and
never rolls back when a fill is later evicted, cancelled, or loses the
invalidation race. Under sustained high-throughput churn (net footprint flat
and far below `max_capacity`) the raw counter balloons past the memory the
cache actually holds, so `effective_available` collapses and the gate reports
false memory pressure — skipping the hottest fills with SkippedMemoryPressure
until the next 5 s refresh. This only lowers hit rate; it never returns wrong
data and self-heals each refresh.

Fix direction 1 (minimal regression): cap the reservation deduction at the
cache's own growth headroom (`max_capacity - weighted_size()`) instead of
letting the unbounded gross counter shrink the system-available budget. The
cache can never hold more than `max_capacity`, so a burst adds at most that
headroom of real memory before moka evicts to stay bounded (net-zero churn
beyond that point) — capping the deduction there keeps the reservation honest
without treating gross churn as growth. Chosen over net-accounting (direction
2, releasing bytes on every failure/cancel/eviction path) because that only
plugs the leak on failed fills and would not address the core defect: churn of
*successful* insert/evict fills over the 5 s window. It also touches only the
gate plus one call site rather than every failure path in moka_backend.

The cap only ever raises `effective_available`, so real memory pressure (a low
snapshot at refresh) still suppresses fills; when the cache is at capacity the
headroom is 0 and the deduction vanishes, correctly reflecting net-zero churn.
`MokaBackend` now stores `max_capacity` and passes the live headroom into
`allows_fill`. Adds targeted gate tests: gross churn far above headroom no
longer falsely suppresses, yet the reservation still bounds a burst while the
cache can genuinely grow.

Refs rustfs/backlog#1212
Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): assert native O_DIRECT path runs in uring read test

uring_preserves_o_direct_for_eligible_reads only compared bytes through
LocalDisk::read_file_mmap_copy. On a filesystem that rejects O_DIRECT the
read silently degrades to the buffered StdBackend fallback and the byte
check still passes, so the test could go green without the native
read_at_direct path ever executing -- a vacuous pass.

Add a per-disk native_direct_reads counter on UringBackend, incremented
only when pread_uring_direct completes, and rebuild the test to drive a
real UringBackend's pread_bytes and assert the counter is non-zero (every
eligible read went through the native tier). When io_uring or O_DIRECT is
unavailable on the host filesystem (restricted CI runners, tmpfs), the
test skips loudly via eprintln instead of asserting a tautology, while
still checking byte-correctness on whatever tier served the read.

The counter also gives a gray release a positive signal that the O_DIRECT
tier is serving reads, not just a fallback count.

Refs rustfs/backlog#1213
Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): warn + count read-time EINVAL on native O_DIRECT reads

classify_direct_read_error is only reached from the read side: the
O_DIRECT open in pread_uring_direct already succeeded (an open-time
refusal is handled earlier as DirectOpenError::ODirectRefused). So an
EINVAL/EOPNOTSUPP arriving here is a read-time error on an fd the kernel
accepted for O_DIRECT -- far more likely an alignment bug in the aligned
read path than an unsupported filesystem. The old code latched the disk's
native path off with only a once-per-disk debug trace, hiding a potential
correctness regression behind a silent buffered-read downgrade.

Diagnostics only: the fallback behaviour is unchanged (the native path is
still latched off and the caller still reads via StdBackend). This adds a
rustfs_io_uring_direct_read_einval_total counter and promotes the
once-per-disk trace from debug to warn so an operator can see an alignment
regression instead of an unexplained latency/CPU shift.

Refs rustfs/backlog#1214
Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(ecstore): document data-blocks-first default and its tail-latency cost

DEFAULT_RUSTFS_GET_DATA_BLOCKS_FIRST_READER_SETUP is true and must stay
true: deferred-parity is the deliberate, already-rolled-out full-object
GET default from backlog#1159/#923. Flipping it back to false in code
would silently revert that rollout for every deployment that has not set
the env var, so this commit only documents -- no behaviour change.

The added notes explain what data-blocks-first does (schedule data shards
up front, engage parity lazily on a missing/corrupt data shard), the known
trade-off (parity is engaged late, so a slow-but-not-dead data drive
raises GET p99 because the faster parity shards are not raced against it
until a data shard is declared missing), and the operational rollback
switch (RUSTFS_GET_DATA_BLOCKS_FIRST_READER_SETUP=false), which is
intentionally an env override rather than a code default change.

No metric was added: the low-risk observability hook for "slow data drive
engaged deferred parity" would live at the deferred-stripe engage point,
which is out of this file's scope; this change stays documentation-only to
avoid touching the hot GET path.

Refs rustfs/backlog#1215
Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(ecstore): document wide-directory walk stall hazard and tuning

list_dir enumerates a whole directory in one os::read_dir call (count =
-1), and the walk caller bounds that entire enumeration with the per-read
stall budget (default 5s) as if it were a single read. For a wide, flat
prefix -- one directory holding millions of immediate children -- a single
readdir can exceed the budget on a healthy disk, trip DiskError::Timeout,
and surface as a ListObjects 500 quorum failure though the drive is fine
(a #2999 sub-class).

This is documented, not rewritten: turning the one-shot readdir into a
streaming/batched enumeration that refreshes the stall deadline between
chunks is an architecture-level change with high regression surface
(ordering, the count contract, quorum merge) and belongs in a separate
follow-up. The supported mitigation today is operational, so the comments
point wide-directory deployments at RUSTFS_DRIVE_WALKDIR_STALL_TIMEOUT_SECS
and the high-latency drive-timeout profile, which widen the budget with no
code change. Notes were added at list_dir, the scan_dir call site, and
get_drive_walkdir_stall_timeout. No behaviour change.

Refs rustfs/backlog#1216
Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(ecstore): document consumer-peek vs producer-stall coupling

In list_path_raw the consumer's peek_timeout is drawn from the same source
and same value (walkdir_stall_timeout, default 5s) as the producer-side
walk stall budget, but the two measure different things: the producer
stall bounds a single drive read, while the consumer peek bounds the gap
between two ADJACENT entries arriving from a reader. Because they share a
value, the consumer cannot wait meaningfully longer for the next entry
than the producer is allowed to spend producing one. Walking a region
dense with non-listable internal items can make a HEALTHY drive miss the
budget between visible entries; the consumer then declares it stalled and
detaches it, dropping a good drive from the merge and capping the "large
prefix succeeds" guarantee.

Documented, not decoupled: giving the consumer peek an independent,
strictly-larger budget would cut these false detaches but equally delays
detaching a genuinely dead drive and shifts listing tail-latency
semantics, so it wants soak data before changing the default. The comment
records the invariant any such follow-up must keep -- consumer peek >=
producer stall, never stricter -- so it can never fail a drive before the
producer would. No behaviour change.

Refs rustfs/backlog#1217
Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(io-metrics): add time-based trigger for low-IOPS latency percentiles

Percentiles were recomputed only every 128 IOs and seeded to 0, so a
low-traffic deployment exported p95/p99 = 0/stale for a long time after
startup. Add a 10s wall-clock trigger alongside the count throttle so the
first recompute can fire before 128 samples accrue. Hot-path per-op mean
update is unchanged.

Refs rustfs/backlog#1218
Co-Authored-By: heihutu <heihutu@gmail.com>

* test(e2e): cover codec-streaming parity under fault injection and NoSuchKey

The codec-streaming compat A/B previously ran only against a healthy
4-disk EC set with successful full GETs: the DiskFaultHarness was
constructed but never faulted, the error path was untested, and the
range assertion silently compared legacy-vs-legacy (ranges always fall
back to the duplex path), overstating what it proved.

Add two genuinely-failable scenarios reusing the existing harness and
fixtures:

- Parity reconstruction A/B: take one data disk offline and re-run the
  full object matrix on both phases while the EC 2+2 set rebuilds each
  large object from the surviving shards. Assert codec == legacy
  byte-for-byte (sha256) and header-for-header, and assert the codec
  phase served the reconstructed objects with zero duplex-pipe fallback
  (the reader gate is drive-health-independent, so the codec fast path
  is really exercised through reconstruction).
- NoSuchKey negative path: compare the HTTP status + S3 error code of a
  missing-key GET across the legacy and codec phases and require them to
  be identical (404/NoSuchKey), guarding against the codec env
  perturbing the error path.

Also clarify the range-phase comment so it is not misread as
codec-range correctness coverage: both sides are served by the same
legacy range path, so the assertion only proves ranges keep working and
keep falling back to legacy with the gates open.

Verified: cargo check/--no-run pass and the test passes locally
(1 passed; dup_codec=0 confirms the codec path ran).

Refs rustfs/backlog#1219
Co-Authored-By: heihutu <heihutu@gmail.com>

* ci(ecstore): exercise native O_DIRECT read path on an ext4 loopback

The uring-integration leg ran on the runner's default TMPDIR, which may sit
on tmpfs/overlayfs where open(O_DIRECT) fails and the native read_at_direct
path silently latches off to the aligned StdBackend fallback. Mount a
dedicated ext4 loopback and point TMPDIR at it so the real io_uring dep
(bumped git->0.1.0->0.2.0->0.2.1) and the native O_DIRECT read path are
actually covered rather than validated only by signature diffing.

Refs rustfs/backlog#1220
Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-12 16:03:28 +00:00
houseme 63b4568f85 fix(ecstore): reclaim orphan data dirs on the healthy heal path (#4781)
PR #4356 wired `reclaim_orphan_data_dirs` only into `heal_object`'s
post-heal tail, which runs after the `disks_to_heal_count == 0` early
return. That early return is exactly the state of the objects the sweep
targets: a valid `xl.meta` with all shards present plus a leaked
pre-#3510 data dir needs no shard healing, so a healthy heal returned
before reclaim and swept nothing. On a healthy deployment (single node,
no degraded disks) the reclaim was therefore dead code — an admin heal
walked the objects, "healed" them, and reclaimed no leaked space.

Run the best-effort reclaim on the `disks_to_heal_count == 0` path as
well, gated on `!opts.dry_run`. The shared match+log block is factored
into `reclaim_orphan_data_dirs_best_effort` so both exits behave
identically. A reclaim failure still never fails the heal.

Adds an end-to-end regression: put a healthy non-inline object, plant an
unreferenced UUID data dir under it on every disk that holds the object,
then drive `heal_object`. A dry-run heal must leave the stray in place; a
real heal must reclaim it while preserving the live data dirs, `xl.meta`,
and object contents. The test fails against the pre-fix control flow.

Refs #3231, #3191, #4356.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-12 14:19:47 +00:00
Zhengchao An 53c5cbed6e docs: update security advisory lessons (#4780) 2026-07-12 22:19:16 +08:00
Zhengchao An cf142e7fdd fix(ci): widen Days=5 expiration poll window to 8*lc_interval (#4779)
Follow-up to #4772. After forcing every-cycle ILM evaluation the Days=1
plateau is reliable, but test_lifecycle_expiration / test_lifecyclev2_expiration
still flaked intermittently on the *second* assertion (`assert 4 == 2`):
the Days=5 (expire3) objects were not expired within their poll window.

Cause: _wait_for_lifecycle_count for expire3 starts its N*lc_interval
deadline only after the Days=1 poll returns (~1 debug-day in). With N=5
and lc_interval == debug_day, the 5*debug_day terms cancel and the slack
past the Days=5 due time is only ~1 debug-day (~9s) -- which a single slow
scanner cycle (observed ~13s spacing under CI load) can exceed, leaving the
count stalled at 4. Bumping the two expire3 windows to 8*lc_interval raises
the slack to ~39s, comfortably above the observed cycle jitter.

Test-only, lane-scoped: does not touch RUSTFS_ILM_DEBUG_DAY_SECS or the
4*lc_interval < 5*debug_day plateau invariant. The expire3 target count (2)
is terminal (nothing expires after it), so a wider window can never
over-expire. Also documents the #4772 RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES=1
knob in lifecycle_behavior_tests.txt.
2026-07-12 14:15:05 +00:00
Zhengchao An 418f505a81 fix(ecstore): allow concurrent directory scans (#4778) 2026-07-12 13:17:50 +00:00
Zhengchao An f84ba243a1 chore(ci): guard against committed planning docs and remove re-added ones (#4777)
chore(ci): guard against committed planning docs; remove re-added ones

PR #4771 removed the docs/superpowers planning-doc archive and added a rule,
but #4765 force-added two more (git add -f bypasses .gitignore):
docs/superpowers/plans/2026-07-12-observability-single-writer.md and
docs/superpowers/specs/2026-07-12-observability-single-writer-design.md — an
agentic implementation plan and its design spec. Delete both.

Close the enforcement gap so this cannot recur:
- New scripts/check_no_planning_docs.sh fails if anything is tracked under
  docs/superpowers/, regardless of how it was added.
- Wire it into make pre-commit/pre-pr/dev-check.
- Run it in CI: ci.yml for code/mixed PRs, and ci-docs-only.yml (which was a
  green stub) so docs-only PRs can no longer slip a planning doc past the
  required 'Test and Lint' check.
- Document the guard in AGENTS.md and the arch-checks skill.
2026-07-12 20:19:06 +08:00
houseme 3b139e5267 fix(obs/cleaner): harden log cleaner durability, symlink safety, and retention (audit OLC-01..14) (#4776)
* fix(obs/cleaner): fsync archive and log dir before deleting source logs

OLC-01: the compression path flushed the BufWriter but never synced the
archive data or the parent directory before renaming, and the source was
then unlinked with no durability barrier. A crash after rename but before
the page cache reached disk could leave a truncated/zero-length archive
while the source was already gone — permanent log/audit data loss. Because
the archive is always renamed to a brand-new name (guaranteed by the
existing exists() guard), ext4 auto_da_alloc does not mask this.

Hand the underlying File back from the writer closure, sync_all() it before
rename, fsync the parent directory, and fsync the log directory after the
unlinks so a delete cannot be reordered ahead of the archive it justified.
Guard the temp file with an RAII cleanup so an early return or panic cannot
leak a *.tmp orphan.

Ref: rustfs/backlog#1194 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): create compression temp file with O_EXCL and O_NOFOLLOW

OLC-03: the temp archive was opened with File::create at a predictable
`<source>.gz.tmp` path with no O_EXCL/O_NOFOLLOW, so an actor with write
access to the log directory could pre-plant that path as a symlink and have
the compressor follow it — truncating and overwriting an arbitrary external
file, then chmod-ing it to the source log's mode. This mirrors the symlink
refusal already enforced on the deletion path (secure_delete).

Route temp creation through create_tmp_archive(), which uses create_new
(O_CREAT|O_EXCL) to refuse a pre-existing entry and, on Unix, O_NOFOLLOW to
refuse a symlink at the final path component.

Ref: rustfs/backlog#1196 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): create compression temp file with restrictive mode

OLC-06: File::create left the temp archive world-readable (0644 & ~umask)
for the entire duration of compressing a large log, exposing the full
plaintext of a possibly-0600 audit log on shared hosts until the mode was
copied only after the write completed. Pass the source mode into
create_tmp_archive and open the temp file with it (default 0600) so it is
restrictive from creation; the post-write chmod still tightens/matches the
source mode exactly.

Ref: rustfs/backlog#1199 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): validate existing archive before skipping recompression

OLC-02: the idempotency guard used Path::exists() (which follows symlinks)
and trusted whatever it found, then the caller deleted the source. A planted
`<archive>.gz` symlink, or a zero-length/truncated archive left by a crashed
run (OLC-01), would green-light deleting the source with no valid backup —
data loss / log destruction.

Replace exists() with symlink_metadata (no follow) and only treat the entry
as a completed prior result when it is a regular, non-empty file whose header
matches the codec magic (gzip 1f 8b / zstd 28 b5 2f fd). Anything else falls
through to recompression, whose atomic create_new+rename replaces the bad
entry (a symlink is replaced, never followed or deleted through).

Ref: rustfs/backlog#1195 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): stop dry-run overstating reclaimed bytes for compression

OLC-08: in dry-run, compress_with_writer returned output_bytes = 0, so
projected_freed_bytes = input and delete_files reported the full input as
freed. A real run keeps the archive on disk (freed = input - archive), so
dry-run overstated reclaim by the whole archive footprint. Estimate the
archive with a deliberately conservative ratio so the projection never
exceeds what a real run reclaims.

Ref: rustfs/backlog#1201 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): make freed-byte accounting resilient; document steal metric

OLC-12: input/output byte sizes were read via metadata().unwrap_or(0), which
silently reports 0 on failure and skews freed-byte metrics (input - 0 = full
input, overstating reclaim). Use the copy() byte count as the authoritative
input size and, when the archive metadata read fails, conservatively assume
no savings instead of 0. Also document that the steal_success_rate counts
only victim steals (batch = one success), so it reads as a relative
rebalancing signal, not absolute task acquisition.

Ref: rustfs/backlog#1205 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): preserve level-0 semantics, allow zstd 22, log effective levels

OLC-09: build() and the codec calls clamped gzip/zstd levels to [1,9]/[1,21],
silently rewriting gzip level 0 (store) and zstd level 0 (codec default) to 1
and blocking the legal zstd maximum of 22. Clamp to [0,9]/[0,22] so those
meanings survive, and echo the effective (post-clamp) levels in the startup
log via new effective_gzip_level()/effective_zstd_level() getters so the log
matches what actually runs.

Ref: rustfs/backlog#1202 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): bound compressed archives by byte cap; warn on retention=0

OLC-04: archive expiry was gated on compressed_file_retention_days > 0, so
retention=0 disabled it entirely while compression kept producing archives,
and max_total_size_bytes only ever bounded uncompressed logs — unbounded disk
growth. Replace select_expired_compressed with select_archives_to_delete,
which applies age expiry (when retention is on) and, regardless of retention,
trims the oldest archives until the set fits under max_total_size_bytes. Also
warn at startup when compression is on with retention=0 so the "keep forever"
semantics are not mistaken for "delete immediately".

Ref: rustfs/backlog#1197 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): warn on invalid exclude glob instead of dropping silently

OLC-05: build() dropped unparseable exclude globs via filter_map(...ok()), so
a typo (or a literal comma splitting a char-class in the config string) turned
"protect this file" into "delete this file" with no signal. Log a warning per
rejected pattern with the raw string and parse error so the misconfiguration
is visible.

Ref: rustfs/backlog#1198 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(obs/cleaner): backoff idle workers, cap worker count, lower small-host floor

OLC-11: the work-stealing loop re-spun on Steal::Retry with no yield and used
yield_now on the empty path, burning CPU during redistribution windows;
worker_count had no upper bound so a mis-set parallel_workers over a directory
of thousands of logs could spawn thousands of threads; and
default_parallel_workers forced >=4 workers even on 1-2 vCPU hosts. Use
crossbeam_utils::Backoff (spin->yield, reset on work) on the idle paths, clamp
worker_count to MAX_PARALLEL_COMPRESS_WORKERS, and lower the default floor to 1.

Ref: rustfs/backlog#1204 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): warn when active-file guard is disabled by empty filename

OLC-13 (defense-in-depth): the scanner protects the live log purely by exact
filename equality against active_filename. An empty active_filename silently
disables that protection, so a non-empty file_pattern could make the live log
a deletion candidate via the public builder. Warn in build() when that unsafe
combination is configured. The audit's "never delete the newest match"
structural guard is intentionally not implemented: it would conflict with the
legitimate keep_files=0 semantics (purge all rotated logs). The naming
contract is instead locked by regression tests (OLC-14).

Ref: rustfs/backlog#1206 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): warn on unknown algorithm/match_mode, echo match_mode

OLC-07: from_config_str silently fell back to defaults for unrecognized
compression algorithm and match mode (any non-"prefix" value became Suffix),
hiding operator typos like "prefixx" that could make the cleaner match no
rotated logs. Warn on a non-empty unrecognized value in both parsers, and
echo the resolved match_mode in the startup log alongside the algorithm.

Ref: rustfs/backlog#1200 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs/cleaner): derive orphan .tmp suffixes and exempt them from min age

OLC-10: orphan `*.gz.tmp`/`*.zst.tmp` cleanup was gated by min_file_age_seconds
(default 3600), so crash-left orphans lingered up to an hour, and the tmp
suffix list was hardcoded rather than derived from compressed_suffixes() — a
new codec would leave `*.<ext>.tmp` orphans the scanner never recognizes.
Derive the temp suffix from CompressionAlgorithm::compressed_suffixes(), and
gate orphan removal on a small fixed grace window instead of min_file_age
(orphans are never live-written after the rename that would promote them).

Ref: rustfs/backlog#1203 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(obs/cleaner): cover symlink, archive expiry, idempotency, and edge cases

OLC-14: add regression tests for the previously-untested safety/correctness
branches — symlink rejection (external target never deleted), archive age
expiry vs fresh retention, archive byte-cap trim with retention disabled,
gz/zst classification, max_single_file_size selection, min_age protecting a
fresh non-empty log, active-file exclusion when the active name also matches
the pattern, invalid exclude glob not aborting build, dry-run + compression
creating no archive, gzip round-trip validity, and the idempotent-archive
branch trusting a valid prior archive.

Ref: rustfs/backlog#1207 (audit rustfs/backlog#1193)

Co-Authored-By: heihutu <heihutu@gmail.com>

* style(obs/cleaner): apply rustfmt and collapse nested if (clippy)

Formatting-only cleanup over the audit fix series: rustfmt normalization of the
multi-line expressions introduced in compress.rs/core.rs, plus collapsing the
delete_files directory-fsync into a single let-chain to satisfy
clippy::collapsible_if. No behavior change.

Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor(obs/cleaner): collapse redundant source stats in compression path

The per-issue fixes to compress_with_writer accumulated three metadata()
syscalls on the source in the real compression path: an input_bytes read that
was immediately shadowed by the copied byte count (a dead read), a source_mode
read (OLC-06), and the pre-OLC-06 post-write chmod re-reading the same mode.
Collapse to a single fd-based read — move the dry-run input_bytes read into
the dry-run branch, read source_mode from the already-open fd (no path stat,
no TOCTOU), and reuse it for the post-write chmod. Behavior is unchanged
(same inode's mode, written for input size); 3 source stats -> 1.

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(obs/cleaner): fix typo flagged by CI (mis-set -> misconfigured)

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-12 12:01:52 +00:00
houseme 13bdca6762 build(toolchain): switch Rust channel to stable (#4775)
* Change Rust toolchain channel to stable

Signed-off-by: houseme <housemecn@gmail.com>

* style: apply clippy --fix and cargo fix lint suggestions

Run `cargo clippy --fix --all-targets --all-features` and
`cargo fix --lib --all-targets` across the workspace, then resolve the
remaining warnings by hand:

- collapse needless borrows in `format!` args, prefer `?` over explicit
  early returns, and use `.values()` / `.flatten()` iterator adapters
- rewrite the `Md5` scan loop via `manual_flatten` and re-indent the
  `select!` macro body (rustfmt skips macro interiors)
- annotate the intentional dead-code `Md5` inherent methods (constructed
  only by the test factory) with `#[allow(dead_code)]`

Behavior is unchanged.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-12 18:59:43 +08:00
houseme a766271246 chore(deps): update flake.lock (#4770)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/d333699' (2026-07-02)
  → 'github:NixOS/nixpkgs/716c7a2' (2026-07-11)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/fe5aee0' (2026-07-04)
  → 'github:oxalica/rust-overlay/a286e5b' (2026-07-12)
2026-07-12 08:30:05 +00:00
cxymds d8e69a3adf fix(logging): enforce single-writer sinks and bound tracing (#4765)
* fix(obs): prevent rolling log stdout aliasing

* fix(ecstore): bound hot-path tracing payloads

* test(logging): guard service and disk log invariants

* fix(obs): silence useless_conversion on st_dev for Linux clippy

rustix's Stat.st_dev is u64 on Linux/glibc, making u64::try_from a no-op
that trips clippy::useless_conversion under -D warnings. The conversion is
still needed on macOS/BSD where st_dev is a signed dev_t, so suppress the
lint on that line rather than dropping the portable fallible conversion.

* fix(logging): keep stdout sink validation portable (#4769)

* fix(obs): keep stdout device conversion portable

* docs(logging): update single-writer plan status

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-07-12 16:03:01 +08:00
houseme 2ddafb4ed9 test(ecstore): bound file sync probe waits (#4767)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-12 16:01:09 +08:00
Zhengchao An 4c9431704c fix(ecstore): cancel orphaned listing walks (#4773) 2026-07-12 15:21:21 +08:00
Zhengchao An 71497ba39b fix(ci): evaluate ILM every scanner cycle in lifecycle behavior lane (#4772)
The s3-tests lifecycle expiration cases (test_lifecycle_expiration,
test_lifecyclev2_expiration, test_lifecycle_deletemarker_expiration)
flaked with counts stalling one plateau behind (e.g. `assert 6 == 4`).

Root cause: a compacted directory is only re-descended -- and its
objects re-evaluated against ILM rules -- once every
DATA_USAGE_UPDATE_DIR_CYCLES scanner cycles (default 16). At the lane's
accelerated RUSTFS_SCANNER_CYCLE=2 that is ~32s between evaluations, so
a Days=1 object due at debug_day (10s) is not actually expired until the
next ~32s boundary (~42s) -- just past the test's 4*lc_interval (40s)
poll window, so the list still returns the pre-expiry count.

Set RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES=1 for this debug-only lane so
compacted directories are re-descended every cycle and ILM fires within
~2s of the due time, comfortably inside the poll window. This does not
touch the 4*lc_interval < 5*debug_day plateau invariant.
2026-07-12 15:20:22 +08:00
Zhengchao An c4c198670d docs: remove agent-generated planning docs and forbid committing them (#4771)
docs: remove agent-generated planning docs, forbid committing them

Delete one-shot planning/progress artifacts that were checked into the tree:
the 14 superpowers plan/tracker docs under docs/superpowers/plans/, plus
issue-scoped implementation plans, optimization conclusions, and dated
benchmark-result snapshots under docs/ (issue-4003 ListObjectsV2 plans,
get-small-file conclusion, issue824/issue829 benchmark results, issue-713
>1GiB GET baseline summary and ops guide).

Codify the rule so they do not come back:
- .gitignore drops the docs/superpowers whitelist, so anything new under
  docs/ stays ignored unless force-added.
- AGENTS.md gains an explicit 'do not commit planning-type documents' rule
  scoping version control to the durable architecture/operations/testing sets.
- docs/architecture/README.md, overview.md, arch-checks SKILL.md, and
  check_doc_paths.sh drop their references to the removed archive.
2026-07-12 14:14:15 +08:00
Zhengchao An b235762fdb fix(ci): unblock e2e-s3tests startup and create disk dirs for distributed volumes (#4768)
The scheduled e2e-s3tests sweep failed at "Wait for RustFS ready" in both
topologies because the server never started (issue #4762).

Two independent startup faults, both surfaced now that the local
physical-disk-independence guard is enforced:

- single: RUSTFS_VOLUMES=/data/rustfs{0...3} all live on one physical
  device on the runner, so the guard aborts startup. Set the
  CI-sanctioned RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true (what the guard's own
  error message and the e2e tests already use).

- multi: the entrypoint's process_data_volumes skipped every non-absolute
  entry, so the distributed URL form
  "http://rustfs{1...4}:9000/data/rustfs{0...3}" never created
  /data/rustfs0..3. LocalDisk init then aborts with VolumeNotFound because
  resolve_local_disk_root no longer auto-creates the disk root. This also
  broke the shipped .docker/compose/docker-compose.cluster.yaml for real
  distributed docker deployments.

entrypoint.sh now:
  1. Expands multiple {N...M} ranges per token (the URL form carries two).
     The previous single-pass expander collapsed it to "http://rustfs1"
     and dropped the disk path; it now re-scans until no ranges remain,
     operating on only the first brace to keep multi-range tokens intact.
  2. Creates the local filesystem path for URL-form endpoints (stripping
     scheme://host:port) without appending them as CLI args — rustfs reads
     the distributed form from RUSTFS_VOLUMES directly.

Absolute-path and default (/data) inputs expand byte-identically to before.
The multi compose also gets the CI disk-check bypass, since the four disks
share one device inside each node's container.
2026-07-12 13:41:46 +08:00
Zhengchao An e3533a4611 test(replication): cover version deletion convergence (#4764)
test(replication): cover version delete convergence
2026-07-12 13:07:32 +08:00
Zhengchao An 0ed0760fa2 fix(ci): restore lifecycle debug day to 10s to fix flaky expiration test (#4766) 2026-07-12 13:01:04 +08:00
Zhengchao An 5a4cf1d4b5 fix: repair flaky moka clear drain loop from #4759 (#4763)
fix(cache): drain pending removals until entry_count reaches zero in clear()

The previous clear() implementation used a single run_pending_tasks()
call after invalidate_all(), which was insufficient under concurrent
fill pressure — moka processes invalidations lazily in batches, so
entries can linger after a single maintenance pass.

Replace the fixed single call with a drain loop (up to 256 rounds) that
calls run_pending_tasks() and yields between iterations until
entry_count() reaches zero. This ensures the concurrency storm test
(moka_backend_concurrency_storm_leaves_no_leaked_state) passes reliably.

The earlier fix (#4759) added a pre-invalidate_all drain and an 8-pass
loop but reordered operations in a way that introduced a new race. This
commit keeps the original invalidate_all-first ordering and only adds
the drain loop after the initial run_pending_tasks() call.
2026-07-12 04:26:38 +00:00
houseme 46e43f608f feat(observability): add Grafana dashboard for the object data cache (#4761)
The GET body cache exports 11 `rustfs_object_data_cache_*` metrics but no
bundled dashboard visualized them. This adds one so operators can see the
cache's behaviour without hand-writing PromQL.

New `grafana-object-data-cache.json` (auto-provisioned from the dashboards
directory, `${DS_PROMETHEUS}`, schema 38) with 19 panels covering every metric:
hit ratio and lookup outcomes, plan decisions and cacheable ratio, fill
outcomes and fill-duration quantiles, hit-vs-fill byte throughput, entries and
weighted bytes vs capacity, in-flight fills, memory-pressure skips,
invalidations by reason/outcome, and size-class breakdowns. PromQL matches each
metric's type — rate() for counters, histogram_quantile() for the fill-duration
histogram, direct reads for gauges.

The observability README (EN + ZH) dashboard table gains a matching row.

Refs: backlog#1107

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-12 11:45:32 +08:00
Zhengchao An 631d93092e docs(obs): align OtelConfig doc defaults with actual constants (#4760)
Six doc-comments on `OtelConfig` fields stated defaults that disagreed
with the constants the runtime actually applies in
`extract_otel_config_from_env`. Correct them to match:

- profiling_export_enabled: false -> true (DEFAULT_OBS_PROFILING_EXPORT_ENABLED)
- sample_ratio: 0.1 -> 1.0 (SAMPLE_RATIO)
- meter_interval: 15 -> 30 (METER_INTERVAL)
- logger_level: info -> error (DEFAULT_LOG_LEVEL)
- log_rotation_time: daily -> hourly (DEFAULT_LOG_ROTATION_TIME), and
  document the full minutely/hourly/daily set plus the daily fallback
- log_cleanup_interval_seconds: 21600 -> 1800 (DEFAULT_OBS_LOG_CLEANUP_INTERVAL_SECONDS)

Doc-comment only; no behavior change.
2026-07-12 11:10:42 +08:00
Zhengchao An 3832f1d270 fix(cache): drain pending removals during clear (#4759) 2026-07-12 09:46:13 +08:00
Zhengchao An b540c7e2d0 test(ecstore): cover list marker key stripping (#4757) 2026-07-12 06:01:44 +08:00
Zhengchao An a5765274fc fix: auto-repair test_rename_data_shares_file_sync_limit hang on macOS (#4758)
fix(test): use canonicalized disk root for file_sync_probe in rename_data test

On macOS, tempfile::tempdir returns /var/folders/... while LocalDisk
resolves the root to /private/var/folders/... via dunce::canonicalize.
The file_sync_probe::enter() path check uses starts_with(), so passing
the non-canonical tempdir path caused the probe to never activate,
making wait_for_active() hang indefinitely.

Use disk.root (already canonicalized) for the probe instead.
2026-07-12 06:01:39 +08:00
Zhengchao An 676f2276b4 fix(replication): refresh targets after site endpoint edits (#4756)
* fix(replication): refresh targets after site endpoint edits

* fix(replication): serialize site bucket lifecycle
2026-07-12 05:03:17 +08:00
Zhengchao An af831bde4b fix(cache): drain entries before clear returns (#4751) 2026-07-12 04:32:21 +08:00
Zhengchao An b449af160c test(ci): stabilize lifecycle behavior checks (#4755) 2026-07-12 04:02:01 +08:00
houseme 5088a6cde4 perf(obs): trim per-collection-cycle waste in report_metrics (#4748)
`report_metrics` runs on every metrics collection cycle and did three things it
did not need to, for every metric, every cycle:

- interned `metric.name`/`metric.help` through a `Mutex<HashMap>` even when the
  `Cow` was already `Borrowed(&'static str)` (the common case for statically
  named metrics);
- re-ran `describe_*!` (which re-locks the recorder's metadata map) although the
  metadata never changes;
- allocated a fresh `Vec<(String, String)>`, cloning every label key and value,
  even though `metric.labels` is already `[(&'static str, Cow<'static, str>)]`.

Now: names/help resolve to `&'static str` without touching the intern cache when
already borrowed; each metric is described once (tracked in a `HashSet`); and the
recorder is fed `&metric.labels` directly, removing the per-cycle label clone.

No metric names, help, label keys/values, or emitted values change. Verified by
building rustfs-obs and running the report unit tests (the `metrics` macro
accepts the borrowed label slice directly).

Addresses rustfs/backlog#1185 (P3, report path).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 17:46:13 +00:00
houseme 6886dca7d1 feat(ecstore): make GET codec-streaming a single rollout switch (backlog#1183) (#4752)
backlog#1183, staged rollout step. Simplify enabling the zero-duplex GET
codec-streaming fast path to a single switch — RUSTFS_GET_CODEC_STREAMING_ROLLOUT
(default "off") — now that body/header parity is proven (parity e2e net + bench
A/B on backlog#1183).

- Add a clean production rollout token "on" (aliases "full"/"production"); the
  legacy "internal"/"benchmark" tokens remain accepted.
- Flip DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENABLE and the two ..._COMPAT_CONFIRMED
  defaults to true. They are retained as emergency kill-switches (set any to
  false to force the fast path off) but no longer gate enablement — the rollout
  switch does.

No production behavior change: with no env set the rollout switch defaults to
"off", so GET stays on the legacy duplex path exactly as before. Flipping the
hard default to on is deferred to a follow-up after a production soak.

Note (intentional semantics change): with the rollout switch opted in
("on"/"internal"/"benchmark"), codec streaming now activates without also
setting the two ..._COMPAT_CONFIRMED vars — compatibility is confirmed, so those
confirmations are baked in.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 17:36:58 +00:00
Ramakrishna Chilaka 028ba6a675 perf(ecstore): parallelize multipart shard syncing (#4734)
Bound large shard-directory syncs per disk and process while preserving small-directory and durability behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-12 01:36:24 +08:00
houseme 633c131cef perf(io-metrics): cache handles for hot label-less metric emitters (#4750)
The `metrics` macros re-run the recorder's `register_*` on every emission — a
`RwLock` read, a name-key hash, and an `Arc` clone — even for a metric that
never varies its key. For the hot, label-less recorders on the per-IO path
(`record_data_transfer`, `record_io_latency`, `record_io_latency_p95/p99`,
`record_io_queue_congestion`) that lookup is pure overhead once observability is
on.

Add `counter_increment_cached!` / `gauge_set_cached!` / `histogram_record_cached!`
that resolve the handle once via `LazyLock` in production and reuse it. Under
`cfg(test)` they re-resolve on every call, because the `metrics` crate resolves
against a thread-local recorder that `with_local_recorder` swaps per test — a
process-global cached handle would bind to whichever recorder was active first
and break test capture. The macros only wrap FIXED (label-less) keys, and the
`metrics_enabled()` gate still short-circuits before any emission when disabled.

Verified: the only callers of these functions are the collector (io-metrics'
own cfg(test) tests, which re-resolve) and production code; no cross-crate test
captures them. rustfs-io-metrics builds on both cfg paths, 147 unit + 4 doctests
pass, clippy clean.

Addresses rustfs/backlog#1185 (P3, per-emission handle caching).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-12 01:33:30 +08:00
houseme 89557a7ffe perf(ecstore): cache io_uring fallback root label (#4747)
`record_uring_fallback` is called at multiple sites in the io_uring read
path whenever a read falls back to `StdBackend`. It formatted
`self.root.display().to_string()` on every call, heap-allocating a
`String` from a `Path` that never changes after construction — pure
per-read waste when io_uring is degraded.

Cache the label once in `UringBackend::try_new` as a `String` field
(`root_label`) and clone it per emission. The metric name and the
`"root"` label value are unchanged; only the redundant `Path` formatting
is removed. The clone is a single alloc of an already-short string.

Refs: rustfs/backlog#1185

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 17:25:06 +00:00
houseme 5282c71f86 perf(io-metrics): make internode peer-health read-mostly with an RwLock (#4746)
`cluster_peer_should_bypass` is called before every internode RPC when the
offline-bypass feature is enabled (remote disk `get_client`, remote locker), and
it took a single process-global `Mutex<HashMap>` even for the overwhelmingly
common case of an online or unknown peer, which is read-only. That serialized
all concurrent internode client acquisitions on one lock.

Switch `CLUSTER_PEER_HEALTH` to a `RwLock`:
- the hot check takes a shared read lock and returns immediately for unknown or
  online peers, so concurrent RPCs no longer serialize;
- only an offline peer (rare) drops to the write lock to record a re-probe, with
  a re-fetch/re-check because the state can flip back online between releasing the
  read lock and taking the write lock;
- the write paths (dial reachable/unreachable) and the read-only
  `cluster_peer_is_offline` move to `write()`/`read()` accordingly.

Behavior is unchanged on every path (online/unknown -> not bypassed; offline ->
bypass with one re-probe per interval); `Instant::now()` now runs only on the
offline path. Poison recovery is preserved. All internode unit tests pass.

Addresses rustfs/backlog#1185 (P2).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 17:10:35 +00:00
houseme 53bd4bb300 test(e2e): pin GET codec-streaming body/header parity vs legacy duplex (backlog#1183) (#4745)
backlog#1183 tracks flipping the default GET data path from the legacy
tokio::io::duplex double-copy to the zero-duplex codec-streaming fast path.
That flip is gated behind RUSTFS_GET_CODEC_STREAMING_BODY_COMPAT_CONFIRMED /
..._HEADER_COMPAT_CONFIRMED, which need empirical evidence the two paths are
byte- and header-identical.

Add an e2e regression net that runs the same object matrix twice against the
same on-disk EC shards, changing only the codec-streaming env gates: phase A
(default) takes the legacy duplex path, phase B (gates opened) takes codec
streaming. It asserts byte-for-byte (sha256) and header-for-header equality
across inline / small / multi-block (1.5M/3M/5M+) / multipart objects, plus a
ranged GET (which falls back to legacy by gate design). Path confirmation is
not assumed: the legacy path logs "Created duplex pipe ..." per full GET, so
the test counts that marker per phase and asserts the codec phase created zero
duplex pipes, proving the fast path actually ran rather than silently falling
back to the path it is compared against.

To capture the child server's logs for that assertion, add an optional
RustFSTestEnvironment.capture_log_path (default None = inherit stdio,
backward compatible) that redirects the spawned server's stdout+stderr to a file.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 16:33:57 +00:00
Zhengchao An 763f246f8a test(ecstore): add shared MockWarmBackend test utility for lifecycle and tier tests (#4716)
* test(ecstore): extract shared MockWarmBackend into a test-util feature (backlog#1148 ilm-6)

The tier/lifecycle integration tests carried two byte-for-byte copies of an
in-memory WarmBackend mock — one in crates/scanner/tests and one in
rustfs/src/app — plus duplicated register_mock_tier and polling helpers. Both
implemented the same ecstore WarmBackend trait.

Consolidate them into ecstore behind a new `test-util` feature, exposed via the
`rustfs_ecstore::api::tier::test_util` facade:

- MockWarmBackend: in-memory WarmBackend with an operation log (for ordering
  assertions such as "local delete precedes remote remove") and fault injection
  (FaultConfig): unreachable, HTTP 5xx, credential rejection, injected latency,
  plus external_remove to simulate an out-of-band remote deletion.
- register_mock_tier / register_mock_tier_backend: register the mock into any
  TierConfigMgr handle (the global manager used by scanner tests or a
  per-instance one used by the app tests).
- xl.meta transition assertion helpers: read_transition_meta,
  assert_transition_meta_consistent (cross-shard consistency of the
  status/tier/remote-key/remote-version-id tuple plus free-version count), and
  free_version_count.
- polling helpers: wait_for_remote_absence, wait_for_object_count,
  wait_for_free_version_absence.

Both existing copies now consume this single definition; `rg 'struct
MockWarmBackend'` collapses to one. The feature is enabled only from
[dev-dependencies], so it never links into the production binary (resolver 3).

Designed for downstream ilm-8 (restore lifecycle) and ilm-11 (tier fault
injection matrix). Coordinates with #4706 (ilm-2), which adds op-logging to the
scanner mock — that op-logging is now part of this shared surface, so #4706
should rebase onto it.

Refs rustfs/backlog#1148 (ilm-6), rustfs/backlog#1155.

* test(ecstore): fix shared MockWarmBackend usage after main merge

- Access stored objects via MockWarmBackend::contains() instead of the now
  private inner objects map (fixes E0609 after the shared test-util refactor).
- Drop dead ReadCloser/ReaderImpl/DiskAPI imports and the unused
  transition_api test re-exports the mock extraction left behind.
- Reword the scanner/rustfs test-util dependency comments so they no longer
  embed the literal rustfs_ecstore:: path that trips the ECStore
  architecture-migration guard.
2026-07-12 00:20:45 +08:00
houseme ce7d3119b2 perf(io-metrics): throttle collector percentile sort off the per-IO path (#4744)
`MetricsCollector::record_io_operation` recomputed P50/P95/P99 on every disk IO
by taking a read lock, collecting up to `max_latency_samples` (1000) into a
`Vec<u128>` and fully sorting it — an O(n log n) alloc+sort per operation on the
GET read path (gated by stage metrics). Both consumers sample only periodically:
the autotuner reads `avg_io_latency_us` on its tuning tick, and the P95/P99
values are never read internally — they only feed OTEL export. Nothing needs
per-IO freshness.

Split the two costs:
- The window mean (stored in `avg_io_latency_us`, read by the autotuner) is now
  maintained in O(1) via a running sum kept in step with the window's push/pop,
  and refreshed on every op — no sort, no warm-up regression.
- The P95/P99 sort is throttled to once per `PERCENTILE_RECOMPUTE_INTERVAL`
  (128) operations. A bounded lag is invisible to the periodic autotuner tick and
  OTEL export while the per-op sort cost is amortised ~128x away.

Semantics are otherwise unchanged: same sliding window, same percentile indices,
same mean value. Tests updated — the percentile test forces a recompute to
exercise the math directly, and a new test asserts the mean tracks every op while
the percentiles only recompute at the interval boundary.

Addresses rustfs/backlog#1185 (P1).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 16:19:12 +00:00
Zhengchao An 96e5699568 test(ilm): self-managed lifecycle expiry e2e + backdated-mtime helper (backlog#1148 ilm-3) (#4710)
Convert the two reliant lifecycle expiry tests from #[ignore] + a hardcoded
localhost:9000 server to self-managed RustFSTestEnvironment tests (random port,
isolated temp dir) and wire them into the PR e2e-smoke subset.

Time control is chosen per test and documented in-module:
- test_lifecycle_expiry_backdated_mtime: mod_time back-dating via the internal
  source-replication backdoor (new put_object_with_backdated_mtime helper) so a
  Days=1 rule is already due, with RUSTFS_ILM_PROCESS_TIME=1 shrinking the
  rounding boundary. Asserts the backdated matching-prefix object expires and a
  non-matching-prefix object with the same backdating survives (isolates the
  prefix filter). Proves the backdoor does not trip the replication gate.
- test_lifecycle_versioned_current_version_expiry_creates_delete_marker:
  RUSTFS_ILM_DEBUG_DAY_SECS=2 (ilm-5) accelerates the day length; asserts a
  latest delete marker is created, the original data version is retained and
  still readable by version id.
- test_lifecycle_zero_day_expiry: Days=0 immediate expiry; matching object
  deleted, non-matching survives.

All three use RUSTFS_SCANNER_CYCLE=1 so the scanner runs every second; tests poll
for the terminal state instead of sleeping fixed wall-clock. Ignore count for
reliant/lifecycle.rs goes 2 -> 0.

CI wiring: added a distinct 'test(/^reliant::lifecycle::/)' clause to the
existing profile.e2e-smoke default-filter in .config/nextest.toml (the single
sanctioned e2e wiring mechanism per crates/e2e_test/README.md; no new e2e job).

Refs rustfs/backlog#1148 (ilm-3), rustfs/backlog#1155
2026-07-11 15:40:29 +00:00
houseme 264b2dd480 perf(metrics): drop needless per-emission work on hot metric paths (#4743)
* perf(metrics): drop needless per-emission work on hot metric paths

Audit of the metrics hot paths surfaced four low-risk wins where emission did
work it did not need to:

- `record_file_cache_reclaim_success/error` (disk/local.rs) called `.to_string()`
  on `kind` (already `&'static str`) and on the `"ok"`/`"err"` literals, heap-
  allocating up to four `String`s per page-cache reclaim window — which runs per
  read-stream reclaim. The `metrics` macros accept `&'static str` label values
  directly, so pass them as-is.
- `record_read_repair_dedup` (set_disk/core/io_primitives.rs) likewise
  `.to_string()`-ed an already-`&'static str` `reason`.
- `SetDisks::get_object_reader` (set_disk/ops/object.rs) captured `Instant::now()`
  and emitted the `rustfs.lock.acquire.*` counter and histogram unconditionally
  on every GET, right beside an already-gated stage timer. Gate them behind
  `get_stage_metrics_enabled()` too, so an inactive observability config pays no
  per-GET clock read or recorder lookups.
- The per-response-body-chunk counter in server/http.rs re-ran the `counter!`
  registry lookup on every chunk (a streamed GET emits many). Resolve the
  label-less handle once into a `LazyLock<metrics::Counter>`; the global recorder
  is installed at startup before any response streams, so the cached handle binds
  to the final recorder.

No metric names or label values change. The only behavior change is that the
`rustfs.lock.acquire.*` GET-path metrics now follow the GET stage-metrics flag,
consistent with the neighbouring stage timings.

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(metrics): gate page-cache reclaim metrics behind metrics_enabled()

`record_file_cache_reclaim_success/error` run per read-stream reclaim window on
large-object reads and emitted unconditionally. When general metrics are
disabled the `counter!`/`histogram!` macros still construct three metric keys
per call for nothing. Skip the emission behind `rustfs_io_metrics::metrics_enabled()`,
matching how the io-metrics free functions self-gate. The serial reclaim-metrics
test now enables the flag (save/restore) alongside the existing stage gate.

Left ungated deliberately: `record_read_repair_dedup` (rare read-repair path,
and its non-serial test would need a global-flag toggle), and the HTTP body-chunk
counter (its cached handle already makes the disabled case a no-op increment).

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 15:27:15 +00:00
houseme e742a540a4 test(cache): guard the body-cache eligibility gate against deny-list regressions (#1146) (#4742)
`full_object_plaintext_len` decides whether a body-cache hook hit may serve
bytes in place of the erasure read. It is a fail-closed allow-list: it excludes
every read whose `ReadPlan::build` applies some other transform (ranged/part,
raw/data-movement, restore, encrypted, remote) with an early `return None`,
then returns a `Some(..)` length only for the whole-plaintext cases. A newly
added `ReadPlan` branch that nobody teaches this gate about falls through to
`None` and safely bypasses the cache. Flip it to a deny-list and the same new
branch silently serves bytes in the wrong representation — the backlog#1108 /
#1109 / #1146 class of bug.

The existing unit and e2e tests only cover the branches that exist today. This
adds `scripts/check_body_cache_whitelist.sh`, a structural guard wired into
pre-commit / pre-pr / dev-check and CI, that asserts every exclusion predicate
and a `return None` still precede the first `Some(..)`. Reordering a predicate,
dropping one, moving the positive return ahead of the gate, or renaming/removing
the function all fail; wording, formatting, and adding a new exclusion in the
same gate do not. Mutation-tested against all four regression shapes.

This machine-enforces the structural invariant that backlog#1146 was kept open
to guard by hand.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 14:51:24 +00:00
Zhengchao An fa27bef532 test: quarantine embedded delete flake (#4741) 2026-07-11 14:29:29 +00:00
houseme 1ab66e124c test(ecstore): pin the remaining io_uring fd-cache invariants (#1180) (#4739)
Close the three test-completeness items in rustfs/backlog#1180 that the earlier
hardening (rustfs/rustfs#4726, #4729) left unpinned; the other two of the five
(sharded cancel routing, bailout-handle error) already landed in rustfs/uring.

- rename_data end-to-end invalidation: drive the real `LocalDisk::rename_data`
  commit (non-inline part, so `invalidate_part_paths` is non-empty) and assert
  the destination part descriptor is dropped — not merely `rename_file`/`delete`.
  Both production `rename_data` call sites share this invalidation, so a
  "fix one copy, miss the other" regression is now caught. The test is
  non-vacuous: it seeds the cache, removes the on-disk data dir out of band (the
  cached fd keeps the old inode alive and clears the path for the directory
  rename `rename_data` performs), and asserts a read still returns the OLD bytes
  before the commit — which fails outright if the cache is off, so it cannot pass
  without a live cache.
- FD_CACHE_TTL backstop: an injected short TTL proves the cache self-evicts a
  descriptor with no explicit invalidation; a static check pins the 5s value.
- zero-length read bounds parity on the cache-HIT path: a `length == 0` read
  past EOF must be rejected identically to the miss path and StdBackend, pinning
  the #1173 fix against regression.

Refactors `FdCache::new` to delegate to a private `with_ttl(ttl)` helper so the
TTL backstop can be exercised with a short TTL instead of a multi-second wait.

Verified: `cargo test -p rustfs-ecstore` on Linux with real io_uring
(seccomp=unconfined, RLIMIT_NOFILE raised, RUSTFS_URING_TESTS_MUST_RUN=1 so a
degraded skip fails rather than passing vacuously).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 13:50:48 +00:00
Zhengchao An 313d282880 fix(ecstore): gate gauge import on Linux (#4737) 2026-07-11 13:29:08 +00:00
houseme 793b2a06e2 perf(ecstore): snapshot per-IO env config on local disk read/write paths (#4736) 2026-07-11 13:26:26 +00:00
houseme 9d64d71bd7 fix(ecstore): reduce GET reader setup shard fanout (#4735) 2026-07-11 21:22:40 +08:00
houseme a8eed1c44d perf(io-metrics): add a global switch to gate general metric emission (#4733)
The io-metrics crate already had per-stage GET/PUT switches, but the request
headers/summaries and ~40 general recorders (I/O scheduler, bytes-pool,
zero-copy, bandwidth, system-resource, error/timeout/retry) emitted
unconditionally, paying their label allocations and arithmetic even when no
metric recorder is installed.

Add `METRICS_ENABLED: AtomicBool` (default false) with `set_metrics_enabled()`
/ `metrics_enabled()`, isomorphic to the existing stage switches, and gate:

- GET headers/summaries via `get_stage_metrics_enabled()`:
  record_get_object_request_start / _request_started / _request_result /
  _timeout / _completion / _total_duration_with_path, plus legacy
  record_get_object.
- PUT headers via `put_stage_metrics_enabled()`:
  record_put_object_request_start / _request_result, plus legacy
  record_put_object.
- 41 general recorders via `metrics_enabled()`.

Startup enables all three switches together under
`observability_metric_enabled()` (startup_observability.rs), with a passthrough
in startup_runtime_sources.rs. The global recorder is itself only installed
when observability metric export is on, so gating off is pure cost savings when
export is disabled and a no-op when it is enabled.

Deliberately NOT gated (would break function, not just observation): the EC
encode in-flight and GET buffered-bytes accounting guards
(add/remove_ec_encode_inflight_bytes, track_get_object_buffered_bytes) whose
values are read back by current_*; and the stateful sibling modules
(lock/deadlock/backpressure/autotuner/adaptive_ttl/collector/internode). The
console realtime metrics endpoint (/admin/v3/metrics) samples system state and
internode metrics directly, so it does not depend on the gated recorders.

Adds a toggle test exercising both the enabled and disabled paths, serialized
on the existing METRICS_FLAG_LOCK to stay robust under nextest's shared-process
model.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 12:11:54 +00:00
houseme c79366d42c refactor(ecstore): tidy io_uring read backend (docs + fd-cache handle) (#4732)
Two behavior-preserving cleanups to the io_uring local read backend that
accumulated as later work layered onto it:

- Reunite the `UringBackend` struct doc comment. The backlog#1145 fd-cache
  constants were inserted into the middle of the struct's doc block, so its
  opening sentences were orphaned onto `ENV_RUSTFS_IO_URING_FD_CACHE` and the
  struct itself was documented by a sentence fragment starting mid-clause with
  "through rustfs-uring's...". Move the opening back onto the struct and give
  the constant its own one-line doc.

- Fold the fd-cache handle and its lookup key into a single
  `Option<(&FdCache, FdKey)>` in `pread_uring`, so the get and the
  insert_if_fresh sites stop re-deriving `self.fd_cache.as_ref()` and
  re-matching presence. Semantics are identical, including the backlog#1176
  generation guard (`gen_at_open` snapshot before open, insert only when the
  generation is unchanged).

No functional change. The borrow/move pattern was validated against a
host-compilable reduction (the cfg(linux) path cannot be built from macOS);
CI compiles the Linux backend.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 12:08:47 +00:00
houseme 4f29dcdcec chore(deps): bump rustfs-uring to 0.2.1 (#4731)
chore(deps): bump rustfs-uring to 0.2.1 from crates.io

rustfs-uring 0.2.1 routes the driver's runtime diagnostics through `tracing`
with structured fields instead of `eprintln!` (rustfs/uring#13). No public API
change — UringDriver::probe_and_start_sharded, read_at, and read_at_direct keep
their signatures — and the read path / cancel-safety ownership model are
untouched, so this is a drop-in patch bump of the version requirement plus the
lockfile entry.

0.2.1 adds a `tracing` dependency; it is recorded in the rustfs-uring lock
entry. tracing is already in the workspace graph, so nothing new is pulled in.

The Cargo.lock change is scoped to the rustfs-uring package only; unrelated
lockfile drift is left for its own change.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 12:01:42 +00:00
houseme ffca98cdbf fix(ecstore): harden io_uring integration (#4726)
* fix(ecstore): close the fd-cache open-then-insert race with a generation guard (rustfs/backlog#1176)

pread_uring's miss path opened a descriptor on the blocking pool and only then
inserted it into the moka cache. moka's invalidations cover only entries present
at call time, so a heal/delete commit that invalidated between the open and the
insert could not stop the just-opened stale inode from being cached afterwards —
serving the pre-heal/pre-delete inode for up to the TTL and defeating the heal.

Add an invalidation generation to FdCache, bumped by invalidate_exact and
invalidate_under before they touch moka. The read path snapshots the generation
before opening and inserts via insert_if_fresh, which refuses the insert if the
generation moved during the open and, with a post-insert re-check, removes the
entry if an invalidation raced the insert itself. Reads that never miss are
unaffected.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): invalidate the fd cache on the primary object-delete paths (rustfs/backlog#1175)

The fd-cache invalidation contract was only wired into DiskAPI::delete,
rename_file and rename_data, but object deletion almost never goes through
LocalDisk::delete — DeleteObject(s) reach delete_version, delete_versions ->
delete_versions_internal, and delete_paths, all of which remove a version's data
dir (move_to_trash / rename_all staging) with no invalidation. A cached io_uring
descriptor kept the deleted part.N inode readable for up to the TTL, so a GET in
that window could still return deleted data.

Invalidate every cached fd under the removed data dir at each site: in
delete_version and delete_versions_internal the data_dir uuid and object path are
in hand (invalidate_cached_fds_under(volume, "{path}/{uuid}")); delete_paths
invalidates under each removed path. A later rollback that restores a data dir
just causes the next read to re-open it.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): close remaining fd-cache invalidation gaps (rustfs/backlog#1177)

Three residual paths could keep serving a stale descriptor:

- delete_volume removed the whole bucket tree (remove_dir_all/remove_dir) with no
  invalidation, and the cache-hit read path skips the volume-access check, so a
  cached fd kept a removed object readable. Add invalidate_cached_fds_for_volume
  (a per-volume moka predicate) and call it after the bucket is removed.

- A retired LocalDisk instance (renew_disk on reconnect builds a fresh one) kept
  its populated cache alive while still referenced by in-flight ops, so
  invalidations through the new instance never reached it. close() now clears the
  backend's cache via clear_cached_fds.

- rename_data's post-commit rollback (a commit-metadata fsync failure under
  strict durability) restored the old data dir without dropping fds cached during
  the committed window; the streaming branch now invalidates the dst part fds on
  those rollback paths. The inline branch's rollback runs inside spawn_blocking
  and is left to the TTL backstop.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): narrow the io_uring latch classes to match StdBackend (rustfs/backlog#1171)

The runtime degradation classification reused the probe-time restriction errnos,
which the driver's C7 contract explicitly warns against, so a single per-file
error could latch a whole disk off io_uring:

- is_io_uring_unsupported no longer includes EACCES: at read time on an
  already-open fd it is per-file (an LSM hooks security_file_permission on every
  read) and StdBackend hits the same denial, so falling back masks nothing and a
  full-disk latch would be wrong. ENOSYS and EPERM (seccomp/LSM applied after
  startup) remain. EOPNOTSUPP is now classified per-path by the caller.

- pread_uring_direct's read-error arm now mirrors StdBackend: an O_DIRECT-shape
  error (EINVAL/EOPNOTSUPP) latches only direct_uring.supported so eligible reads
  take StdBackend's aligned path, instead of over-latching the whole io_uring
  backend or never latching a read-side EINVAL at all.

- try_new only negative-caches genuine restriction-class probe failures in
  URING_UNSUPPORTED_DISKS; an unexpected (possibly transient) probe failure now
  falls back without latching, so the next reconnect re-probes.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(ecstore): log when a disk latches io_uring off at runtime (rustfs/backlog#1172)

A probe-gated gray release was flying blind: the permanent per-disk `active`
latch flipped with no log and no metric, so the only message operators ever saw
was the startup "io_uring read backend enabled" line — which stayed true on
dashboards even after the very first read latched the disk back to StdBackend
forever.

Add latch_active_off, which flips the latch with `swap` and logs the true->false
transition exactly once at warn with a dedicated event constant, disk root, and
errno. Both the buffered and O_DIRECT read paths use it. A fallback/latch metric
counter and periodic export of the driver StatsSnapshot (cq_overflow,
cancel_already) remain as follow-ups that need rustfs_io_metrics plumbing.

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(audit): correct the stale rustfs-uring license-allow rationale (rustfs/backlog#1181)

The dependency-review allow said rustfs-uring is "pulled as a git dependency",
but ecstore now pins it from crates.io. Update the rationale and scope the allow
to the exact pinned version (pkg:cargo/rustfs-uring@0.1.0) so a future version
bump forces a conscious re-review of the license/provenance claim instead of
being waved through on an outdated justification.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): offload io_uring driver teardown off the tokio worker (rustfs/backlog#1170)

UringBackend held Arc<UringDriver> and had no Drop, so when the last LocalDisk
reference dropped in async context (disk reconnect via renew_disk, or shutdown),
UringDriver's own Drop ran on that thread — sending Shutdown and joining each
shard thread, which can block up to the bounded-drain timeout (5s) on a hung /
D-state disk, stalling a tokio worker.

Wrap the driver in ManuallyDrop (deref is transparent, so read call sites are
unchanged) and add a Drop that takes the Arc and, when a runtime is present,
drops it on a blocking thread so the potentially-blocking join never runs on a
runtime worker. Off-runtime it drops inline.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): restore StdBackend read parity on the uring paths (rustfs/backlog#1173)

Two byte-for-byte parity breaks against StdBackend on the io_uring read paths:

- A zero-length read on an fd-cache hit returned Ok(empty) without any bounds
  check, while StdBackend and the uring miss path return FileCorrupt for an
  offset past EOF. Fstat the cached descriptor on the length==0 path and match.

- reclaim_read_range fadvise(DONTNEED)'d the raw unaligned [offset, offset+len)
  range, but fadvise only drops fully-covered pages, so the head partial page
  stayed resident — whereas StdBackend's mmap path reclaims the page-aligned
  superset. Bitrot shards' 32-byte block headers keep offsets off page
  boundaries, so this diverged on the common case. Page-align the reclaim window
  to match the mmap path exactly.

(The third parity item from the audit — a failed reclaim fadvise failing the
read — is already parity: StdBackend's mmap path propagates the same fadvise
error with `?`, so no change is needed.)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): bound worst-case in-flight memory by chunking huge uring reads (rustfs/backlog#1174)

The driver's backpressure permits count operations, not bytes, and it zero-fills
a full-size buffer per op, so a single unbounded read could pin ~length bytes per
permit (128 permits x shards x up to ~2 GiB). ecstore passes a whole part's shard
range as one pread_bytes with no upstream chunking.

On the buffered path, split reads larger than URING_MAX_OP_LEN (128 MiB) into
sequential chunks, awaited one at a time, so worst-case in-flight memory is
bounded by permits x URING_MAX_OP_LEN per shard. The threshold is high enough
that ordinary shard reads keep the single-op, zero-copy fast path unchanged. The
O_DIRECT path (opt-in, alignment-constrained) is left for a follow-up.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): gate the io_uring fd cache on RLIMIT_NOFILE headroom (rustfs/backlog#1178)

The fd cache holds up to FD_CACHE_CAPACITY (512) descriptors per disk, but
try_new cannot know the disk count and nothing checked the process fd budget. On
a bare-metal / non-systemd run with the common 1024 soft RLIMIT_NOFILE, two disks
would already exhaust fds with EMFILE surfacing on reads and probes.

Check the soft limit at try_new: enable the cache only with ample headroom
(>= 16384), otherwise log a warning once and fall back to open-per-read. The
packaged systemd unit sets 1,048,576, so tuned deployments are unaffected.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): make io_uring test skips visible and gate non-vacuity (rustfs/backlog#1179)

The ecstore io_uring tests degrade to a silent pass when io_uring is unavailable
(bare `return`s or plain eprintlns), so a CI leg on a restricted runner never
exercises the real UringBackend/FdCache/latch paths yet still goes green — an
integration regression could merge unseen.

Add uring_test_skip: it emits a grep-able `SKIP <name>` line and, when
RUSTFS_URING_TESTS_MUST_RUN is set (a CI leg that guarantees io_uring, e.g. a
seccomp=unconfined container), panics instead of skipping. Route the silent-skip
sites through it. Wiring a dedicated CI leg that sets that env on a capable
runner is tracked in the issue; this provides the enforcement mechanism.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): cover delete_paths fd-cache invalidation (rustfs/backlog#1180)

Add an end-to-end test that seeds the descriptor cache with a read, removes the
part via disk.delete_paths (one of the primary object-delete entry points that
does not go through LocalDisk::delete), and asserts the next read no longer
returns the removed inode — pinning the invalidation added in #1175. The sharded
cancel-routing half of #1180 is covered in the rustfs-uring PR.

Co-Authored-By: heihutu <heihutu@gmail.com>

* io_uring audit follow-ups: O_DIRECT chunking, inline invalidation, metrics, CI leg (backlog#1160) (#4729)

* fix(ecstore): chunk large O_DIRECT reads too, bounding in-flight memory (rustfs/backlog#1174)

The buffered read path already splits reads above URING_MAX_OP_LEN into
sequential chunks; do the same for the O_DIRECT path, which was left for a
follow-up. read_at_direct aligns each chunk's sub-range internally, and chunk
sizes are a multiple of URING_MAX_OP_LEN so a boundary re-read is at most one
block. Extract classify_direct_read_error so the single-op and chunked paths
share one copy of the EINVAL/EOPNOTSUPP-vs-subsystem latch classification rather
than duplicating it.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): invalidate cached fds on the inline rename_data rollback (rustfs/backlog#1177)

The streaming rename_data branch invalidates cached part fds on its post-commit
rollback paths, but the inline branch runs its commit and rollback inside a
single spawn_blocking closure where the async invalidate cannot be called, so it
was left to the TTL backstop.

Capture the closure's result instead of `??`-propagating it: on error (a
commit-metadata fsync failure under strict durability rolls the committed rename
back), invalidate the dst part paths at the async level before returning. Inline
objects keep their data in xl.meta rather than separate part inodes, so this is
largely defensive, but it removes the caveat and keeps the two branches
consistent.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(ecstore): export io_uring latch/fallback and driver stats metrics (rustfs/backlog#1172)

Complete the gray-release observability. Beyond the warn log added earlier, emit
metrics so a dashboard can answer "how much traffic is on io_uring vs falling
back, and is any disk degrading":

- rustfs_io_uring_latch_off_total — a disk latching io_uring off at runtime.
- rustfs_io_uring_read_fallback_total — each io_uring -> StdBackend read
  fallback (latched-off short-circuit, O_DIRECT error, buffered error).
- a low-frequency per-disk exporter of the driver StatsSnapshot as gauges
  (in_flight, cq_overflow, cancel_already), spawned in try_new. It holds only a
  Weak reference so it never keeps the driver alive, and drops any temporary
  strong reference on the blocking pool so a last-reference UringDriver::Drop
  join never runs on an async worker (rustfs/backlog#1170).

submit_errors is deliberately not exported yet: it is a field added in the
unreleased rustfs-uring 0.2.0, and ecstore still pins 0.1.0. It lands once the
dependency is bumped (rustfs/backlog#1181).

Co-Authored-By: heihutu <heihutu@gmail.com>

* ci: add a real-io_uring integration leg on ubuntu-latest (rustfs/backlog#1179)

The existing self-hosted sm-standard runners cannot guarantee io_uring is
available (a container seccomp filter can block io_uring_setup), so the ecstore
uring tests degrade to a silent skip and never exercise the real
UringBackend/FdCache/latch paths in CI.

Add a job on GitHub-hosted ubuntu-latest, which runs a recent kernel with no
container seccomp filter, running the uring-named ecstore tests with
RUSTFS_IO_URING_READ_ENABLE=true and RUSTFS_URING_TESTS_MUST_RUN=1 — the
non-vacuity gate makes the leg fail rather than skip if io_uring is unavailable,
so an integration regression can no longer merge green behind a vacuous pass.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 11:15:29 +00:00
Zhengchao An 3f1acfe8a7 fix(docker): warn instead of rejecting default or missing credentials (#4728)
* fix(docker): warn instead of rejecting default or missing credentials

The entrypoint hard-reject from #4278 broke the container-first UX and
the repo's own scheduled e2e lanes (e2e-s3tests, mint boot rustfs-ci
images with default credentials and died at startup). Maintainer
decision: ship no baked-in credentials, warn instead of block.

- missing credentials: warn and start; wording accounts for the
  RUSTFS_ROOT_*/MINIO_* alias sources the entrypoint does not inspect
- default rustfsadmin via env/CLI/file: warn and start; the warning
  notes all-default pairs cannot derive an internode RPC secret
- malformed config stays fatal: source conflicts, unreadable files,
  empty or whitespace-only values, flags missing their argument
- present-but-empty env vars now hit the empty-value hard failure
  instead of running the binary with an empty root credential
- empty/default checks trim CR and blanks like the binary; files
  without a trailing newline are no longer falsely rejected as empty
- the no-baked-credentials guard covers all four Dockerfiles, and the
  test harness refuses hosts where /usr/bin/rustfs exists
- e2e-s3tests/mint move to non-default credentials (rustfsadmin-ci),
  which also restores RPC-secret derivation for the multi-node lane

GHSA-68cw fail-closed RPC derivation (#4402) is untouched; helm stays
fail-closed by design.

* chore(docker): reword entrypoint comment flagged by typos check
2026-07-11 19:13:04 +08:00
houseme ab60244d7d chore(deps): bump rustfs-uring to 0.2.0 (#4730)
chore(deps): bump rustfs-uring to 0.2.0 from crates.io

rustfs-uring 0.2.0 is published on crates.io. It carries the read-driver
hardening from the backlog#1160 adversarial audit (cancel-safety and
graceful-degradation fixes on the Linux io_uring read path). The public
API ecstore consumes is unchanged — UringDriver::probe_and_start_sharded,
read_at, read_at_direct all keep their 0.1.0 signatures — so this is a
drop-in bump of the version requirement plus the lockfile entry.

The Cargo.lock change is intentionally scoped to the rustfs-uring package
only; unrelated lockfile drift is left for its own change.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 11:12:15 +00:00
Zhengchao An 67a88f3feb test(replication): cover multipart fanout integrity (#4727) 2026-07-11 09:07:54 +00:00
Zhengchao An bec5227018 test(e2e): add negative presigned URL SigV4 coverage (#4714)
test(e2e): negative presigned-URL SigV4 suite (backlog#1151 sec-2)

Add crates/e2e_test/src/presigned_negative_test.rs, the query-string
SigV4 sibling of the header-SigV4 suite (sec-1, #4708). Presigned URLs
were only exercised on the happy path, so nothing pinned our end-to-end
enforcement of expiry or query-signature verification.

Against a live single-node RustFSTestEnvironment (random port, isolated
temp dir), the suite asserts HTTP status + S3 error <Code> for:

  - expired presigned GET -> 403 AccessDenied (Request has expired)
  - tampered X-Amz-Signature -> 403 SignatureDoesNotMatch
  - wrong secret key -> 403 SignatureDoesNotMatch
  - tampered target key (swapped after signing) -> 403 SignatureDoesNotMatch
  - tampered presigned PUT -> 403 SignatureDoesNotMatch + object not stored
  - positive controls: valid presigned GET (body match) and PUT (HEAD verify)

Expiry is controlled without real waiting via the AWS SDK presigner's
start_time (sign as of one hour ago with a 60s window). s3s checks
expiry before the signature, so the expired case surfaces AccessDenied.

Wired into the e2e-smoke nextest profile (fast, single-node, no external
deps) so it runs on every PR per the crates/e2e_test/README.md admission
criteria.

Refs rustfs/backlog#1151 (sec-2), rustfs/backlog#1155.
2026-07-11 16:07:41 +08:00
Zhengchao An d6e3aa9140 test(admin-auth): add unit and e2e coverage for the admin authorization gate (#4717)
test(admin-auth): unit + e2e coverage for the admin authorization gate

Adds the first tests for the central admin authorization gate
`rustfs/src/admin/auth.rs`, which previously had zero coverage
(backlog#1151 sec-4, master plan #1155).

Unit tests (rustfs/src/admin/auth.rs):
- Refactor the two `validate_admin_request*` entry points to share a
  single generic decision core `evaluate_admin_actions<S: Store>` /
  `check_admin_request_auth<S: Store>` (removes the duplicated
  action-loop and makes the gate testable without a running cluster).
- Cover: owner/root credential allowed; authenticated non-admin
  credential denied with AccessDenied; missing credential denied; and
  the multi-action loop grants on any permitted action, denies when
  none pass. Backed by an in-memory empty IamSys so the owner path
  short-circuits to allow and every other principal resolves to deny.

E2E (crates/e2e_test/src/admin_auth_test.rs, raw SigV4 over HTTP, no
awscurl dependency):
- non_admin_credential_denied_on_admin_api: a limited IAM user gets
  403 AccessDenied on GET /rustfs/admin/v3/info while root succeeds.
- root_credential_rotation_takes_effect: restart with rotated
  --access-key/--secret-key; new credential works and the stale one is
  rejected, on both the admin plane and the S3 data plane.
- default_credentials_emit_startup_warning: booting with the default
  rustfsadmin credentials emits the default-credentials warning.

Refs rustfs/backlog#1151 (sec-4), #1155.
2026-07-11 16:07:36 +08:00
houseme 2ebe8e561b fix(replication): allow loopback replication targets under an explicit test opt-in (#4725)
* fix(replication): allow loopback replication targets under an explicit test opt-in

Commit 5c7c757a3 (#4712) activated the previously-dormant replication e2e
suite (they had never run anywhere). All 9 fast tests then failed on main
because the SSRF egress guard rejects the 127.0.0.1 targets the e2e harness
configures: `target endpoint is not allowed: outbound URL host '127.0.0.1'
is not allowed: loopback address`. The whole harness runs on loopback, so
every replication test hit this before reaching its actual assertion.

Loopback is a genuine SSRF vector and must stay rejected in production, so
this does not relax the guard. Instead `validate_replication_target_endpoint`
gains an off-by-default opt-in (`RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET`)
that re-enables loopback targets (127.0.0.1 / ::1 / localhost) for single-host
multi-instance dev and the e2e harness. Private addresses stay unconditionally
allowed as before; the opt-in does not widen into link-local or the cloud
metadata endpoint. The e2e harness sets the env for every server it spawns
(single-node and cluster paths), overridable via extra_env.

Verified end-to-end: all 9 previously-failing replication_extension_test
smoke tests pass against a locally built binary. New unit tests in
bucket_target_sys pin the matrix — public/private always allowed, loopback
gated on the opt-in in both IP and hostname forms, and metadata/link-local
still rejected even with the opt-in on.

Refs: backlog#1147

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(replication): rename optin -> opt_in to satisfy typos check

Pure rename of three unit-test function names; no behaviour change.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 07:49:39 +00:00
Zhengchao An f63af3df63 chore: retire completed-migration scaffolding, wire orphaned boundary check (#4719)
The ecstore/global-state migrations are done (backlog#815, #939, #1052 all
closed). Review of every migration-era test/gate measure found three things
actually retirable or broken — everything else is a live anti-regression
guard and stays.

Remove:
- scripts/check_metrics_migration_refs.sh — guards a migration that
  finished: rustfs_metrics:: has zero hits, the metrics crate no longer
  exists, and the script was never wired into CI or make (only reference
  was one line in config-model-boundary-adr.md, also removed).
- crates/obs init_metrics_collectors — the "backward-compatible alias kept
  during migration" the removed script was guarding. Zero callers; pure
  delegate to init_metrics_runtime.

Archive (docs/superpowers/plans/, continuing the 2026-07 convention,
with the standard archived banner):
- startup-timeline.md, scheduler-baseline.md,
  profiling-numa-capability-inventory.md,
  kms-development-defaults-inventory.md — one-shot snapshots whose only
  consumer is the already-archived migration-progress ledger (their
  same-dir links there start resolving again after the move); zero script
  pins; fed the closed backlog#660/#665 architecture-review ledger.
  Fixed the one outbound link (startup-timeline -> readiness-matrix) that
  the move would have broken — check_doc_paths.sh deliberately does not
  scan plans/, so nothing else would have caught it.

Wire (found orphaned by the same review):
- scripts/check_extension_schema_boundaries.sh guards a live contract
  crate but was never invoked anywhere. Add lint-fmt.mak target, include
  in pre-commit/pre-pr/dev-check, add ci.yml Quick Checks step (job
  already installs ripgrep), sync the CONTRIBUTING.md enumerated list,
  and harden the script against a silently-passing rg probe when src/
  is missing.

Keep (verified live, documented so the next cleanup pass does not repeat
this analysis):
- scripts/check_architecture_migration_rules.sh — added a header stating
  it is a permanent boundary guard, not retirable migration scaffolding;
  'migration' in the name is historical.
- check_migration_gate_count.sh + floor, delete-marker e2e proof, all
  pinned docs, compat-cleanup-register sync, remaining inventories
  (referenced by live docs).

Verification: all 7 guard scripts pass, actionlint clean,
cargo check --workspace (excl e2e) clean, cargo fmt --check clean.
Adversarially reviewed by two independent skeptic passes; their 7
findings (alias left behind, broken outbound link, missing banners,
wrong backlog attribution, CONTRIBUTING drift, rg exit-2 hole, missing
header rationale) are all folded in.
2026-07-11 13:42:56 +08:00
Zhengchao An a97f3a9c52 fix(test): isolate health tests from minimal-response env var race (#4702)
Three health handler tests assert on payload fields (degradedReasons,
details) that are absent when RUSTFS_HEALTH_MINIMAL_RESPONSE_ENABLE is
true. The minimal-mode sibling test sets that env var via
temp_env::with_var, which leaks across parallel test threads.

Wrap the three affected tests with their own with_var guard pinning the
env var to false so they are deterministic regardless of thread order.
2026-07-11 13:39:34 +08:00
Zhengchao An 5650dcdc5d ci: pull replication tests out of e2e-smoke (loopback targets fail every PR) (#4724)
ci: pull replication tests out of e2e-smoke — loopback targets fail every PR

repl-1 (#4712) added 20 bucket-replication admin-path tests to the
e2e-smoke PR lane. They set a remote replication target at a loopback
endpoint (127.0.0.1, a second local server), which RustFS's target SSRF
guard rejects by default ('outbound URL host 127.0.0.1 is not allowed:
loopback address'). repl-1's local verification was inconclusive under
machine load, so this shipped broken and failed End-to-End Tests on
every PR based on post-repl-1 main (#4707 etc).

Move all replication e2e tests to the e2e-repl-nightly lane (now the full
replication_extension_test set, no allowlist split) to un-block PRs. The
nightly job needs loopback-target-allow server config for these to pass;
tracked as a repl-1 follow-up (backlog#1147). e2e-smoke returns to the
17 functional modules from ci-4 (63 tests).
2026-07-11 13:39:15 +08:00
houseme 9bf102f965 fix(cache): reserve admitted bytes in the memory gate to bound burst overshoot (#4718)
The fill gate compared each request against a snapshot refreshed at most every
5 s, with no accounting for what it had already let through. A burst arriving
while the snapshot still read high therefore all passed the same check-then-act
test and over-allocated far past the real headroom before the next refresh — a
gap the burst stress test could expose but not close.

Track admitted bytes since the last refresh in the shared snapshot cell and
subtract them from available memory in `allows_fill`, reserving the request's
size on each admission. Cumulative admission is now bounded to the real budget
even though every fill reads the same stale snapshot; the refresh resets the
counter because the fresh reading already reflects those allocations. The
`min_free_memory_percent == 0` opt-out still short-circuits first, and the
already-low-memory path is unchanged.

New test `moka_backend_gate_reservation_bounds_burst_under_stale_snapshot`:
20 concurrent 40 KiB fills against a 500 KiB stale-high snapshot (300 KiB
budget) admit only a bounded handful, not the whole storm. Passed 10/10 runs;
mutation-verified — dropping the reservation admits all 20 and fails the test.

Refs: backlog#1107

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 05:36:20 +00:00
Zhengchao An 05fae6f939 test(ecstore): add TierConfigMgr state-machine unit coverage (#4713)
* test(ecstore): unit-test TierConfigMgr add/edit/remove/verify state machine (backlog#1148 ilm-4)

Covers the tier config state machine and persistence paths that previously
had only 4 codec tests and none for tier_config.rs:

- add: non-uppercase name, duplicate name, unsupported type, missing
  backend payload, and a regression anchor documenting that AWS-reserved
  names (STANDARD) are not currently rejected.
- edit: unknown tier, missing-credentials rejection for RustFS and MinIO.
- remove: idempotent unknown-tier no-op, in-use rejection, empty-backend
  success, force skips the in_use probe, and probe-error surfacing.
- verify: unknown tier, healthy backend, unhealthy backend.
- pure query helpers (empty/is_tier_valid/tier_type/get/list_tiers).
- persistence: JSON marshal/unmarshal roundtrip, external tier-config.bin
  roundtrip for Azure and GCS payload mapping, truncated/unknown-format/
  unknown-version rejection, legacy v1 version-word acceptance, and encode
  failure on missing payload.

Tests are hermetic: error paths return before backend construction, and a
MockWarmBackend injected into driver_cache exercises remove/verify without
any real remote. Refs backlog#1155.

Co-authored-by: overtrue <anzhengchao@gmail.com>

* fix(tier): reject reserved names STANDARD/RRS in TierConfigMgr::add (backlog#1148 ilm-4) (#4721)
2026-07-11 05:31:30 +00:00
Zhengchao An 846aa95c32 test(security): GHSA-named regression tests for 3p3x and r5qv (backlog#1151 sec-6) (#4707)
test(security): add GHSA-named regression tests for 3p3x and r5qv (backlog#1151 sec-6)

Anchor the two fixed advisories to discoverable, named regression tests so
`rg -i "ghsa|3p3x|r5qv"` finds a guard for each, and future fixes are forced
to update pinned behavior (red -> green).

GHSA-3p3x-734c-h5vx (constant-time WebDAV/FTPS secret comparison, rustfs#4403):
- ftps_core.rs: new `assert_ftps_ghsa_3p3x_wrong_credentials_rejected` drives
  the `ct_eq` reject branch in FtpsAuthenticator::authenticate; asserts wrong
  password and unknown user are both rejected (530) and indistinguishable.
- webdav_core.rs: the auth-failure block now sends a valid access key with a
  wrong secret (exercising the `ct_eq` branch, not just the unknown-access-key
  path) plus an unknown user, asserting both 401 and indistinguishable.
- Module doc comments map advisory -> tests -> fix PR on both files.

GHSA-r5qv-rc46-hv8q (internode RPC fail-closed, rustfs#4402):
- http_auth.rs: renamed the default-fallback rejection test to
  `ghsa_r5qv_resolve_shared_secret_rejects_default_fallback` (and broadened it
  to cover default env secret + blank secrets), and added
  `ghsa_r5qv_verify_rpc_signature_fails_closed_on_missing_or_invalid_auth`
  pinning the exact advisory scenario (missing/forged/cross-URL signature is
  rejected; a correctly signed request still passes). File-level doc maps the
  advisory.

Docs: new docs/testing/security-regressions.md with the advisory -> test
mapping table and where each layer runs; linked from docs/testing/README.md.
sec-14 will formalize the written policy in AGENTS.md.

The unit-level ghsa_r5qv_* tests run in the default CI pass. The WebDAV/FTPS
e2e live in the protocols suite (fixed ports, --test-threads=1); they cannot
join the e2e-smoke profile and are wired into CI by sec-5.

Refs: rustfs/backlog#1151 (sec-6), rustfs/backlog#1155
2026-07-11 05:18:27 +00:00
Zhengchao An 4b83efaf36 ci(ilm): add gated s3-tests lane for lifecycle expiration cases (#4715)
* ci(ilm): move first batch of 5 s3-tests lifecycle expiration cases into a gated behavior lane (backlog#1148 ilm-10)

The 20 lifecycle cases in excluded_tests.txt were labeled "vendor-specific"
but the real blocker was the absence of a Ceph lc_debug_interval equivalent.
RUSTFS_ILM_DEBUG_DAY_SECS (ilm-5) now provides that, so Days>=1 expiration
behavior is testable in seconds.

These cases assert that objects/versions/uploads are actually removed by the
background scanner and the stale-multipart cleanup loop. They cannot join the
default single-server s3-implemented-tests gate: it disables the scanner, and a
global RUSTFS_ILM_DEBUG_DAY_SECS also shrinks the x-amz-expiration header that
the already-passing test_lifecycle_expiration_header_* cases assert on. So this
adds an isolated lane instead of putting them in implemented_tests.txt.

First batch (5), each verified against the pinned upstream source and the
RustFS evaluator/scanner/stale-multipart execution paths:
  - test_lifecycle_expiration          (prefix Days=1/Days=5, scanner delete)
  - test_lifecyclev2_expiration        (same via ListObjectsV2)
  - test_lifecycle_noncur_expiration   (NoncurrentVersionExpiration)
  - test_lifecycle_deletemarker_expiration (ExpiredObjectDeleteMarker cascade)
  - test_lifecycle_multipart_expiration    (AbortIncompleteMultipartUpload)

Dropped from the batch, kept excluded with reason:
  - test_lifecycle_expiration_days0: RustFS accepts Expiration{Days:0} (returns
    200; only Days<0 is rejected in crates/lifecycle/src/core.rs validate()),
    while AWS/this test expect InvalidArgument. Real validation gap.

Changes:
  - scripts/s3-tests/lifecycle_behavior_tests.txt: new exact-node-id run set.
  - scripts/s3-tests/run.sh: honor an IMPLEMENTED_TESTS_FILE override so a lane
    can point at an alternate whitelist.
  - .github/workflows/ci.yml: new s3-lifecycle-behavior-tests PR-gate job that
    starts rustfs with RUSTFS_ILM_DEBUG_DAY_SECS=10, scanner enabled (cycle 2s,
    no start delay) and a 2s stale-multipart cleanup interval, running the new
    list serially.
  - scripts/s3-tests/report_compat.py: recognize the behavior list so the weekly
    scope=all sweep (plain server) does not misclassify expected failures.
  - scripts/s3-tests/excluded_tests.txt: remove the 5 enabled cases; re-annotate
    the remaining 15 lifecycle exclusions with real reasons + batch-2 plan.
  - docs/architecture/s3-compatibility-matrix.md: sync counts (implemented 451,
    excluded 274, behavior 5) and document the lane.

Refs backlog#1148 (ilm-10), master plan backlog#1155.

* fix(lifecycle): reject zero-day expiration/noncurrent/abort rules (backlog#1148 ilm-10) (#4722)
2026-07-11 05:16:55 +00:00
houseme 4de73c0653 fix(docker): use non-default credentials in cluster compose/scripts (#4720) 2026-07-11 12:41:02 +08:00
Zhengchao An abe6c41227 test(e2e): negative header-SigV4 rejection suite (backlog#1151 sec-1) (#4708)
test(e2e): add negative header-SigV4 rejection suite (backlog#1151 sec-1)

RustFS delegates SigV4 verification to the s3s dependency, so nothing in
this repo pins OUR end-to-end wiring of it. Add negative_sigv4_test.rs
sending REJECTED header-SigV4 requests against a live RustFSTestEnvironment
and asserting the HTTP status plus the S3 error code XML:

- valid_header_sigv4_request_succeeds (positive control so the negatives
  cannot pass for the wrong reason)
- tampered_signature_returns_signature_does_not_match -> 403 SignatureDoesNotMatch
- wrong_secret_key_returns_signature_does_not_match -> 403 SignatureDoesNotMatch
- tampered_payload_is_rejected (body != signed x-amz-content-sha256) -> not 200
- skewed_date_returns_request_time_too_skewed (>15min) -> 403 RequestTimeTooSkewed
- malformed_authorization_header_returns_clean_4xx (present-not-missing) -> 4xx, never 5xx

Signatures are hand-built via rustfs_signer::request_signature_v4 primitives
(get_signing_key/get_signature/get_scope) so the test controls the timestamp,
secret, signed payload hash, and final signature bytes. Missing-credential
negatives are intentionally not duplicated (covered by multipart_auth_test /
anonymous_access_test).

Refs backlog#1151 (sec-1), master plan backlog#1155.
2026-07-11 03:28:56 +00:00
Zhengchao An ac646cfbe4 test(e2e): large-object degraded-read EOF truncation regression net (dist-13) (#4709)
Adds an S3-API-level e2e regression net proving that a large-object GET on a
degraded EC set never returns a silently truncated body under a full
Content-Length -- the historical "unexpected EOF" bug fixed on main by
rustfs#4594 (short-body GetObject stream -> UnexpectedEof), rustfs#4560
(in-place per-part legacy degradation for the lazy multipart reader), and
rustfs#4585 (DARE package-boundary truncation detection). Those fixes each
ship a *unit* regression; this covers the layer they do not -- the full HTTP
GET path streaming a real body reconstructed from real on-disk EC shards.

New file crates/e2e_test/src/degraded_read_eof_regression_test.rs, single-node
4-disk (EC 2+2) DiskFaultHarness, three scenarios:

  (a) one disk offline: a 6 MiB single object (>=2 EC stripes) and a 3x5 MiB
      multipart object GET back byte-identical with the correct Content-Length.
  (b) mid-stream bitrot within quorum: 2 of 4 shards corrupted mid-file on a
      large multipart object still reconstructs the full, hash-matching body.
  (c) beyond read quorum (the heart of the net): 3 of 4 shards corrupted
      mid-file -- the read MUST fail cleanly (non-2xx or a mid-stream body
      error), NEVER close with a truncated body under the full Content-Length.

The shared get_checked() helper panics on the forbidden outcome (a clean 2xx
whose collected body is shorter than the advertised Content-Length), so the
truncation bug can never be silently tolerated.

CI placement: these spawn a 4-disk server per test and are resource-heavy, so
they stay OUT of the fast PR e2e-smoke filter. A new e2e-reliability nextest
test-group (max-threads=1) serializes them (and the existing
reliability_disk_fault_test) across nextest's process boundary; ci-7's nightly
full-e2e run picks them up. Visible via `cargo nextest list -p e2e_test`.

Refs rustfs/backlog#1150 (dist-13), rustfs/backlog#1155, rustfs#4594,
rustfs#4560, rustfs#4585, rustfs#2955.
2026-07-11 02:49:48 +00:00
houseme d5d6f1160d test(object-data-cache): add concurrency stress tests and GET benchmarks (#4711)
* test(object-data-cache): add concurrency stress tests for the fill machinery

The race coverage so far pins specific interleavings with an injected barrier.
These add sustained, real-parallelism contention to catch what pinned tests
cannot — a leaked singleflight leader, a stranded semaphore permit, an
index/cache divergence that only surfaces under volume:

- moka_backend_concurrency_storm_leaves_no_leaked_state: 48 tasks x 400 mixed
  fill/lookup/invalidate ops on a 6-key pool over 4 worker threads, then
  asserts inflight_fills == 0 (no leaked in-flight fill), a clean post-storm
  fill/hit/invalidate sequence still works (state not corrupted), and clear()
  drains the cache.
- moka_backend_gate_bounds_admission_under_concurrent_burst: a low-memory
  snapshot must reject an entire 32-fill burst — admission is bounded, not a
  check-then-act race that lets the burst through. (This does not cover the
  separate 5s-stale-snapshot overshoot, which needs byte-based reservation.)

Both are mutation-verified: neutering invalidate_object fails the storm's
"invalidation must win" assertion, and making allows_fill always-true fails the
burst's full-rejection assertion. The storm passed 20/20 runs. `rt-multi-thread`
is added to dev-deps so the tasks run on real worker threads.

Refs: backlog#1107

Co-Authored-By: heihutu <heihutu@gmail.com>

* bench(object-data-cache): measure the GET-path cost the audit argued statically

Adds a criterion benchmark driving the public ObjectDataCache facade, so the
"hot path is clean / high throughput" claim rests on numbers, not analysis:

- plan_get      ~104-112 ns  (per-GET planning + metric label-set hash)
- lookup_hit    ~143-147 ns  (a resident-body hit: moka get + Bytes clone + metric)
- fill_distinct ~2.2-2.4 us  (cold fill: plan + singleflight + index + moka + inner spawn)

A hit at ~143 ns against a multi-millisecond erasure read is the ~1000x saving
the cache exists for; the per-GET metric cost is real but ~100 ns, not a
bottleneck; the fill cost (incl. the cancellation-safety spawn kept on purpose)
is negligible on a background task. Run with
`cargo bench -p rustfs-object-data-cache`.

Refs: backlog#1107

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 02:30:19 +00:00
Zhengchao An ce53feee87 test(lifecycle): regression test for expire/GET race (#3491) (#4706)
Adds a serial-lane ILM integration regression test that pins the
local-first ordering contract established by rustfs#3491, where
`expire_transitioned_object` deletes local metadata BEFORE any remote
tier cleanup so a concurrent GET can never observe live local metadata
pointing at an already-removed remote tier version (user-visible
`NoSuchVersion`).

`serial_tests::test_expire_transitioned_object_never_races_concurrent_get`
in `crates/scanner/tests/lifecycle_integration_test.rs`:

- Transitions an object to a mock warm tier, then runs a tight
  concurrent GET loop while calling `expire_transitioned_object`; every
  GET must return a full, correct body or a clean object/version-not-found
  -- never a tier-fetch failure.
- Deterministic ordering assertion (revert-proof): immediately after
  expiry the remote tier object is still present and the mock recorded
  zero remote `remove` calls, proving remote cleanup is deferred to
  free-version recovery. Reverting to remote-first ordering turns this
  assertion red.

Supporting changes:
- Re-export `expire_transitioned_object` from `api::bucket::lifecycle`.
- Record remote-tier mutating ops in the scanner test `MockWarmBackend`.
- Update tier-ilm-debugging.md to reference the new regression test.

Runs in the CI ILM Integration (serial) lane (ci.yml
test-ilm-integration-serial), picked up by the existing
binary(lifecycle_integration_test) filter.

Refs: rustfs/backlog#1148 (ilm-2), rustfs/backlog#1155
2026-07-11 01:53:24 +00:00
Zhengchao An 5c7c757a30 ci(repl): wire replication e2e suite into nextest profiles (backlog#1147 repl-1) (#4712)
Activate the 36 dormant replication e2e tests in
crates/e2e_test/src/replication_extension_test.rs (zero ran anywhere before).
Split via the ci-4 nextest profile mechanism, no hand-rolled cargo-test lane:

- PR smoke (profile.e2e-smoke, existing e2e-tests job): the 20 fast
  bucket-replication tests (target-registration / replication-check / list /
  remove / delete admin paths) that validate config synchronously and never
  wait for async convergence. Each spawns its own single-node rustfs server(s)
  on random ports with isolated temp dirs, so parallel-safe by construction
  (serial_test's #[serial] is a no-op under nextest's process-per-test model;
  no test-group needed).
- Nightly (profile.e2e-repl-nightly + .github/workflows/e2e-replication-nightly.yml):
  the remaining 16 = 6 slow data-plane tests + 9 _real_dual_node + 1
  _real_single_node. Defined as 'replication module MINUS the PR allowlist' so
  new replication tests default to nightly and are never silently unrun.

The nightly workflow builds the binary once, installs awscurl so the STS
dual-node test runs (skips gracefully with a visible log line otherwise), and
routes scheduled failures through .github/actions/schedule-failure-issue
(ci-8). Explicit division of labor with ci-5 e2e-full: these run only here.

Counts (cargo nextest list): e2e-smoke 83 (63 + 20), e2e-repl-nightly 16.
Docs updated: e2e-suite-inventory.md, e2e_test/README.md.

Refs backlog#1147 repl-1, backlog#1155.
2026-07-11 09:45:47 +08:00
Zhengchao An c41a813a31 docs(e2e): contributor guide for the e2e_test crate (backlog#1153 infra-10) (#4705)
docs(e2e): write contributor guide for e2e_test crate (backlog#1153 infra-10)

Turn the e2e_test README skeleton into a dense, link-heavy contributor guide:
crate overview + grouped module map, how to run (whole crate / one module /
e2e-smoke profile / ILM serial lane / protocols suite), #[ignore] semantics as
a pointer to live sources, how to add a test (RustFSTestEnvironment +
RustFSTestClusterEnvironment, common.rs/chaos.rs helper inventory, isolation
rules, #[serial]-vs-nextest reality), a CI subset map table, and a
troubleshooting section (stale binary / RUSTFS_TEST_PORT / orphan processes).

Keeps the ci-4 "CI smoke subset" section intact and restructures around it.
Adds a reciprocal link from crates/e2e_test/AGENTS.md.

Refs rustfs/backlog#1153 (infra-10), #1155.
2026-07-11 09:27:30 +08:00
Henry Guo 3f25426534 fix(ecstore): reject incomplete listing usage refreshes (#4698)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-11 08:47:49 +08:00
houseme 7437f99c45 fix(cache): key the object body cache on data_dir for write-uniqueness (#4703)
* refactor(object-data-cache): derive Default for ObjectDataCacheGetRequest

The GET request literal is hand-listed field-by-field across ~13 test sites in
two crates. Adding `mod_time_unix_nanos` in backlog#1111 had to touch every one
and still missed a literal, producing a compile error caught only in a later
CI lane. Derive `Default` and spread the engine-crate literals so the next
field addition is absorbed rather than fanned out.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): key the object body cache on data_dir for write-uniqueness

The cache key's correctness rested on being write-unique, but its only
write-scoped component was `mod_time` — a wall-clock timestamp that is not
monotonic and can be absent. Two writes that collide on MD5 (same etag+size)
and land on an equal mod_time (clock skew, same-tick, or absent) derived the
same key, so a node that never saw the overwrite could serve the previous body
for up to the TTL, and the same collision turned the fill-after-invalidation
race into a serving bug. This is the store's strong-read-after-write guarantee
leaning on a probabilistic argument.

Add `data_dir` — the xl.meta directory UUID ecstore regenerates on every body
write — as the primary write-unique anchor:
- surface `data_dir: Option<Uuid>` on ObjectInfo, copied from FileInfo in the
  single GET-path constructor `from_file_info`;
- carry it into ObjectDataCacheKey as `data_dir_u128` (held as u128 to keep the
  engine crate free of a uuid dependency), derived in the one planner site both
  the ecstore hook and the usecase layer share, so both produce an identical
  key by construction;
- keep `mod_time` as a second anchor and `etag+size` as belt-and-braces; an
  absent data_dir falls back to the prior behavior — strict improvement, no
  regression.

Two writes distinct only by data_dir now derive different keys even under an
MD5 collision with identical mod_time — the case mod_time alone cannot cover.

Blast radius is compiler-guarded: ObjectInfo derives Default and every real
construction site uses `..Default::default()`, so only from_file_info and one
full-literal test needed the field. The three P0 body_cache_hook_e2e
regressions, engine (80), and app (36) suites pass unchanged; a mutation that
severs the planner wiring fails planner_key_changes_with_data_dir.

Refs: backlog#1111, backlog#1118

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): thread data_dir field through the merged mutation-hook test

Merging main (which landed the object-mutation-hook work, backlog#1131) brought
in a GetRequest test literal that predates the data_dir field. Spread it via
`..Default::default()` — the derive(Default) added here means this is the last
such hand-listed literal to need touching.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 08:00:52 +08:00
Zhengchao An a35ebb92b8 fix(test): add missing mod_time_unix_nanos field in mutation_hook test helper
The recent addition of ObjectDataCacheGetRequest.mod_time_unix_nanos
(backlog#1111 / ODC-06) missed the test helper in mutation_hook.rs,
causing a compile error under clippy --all-targets.
2026-07-11 07:01:56 +08:00
Zhengchao An a32af463ed ci: quarantine walk_dir stall-budget flake under the ci profile (#4691)
ci: quarantine walk_dir stall-budget flake under the ci profile (rustfs#4690)

First application of the flake policy from docs/testing/README.md: the
test failed on a zero-Rust-diff PR (#4674, run 29099382470) — timing
windows in the stall-budget accounting stretch under CI load. retries=2
under profile.ci only; local default profile still never retries.
Tracked by rustfs#4690 (30-day fix-or-delete deadline).
2026-07-11 04:10:32 +08:00
houseme 85fd824581 feat(object-data-cache): close write-side invalidation gaps and add an admin surface (#4694)
* feat(object-data-cache): close write/delete-side invalidation gaps

The object data cache exposed only a single per-(bucket,object)
invalidation primitive and no write-side ecstore hook, so several
delete paths left dead bodies resident until TTL (hygiene/capacity, not
stale-serving: lookups follow a fresh metadata quorum and cannot serve a
gone object). This adds the missing primitives and wires them in.

ODC-26 (backlog#1131): add an `ObjectMutationHook` trait beside the GET
body hook, registered next to it at startup, and call it from the
ecstore-internal delete paths (`apply_expiry_on_non_transitioned_objects`,
`expire_transitioned_object` including the restored-copy branch, and
`delete_object_versions`). The app impl is one `invalidate_object` call
under a new `AfterLifecycleExpiry` reason.

ODC-27 (backlog#1132): force prefix delete now invalidates the whole
prefix, not just the prefix string. `store.delete_object(delete_prefix)`
returns no deleted-name list, so this uses a new prefix primitive rather
than the batch path.

ODC-28 (backlog#1133): DeleteBucket now flushes the bucket via a new
bucket-scope primitive (covers force and non-force, which share the
delete_bucket call).

ODC-C2 (backlog#1143): add `ObjectDataCache::clear()` and two admin
handlers (GET stats, POST flush) routed through admin runtime_sources.

The starshard identity index gains a single `remove_matching` full-scan
API backing prefix/bucket/clear; it is documented as admin/delete-path
only and never runs on the GET or fill hot path. New invalidation
reasons and metric labels added; outcome (removed/noop) labelling kept
correct for every new primitive.

Also fixes a pre-existing broken intra-doc link in memory.rs.

Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor(ecstore): extract the shared HookSlot behind both cache hooks

This PR introduced object_mutation_hook.rs by mirroring body_cache_hook.rs,
which left two process-global registration slots whose register/get/clear
bodies were line-for-line identical except the trait type and the WARN string:
a RwLock<Option<Arc<dyn _>>>, an Arc::ptr_eq "different instance" warning, the
poison-recovery closure, and the same read-lock-and-clone read. Two copies of
the same swap-vs-warn logic can drift apart under maintenance.

Hoist it into a generic HookSlot<T: ?Sized> that owns the logic once. Each hook
module keeps its `static HOOK: HookSlot<dyn XxxHook>` and its thin, unchanged
public wrappers (register_/get_/clear_), so the crate's public surface and
every call site are untouched — this is an internal consolidation, not a
contract change.

The load-bearing #1126 guarantee (newest registration wins, so a rebuilt
AppContext is never stranded on a first-wins slot) previously had no direct
test — the hook tests only covered register-then-notify. HookSlot now has its
own unit tests including re_registration_swaps_to_the_latest_instance;
mutation-testing confirms a first-wins regression fails exactly that test.

No behavior change: the two hooks' existing tests, the P0 body_cache_hook_e2e
regressions, and the app-layer mutation-hook tests all pass unchanged.

Refs: backlog#1126, backlog#1131

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(admin): register the object-data-cache routes in the policy inventory

This PR added GET /object-data-cache/stats and POST /object-data-cache/flush
but did not list them in the two registries that must account for every
admin route: the route-policy inventory (route_policy.rs) and the route
matrix (route_registration_test.rs). Their coverage tests —
route_policy_inventory_covers_registered_routes and
test_admin_route_matrix_matches_registered_routes — failed on CI because a
registered route had no policy/matrix entry.

These two tests are not part of `make pre-commit` (which runs fmt + arch +
quick-check, not the full suite), so the gap passed local pre-commit and
only surfaced in the CI Test-and-Lint lane.

stats is a read (ServerInfoAdminAction, Sensitive); flush mutates
(ConfigUpdateAdminAction, High) — matching the actions the handlers already
enforce. The MinIO-alias matrix test is unaffected: these are native rustfs
endpoints with no MinIO equivalent.

Refs: backlog#1143

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 20:04:05 +00:00
houseme f9874b591a [DO NOT MERGE until published] chore(ecstore): switch rustfs-uring to crates.io 0.1.0 (#4701)
* chore(ecstore): switch rustfs-uring from the git rev to crates.io 0.1.0

Prepared ahead of the rustfs-uring 0.1.0 crates.io release (rustfs/uring):
flip ecstore's dependency from the pinned git revision to the published
`0.1.0`, and drop the now-unneeded git-source allowance in deny.toml.

DO NOT MERGE until rustfs-uring 0.1.0 is on crates.io. Until then cargo
cannot resolve `rustfs-uring = "0.1.0"`, so this branch will not build and
CI will be red — that is expected, not a defect in the change.

After the crate is published:
  1. `cargo update -p rustfs-uring --precise 0.1.0` to regenerate Cargo.lock
     (deliberately not touched here — the registry entry, with its checksum,
     cannot be written before the crate exists);
  2. push the Cargo.lock;
  3. CI goes green; merge.

No code change. The guard `scripts/check_no_tokio_io_uring.sh` is unaffected
(it bans only tokio's io-uring runtime feature, not an explicit rustfs-uring
dependency). `audit.yml`'s `allow-dependencies-licenses` for rustfs-uring is
left in place — it is a first-party Apache-2.0 crate either way.

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore: refresh rustfs-uring lockfile

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 19:53:23 +00:00
houseme 48bdf87e53 refactor(obs): drop the dial9 S3 config left stranded by the upload revert (#4700)
#4663 added a full S3 upload path (config fields + env parsing + wiring), then
reverted the wiring when its dependency turned out to carry RUSTSEC advisories
(D9-14). The revert stopped at enabled.rs and left the config layer half torn
down: `Dial9Config` still carried `s3_bucket`/`s3_prefix` with no consumer, plus
a dead `s3_upload_requested()`, and `Dial9SessionGuard::config()` was likewise
never called. All three are zero-consumer dead code.

Since S3 upload cannot be built at all, the config type should not model it.
Drop the two fields and both accessors. `from_env` still reads the S3 env vars
and warns about them (an operator who set them deserves to know why they do
nothing — that warning is deliberate and stays), but drops the values instead
of storing configuration nothing reads.

While here, collapse `from_env`'s two `record_config` calls into one by
extracting `from_env_enabled`, so the enabled/disabled paths share a single
publish point.

No behavior change: the S3 warning fires identically, and every other field is
untouched. Net -13 lines, and `Dial9Config` drops from 8 fields to 6.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 19:44:07 +00:00
houseme 95819cb986 docs(object-data-cache): state the correctness boundary and timing channel (#4695)
docs(object-data-cache): state the correctness boundary and the timing channel

The crate doc still described itself as "a minimal skeleton for the initial
rollout phase", which stopped being true several releases ago, and neither
the crate nor the operator-facing env constant said anything about what the
cache does or does not guarantee.

Record two things a reader has to know.

The correctness boundary: a hit is sound because the key matched metadata the
caller just resolved from a read quorum, not because invalidation ran.
Process-local invalidation is hygiene that frees capacity; the key is what
keeps a stale body from being served. Anyone tempted to weaken the key should
meet this sentence first.

The timing side channel: a hit skips the erasure read, bitrot verify and
decode, so it is reliably faster than a miss. A principal authorized to read
an object can time a single GET and learn whether someone read that object
within the entry's lifetime. This crosses no authorization boundary — the
probe needs read access to that exact object, checked before the cache is
consulted — but it does disclose a co-tenant's recent access pattern. Say so
where operators look: on RUSTFS_OBJECT_DATA_CACHE_ENABLE. Timing noise would
cost precisely the latency the cache exists to save, so the mitigation is to
leave the cache off for buckets where access-pattern confidentiality matters.

Refs: backlog#1139

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-11 02:37:34 +08:00
houseme 6780140318 fix(object-data-cache): make the GET key write-unique and dedup the lookup (#4693)
fix(object-data-cache): make GET body cache key write-unique and dedup lookups

Address four object-data-cache GET-path findings (backlog#1107 batch):

ODC-06 (backlog#1111): the cache key was content-unique, not write-unique.
Extend ObjectDataCacheKey with the resolved version's modification time
(i128 unix nanoseconds, None -> 0), derived once in the shared planner so the
ecstore hook and the usecase layer produce an identical key. An unversioned
overwrite advances mod_time, so a stale node can no longer serve old bytes for
up to the TTL under an MD5 collision; etag + size stay as belt-and-braces.

ODC-16 (backlog#1121): every cacheable GET planned and looked up twice (once in
the ecstore hook, once in the usecase layer), double-counting hits, hit_bytes
and lookups. GetObjectReader now carries a GetObjectBodySource marker
(Unprobed / HookMissed / HookServed); the hook stamps it, and
build_get_object_body_with_cache serves a hook-served body directly and skips
its lookup whenever the hook already probed. One hook-served GET now records
exactly one lookup.

ODC-19 (backlog#1124): ENABLE=true with no explicit mode defaulted to HitOnly,
which never fills and keeps a permanent 0% hit rate. Default to
FillBufferedOnly, log the resolved mode at startup, and warn when HitOnly is
selected explicitly.

ODC-24 (backlog#1129): max_entry_bytes above the in-memory GET fill limits was
silently inert. Clamp the planner's size eligibility to
min(max_entry_bytes, seek-support threshold, 64 MiB buffer cap) so ineligible
sizes plan SkipTooLarge instead of being reported eligible, and warn at startup
when the excess is inert.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 18:22:20 +00:00
Zhengchao An d0ca14d8df test(ecstore): cover post-commit hard-crash in rename_data crash harness (#4689)
The rename_data crash-consistency harness (backlog#935/#878) only armed
pre-commit crash points (AfterDataRename, AfterBackupBeforeMetaCommit),
which assert the *old* version survives. The other half of the
"partial commit -> old or new, never mixed" invariant — a hard power loss
*after* the xl.meta commit rename must leave the *new* version — had no
crash-point coverage. The existing after-metadata-commit failpoint only
exercises the graceful in-process rollback (restores old), a different
path from a hard crash (no rollback runs, commit stays on disk).

Add a RenameDataCrashPoint::AfterMetaCommit injection right after the
commit rename (cfg(test), compiles to a const-false no-op in production,
like the existing points) and overwrite/fresh x strict/relaxed tests
asserting the object reads back as the new version. The fresh case pins
that the point is genuinely post-commit: a pre-commit misplacement would
leave no readable object and fail the assertion.

cargo test -p rustfs-ecstore crash_consistency: 9 passed. clippy -D
warnings clean; fmt and arch guards clean.
2026-07-11 02:21:07 +08:00
houseme d715cb5c34 refactor(ecstore): single-source the bitrot read/verify path (backlog#1159) (#4697)
P-A (`read_appending`) and P-C (the in-memory fast path) each copied the
hashed-read logic, so `BitrotReader` ended up with the hash verification, the
short-read error, and the scratch-buffer fill written three times across
`read` and `read_appending`'s two branches. That is patch-on-patch: a change
to the bitrot contract would have to be made in three places and kept in
sync by hand.

Collapse the duplication onto three single-source pieces:
- `split_and_verify` — a free function that splits `[hash][data]`, verifies
  (unless skip_verify), and returns the data slice plus the hash time. Free
  rather than a method so it can run while `self` is borrowed for the block.
- `read_scratch_block` — the single-pass fill of `self.buf` with the
  short-read-to-UnexpectedEof contract.
- `short_shard_read` / `begin_read` — the shared error and preamble.

`read` and `read_appending` now differ only in what they must: how the block
is acquired (caller's slice vs `try_take_block` vs scratch fill) and where the
verified shard lands (`copy_from_slice` vs `extend_from_slice`).

This also fixes a latent inconsistency the duplication hid: the old `read`
did `copy_from_slice` *before* verifying, so on a hash mismatch it left the
corrupt bytes in the caller's buffer before returning the error, while
`read_appending` verified first. Both now verify before writing, so a shard
that fails the hash never reaches the caller's buffer on either method — the
stronger of the two behaviors.

Net -19 lines; behavior otherwise unchanged. Verified: `erasure::` 215
passed, 0 failed (including the fast-path equivalence and corrupt-shard tests,
and the existing `test_bitrot_read_hash_mismatch`); clippy --all-targets
-D warnings clean.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 18:08:07 +00:00
houseme 60ad15a7f9 fix(obs): remove the dial9 task-dump switch that could never work; correct measured claims (#4688)
* fix(obs): remove the dial9 task-dump switch that could never do anything

Measured on a bench host (Linux x86_64) against the code merged in #4663: with
`RUSTFS_RUNTIME_DIAL9_TASK_DUMP_ENABLED=true`, a `dial9-taskdump` build, and
`--cfg tokio_taskdump`, dial9 recorded **zero** TaskDump events.

dial9 captures a task dump only for futures it wrapped itself — those spawned
through `dial9_tokio_telemetry::spawn`, which is where `TaskDumped<F>` gets
applied. `tokio::spawn` gets no wrapper, and RustFS spawns with `tokio::spawn`
throughout. Same workload, same binary, only the spawner changed:

    tokio::spawn   ->      0 dumps
    dial9::spawn   ->  14709 dumps, all with callchains

Upstream documents this (README line 151) and tracks the doc gap at
dial9-rs/dial9#477. I did not read it before wiring `with_task_dumps` in #4663,
and so shipped exactly the kind of lying configuration knob that PR set out to
delete. Remove it: the two environment variables, the config fields, the
`with_task_dumps` call, and the `dial9-taskdump` feature — whose only effect was
to constrain the build to Linux while recording nothing.

Re-adding it only makes sense together with migrating the paths under
investigation to dial9's spawner. Tracked as D9-16 in rustfs/backlog#1157.

Also drop the `--cfg tokio_taskdump` requirement from the Makefile. Measured:
dumps are captured with and without it (14709 vs 14674, within noise), and
upstream never asked for it. That requirement was mine, invented and untested.

Cargo.lock loses tokio's `backtrace` dependency, which `tokio/taskdump` pulled in.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(obs): replace guessed dial9 retention numbers with measured ones

Three corrections, all to claims I wrote in #4663 without measuring them.

"Under a high poll rate that budget can wrap in minutes" was a guess. Measured on
a single-node 4-drive cluster under warp mixed (66 MiB/s, 110 obj/s, 32 concurrent):
13023 events/s, 0.16 MiB/s, so the default 1 GiB budget wraps after roughly 108
minutes. Even at ten times the throughput that is ~11 minutes. State the measured
rate and how to scale it instead.

dial9 was described as the tool for drive stalls. It is not. RustFS does disk I/O
on the blocking pool and through io_uring, never on an async worker, so a slow
drive never lengthens a poll. Injecting 200 ms of latency on one of four drives
cut throughput by 64% and left the poll distribution unchanged (polls >= 5 ms:
49 -> 56; p999: 2.67 ms -> 2.75 ms). Enabling dial9's CPU and sched profilers
does not help: sched events are per-worker only, and the CPU profiler samples
on-CPU while a stalled drive is an off-CPU wait. Say so plainly, and point at
the `rustfs_io_*` metrics instead.

What dial9 *is* good for, on the same traces: single polls of 418-625 ms with no
fault injected at all — real worker stalls nothing else in the obs stack surfaces.
Lead with that.

Also link the two upstream issues filed for the gaps we documented:
dial9-rs/dial9#658 (writer death unobservable) and #659 (worker-s3 CVEs).

Measurements: rustfs/backlog#1157 (D9-11, D9-13, D9-18).

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 17:34:59 +00:00
houseme 6f05a740b3 refactor(ecstore): extract the shared io_uring read preamble (backlog#1145) (#4696)
`pread_uring` and `pread_uring_direct` accumulated across seven rounds
(#4632/#4635/#4645/#4649/#4653/#4658/#4662) and each carried its own copy of
the same open preamble: resolve the bucket path, run the volume access check,
resolve the object path, and check the path length. The only real difference
is what happens after — one opens buffered and errors as `DiskError`, the
other opens `O_DIRECT` and errors as `DirectOpenError` so it can latch an
O_DIRECT refusal.

Extract that shared sequence into `resolve_uring_object_path`, a blocking
helper called inside both `spawn_blocking` closures. Each caller keeps exactly
what differs — its open flags, its error type, its metadata/bounds/align
handling — and no longer restates the resolution. No behavior change: the same
checks run in the same order and produce the same errors (the O_DIRECT path
still maps them through `DirectOpenError::Disk`).

Net -12 lines. Verified on a real Linux host (16-core, real io_uring):
clippy -p rustfs-ecstore --all-targets -D warnings clean; disk::local tests
144 passed, 0 failed — including the io_uring, O_DIRECT, fd-cache, and
page-cache-reclaim cases that exercise both read paths.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 17:03:34 +00:00
Zhengchao An ca1463a6c5 ci: run e2e smoke subset via nextest e2e-smoke profile (#4674)
ci: run e2e smoke subset via nextest e2e-smoke profile (backlog#1149 ci-4)

The e2e-tests job previously ran a single test (delete_marker_migration
_semantics) out of ~400 in the e2e_test crate. Define a nextest
profile.e2e-smoke whose default-filter selects 17 fast single-node
dependency-free modules (63 tests) and run it in the job, reusing the
downloaded debug binary. The profile is the single wiring mechanism for
e2e tests in CI; admission criteria live in crates/e2e_test/README.md,
and docs/testing/e2e-suite-inventory.md records authoritative per-module
counts from cargo nextest list.
2026-07-11 01:03:03 +08:00
Zhengchao An 3169982623 fix(make): align clippy-check with CI to fix macOS pre-pr gate (#4692)
CI runs `cargo clippy --all-targets -- -D warnings` without
`--all-features`, but the Makefile's clippy-check used `--all-features`.
After PR #4663 made the dial9 telemetry feature opt-in and removed
`--cfg tokio_unstable` from .cargo/config.toml, `--all-features`
activated dial9 (which requires tokio_unstable) and dial9-taskdump
(which requires tokio/taskdump, Linux-only), breaking local
`make pre-pr` on macOS.

Align the Makefile with CI by dropping `--all-features` from clippy-check.
2026-07-10 16:23:59 +00:00
houseme c2362bca14 perf(ecstore): slice in-memory shards instead of copying them twice (#4687)
* perf(ecstore): slice in-memory shards instead of copying them twice (backlog#1159)

The GET path reads a shard out of the page cache into a `Bytes`, then
`open_disk_reader` erased it behind `Box<dyn AsyncRead>` by wrapping it in
a `Cursor`. Downstream, `BitrotReader` could only get it back by copying:
once out of the `Cursor` into its scratch buffer, and once from there into
the caller's buffer. CPU profiling of a cached 1 MiB GET (device reads = 0)
attributed 8.23% of the whole server to `Cursor::poll_read` alone — a copy
of data that was already sitting in memory.

Keep the source concrete instead of erasing it. `ShardReader` is an enum of
`InMemory(Cursor<Bytes>)` and `Stream(Box<dyn AsyncRead ...>)`, and the new
`ShardSource::try_take_block(n)` lets an in-memory source hand over the
`[hash][data]` block as a slice. `read_appending` uses it to verify the hash
on the slice and `extend_from_slice` the shard straight into the caller's
buffer: one copy instead of two.

`try_take_block` defaults to `None`, so a streaming source keeps the old
path byte for byte, along with its short-read and EOF semantics. A source
that cannot serve `n` bytes declines rather than truncating, so a partial
block still becomes UnexpectedEof rather than a short shard. The hash is
still checked before anything is appended, so a corrupt shard never reaches
the caller's buffer on either path. The deferred parity reader opens its
source lazily and stays on the streaming path; parity is only read when a
data shard fails.

Tests gate equivalence and non-vacuity:
  * `try_take_block` fires for `Cursor<Bytes>`, advances the position exactly
    as a read of the same length would, declines when fewer than `n` bytes
    remain, and returns `None` for a non-`Bytes` source — without this the
    equivalence test below would silently compare one path against itself;
  * both paths return identical bytes for the same shard;
  * a corrupt shard fails on the fast path too, appending nothing.

Verified: clippy --tests -D warnings clean; `erasure::` 215 passed, 0 failed;
`set_disk::core::io_primitives` 49 and `io_support::` 22 pass.

Stacked on #4681 (`read_appending`), which this builds on.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): implement ShardSource for Cursor<&[u8]> used by the erasure bench

`crates/ecstore/benches/erasure_benchmark.rs` builds
`BitrotReader<Cursor<&[u8]>>`, which the new `ShardSource` bound on
`ParallelReader`/`decode` does not accept. `cargo clippy --tests` does not
compile bench targets, so this only surfaced in CI's `--all-targets` run.

A borrowed slice carries no `Bytes` to hand out, so it takes the default
`try_take_block` and keeps the old streaming copy path — no behavior change.

Verified with the same target set CI uses:
`cargo clippy -p rustfs-ecstore --all-targets -- -D warnings` clean.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 15:57:10 +00:00
Zhengchao An b9e5e818a9 ci: fix migration-gate count guard failing on every PR (nextest JSON counting) (#4686)
ci: count migration-gate tests via nextest JSON, not human-format indentation

The count-floor guard (#4673) parsed nextest's human list format with
grep -c '^    '. CI's newer nextest emits a different indentation, so the
count collapsed to 0 and the gate failed on every PR based on current
main (first observed on #4683, then #4674/#4677/#4680). Count via
--message-format json + jq instead, which is stable across versions, and
fail loudly if the JSON shape ever changes.
2026-07-10 22:19:17 +08:00
Zhengchao An f13e98eb81 fix(perf): keep build_ref_binary stdout clean and fail fast on missing baseline (#4683)
git worktree add prints 'HEAD is now at …' on stdout, which polluted the
path captured from build_ref_binary and made the rig exec a two-line
string as the baseline binary (dispatch run 29087503110 died 1s after
spawn with 'No such file or directory'). Route command output to stderr,
verify the baseline binary exists before returning (exempt in dry-run),
and fix the 'mis-reports' typo flagged by the Typos check.
2026-07-10 21:58:00 +08:00
Zhengchao An 13f8768e7f feat(ecstore): make replication timing intervals env-overridable for tests (#4680)
Introduce RUSTFS_REPL_HEALTH_CHECK_INTERVAL_MS, RUSTFS_REPL_MRF_FLUSH_INTERVAL_MS
and RUSTFS_REPL_RESYNC_POLL_MAX_MS so tests and operators can shorten the
replication background loops. Defaults are unchanged; invalid values fall
back with a warn and values below 10ms are clamped to avoid busy-spin.
Add replication_fast_env() e2e helper (backlog#1147 repl-4).
2026-07-10 21:57:43 +08:00
Zhengchao An 40875026df feat(ecstore): add RUSTFS_ILM_DEBUG_DAY_SECS lifecycle time-acceleration for tests (#4677)
feat(ecstore): add RUSTFS_ILM_DEBUG_DAY_SECS lifecycle time-acceleration for tests (backlog#1148 ilm-5)

Model a Ceph rgw_lc_debug_interval-style switch: when RUSTFS_ILM_DEBUG_DAY_SECS
is set to a positive integer N, lifecycle Days-based rule evaluation treats one
'day' as N seconds instead of 86400, so Days>=1 expiration/transition/noncurrent
rules become exercisable in seconds. Unset => byte-identical to production.

All Days->deadline conversions funnel through expected_expiry_time(); the new
ilm_day_secs() helper (OnceLock-cached in production, env-read under cfg(test))
rescales both the day offset and the default rounding boundary. An explicit
RUSTFS_ILM_PROCESS_TIME still wins for the rounding boundary. Absolute Date-based
rules are deliberately NOT rescaled. Activation emits a WARN; test/debug only.

Refs rustfs/backlog#1148 (ilm-5), rustfs/backlog#1155.
2026-07-10 21:57:19 +08:00
Zhengchao An 6fed0a4067 docs: fix 'mis-reports' typo breaking the Typos check on main (#4685)
docs: fix 'mis-reports' typo failing the Typos check on every PR

Merged via #4669 into docs/operations/hotpath-warp-ab-runbook.md:132; the
Typos CI check now fails on all PRs based on current main.
2026-07-10 21:06:57 +08:00
Zhengchao An 515e14cd42 test(ilm): activate ignored ILM integration tests via serial CI lane (#4682)
Implements ilm-1 from the ILM test-strategy plan: the 33 #[ignore]d ILM
integration tests (14 in crates/scanner/tests/lifecycle_integration_test.rs,
19 in rustfs/src/app/lifecycle_transition_api_test.rs) never ran anywhere.
This adds a dedicated serialized CI job and repairs the app-layer suite,
activating 29 of them.

- ci.yml: new test-ilm-integration-serial job running the two ILM test
  surfaces with 'cargo nextest run -j1 --run-ignored ignored-only'. The
  tests share process-global singletons and fixed ports (9002/9003);
  serial_test's #[serial] does not serialize across nextest's
  process-per-test boundary, so -j1 is the serialization mechanism.
- app suite repair: the 19 app tests rotted after the InstanceContext
  migration (usecases resolve the store via AppContext; the test env never
  installed one, so every store-touching call failed with InternalError
  'Not init'). Add a test-only install_test_app_context helper behind the
  sanctioned context/runtime_sources boundaries and switch the tests from
  without_context() to from_global(). All 19 now pass.
- delete test_lifecycle_transition_basic (+3 orphaned helpers): required
  an external MinIO at localhost:9000 and slept 1200s; superseded by the
  mock-tier tests in the same file.
- fix a timing flake: poll free-version metadata removal instead of
  asserting a single read right after remote-object absence.
- 3 scanner tests fail on main for product reasons (multipart restore
  UnexpectedEof; noncurrent transition/expiry after immediate compensation
  transition on versioned buckets); they keep #[ignore] with a backlog
  reference and are excluded from the lane by name.

Lane: 29 tests, ~43s test time, green twice locally.

Refs rustfs/backlog#1148 (ilm-1), rustfs/backlog#1155.
2026-07-10 21:01:37 +08:00
Zhengchao An 12c44abce0 ci: add count-floor guard to migration-critical test gate (#4673)
The migration-proof step in ci.yml selects tests by name substring
(data_movement / rebalance / decommission / source_cleanup /
delete_marker), so a rename can silently thin the gate to zero with no
CI signal. Move the filter expression into
scripts/check_migration_gate_count.sh as its single source of truth:
the script counts the selected tests with cargo nextest list, fails if
the count drops below the committed floor in
.config/migration-gate-floor.txt, and then runs the gate with the same
filter so the check and the run cannot drift.

The floor equals the exact selected-test count at landing (571), so any
reduction fails CI and intentional shrinks must edit the floor file in
the same PR, keeping gate changes visible in diffs.

Refs rustfs/backlog#1153 (infra-12), rustfs/backlog#1155.

Signed-off-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-10 20:55:52 +08:00
Zhengchao An 173e5ddc06 build(make): require cargo-nextest for make test with explicit escape hatch (#4672)
build(make): require cargo-nextest for make test with explicit escape hatch (backlog#1153 infra-14)

cargo-nextest changes test semantics — it runs each test in its own process
(so serial_test's #[serial] mutex does not serialize across tests) and is the
only runner that honours .config/nextest.toml [test-groups] (e.g. the
ecstore-serial-flaky serialization guard). CI installs and runs nextest, but
`make test` only warned when it was missing and then silently fell back to
`cargo test`, which runs with different serialization behaviour than CI and can
mask or invent flakes.

Make cargo-nextest a hard dependency of `make test`:

- Missing nextest now fails `make test` with a clear message and install
  instructions (`cargo install cargo-nextest --locked` or a prebuilt binary
  via https://nexte.st/docs/installation/).
- Set RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 to opt into the plain `cargo test`
  fallback anyway; it prints a loud warning that serialization semantics differ
  from CI and .config/nextest.toml test-groups will NOT apply.
- The check stays cheap (command -v). The warn-only test-deps prerequisite is
  dropped from check.mak in favour of recipe-level gating.

Install docs live in the make-layer error message plus a header comment in
tests.mak, with a single-line note in CONTRIBUTING.md (kept minimal to avoid
conflicting with #4667 / infra-9, which rewrites the verification sections).

No CI change: .github/workflows/ci.yml already runs cargo nextest and
.github/actions/setup/action.yml already installs it.

Refs rustfs/backlog#1153 (infra-14), master plan #1155.

Signed-off-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-10 20:54:40 +08:00
Zhengchao An 4310b55238 ci: add schedule-failure-issue composite action and wire nightly alerts (#4671)
Scheduled workflow failures previously went unnoticed (15 consecutive red
weekly s3-tests sweeps, silent perf nightly failures). Add a reusable
composite action that opens a tracking issue titled
"[scheduled-failure] <workflow name>" — or appends a comment to the
existing open one (dedupe key = workflow name) — with the run URL, run
attempt, and failed job names, labeled "infrastructure".

Wire it into the scheduled paths of e2e-s3tests, mint, fuzz (nightly) and
performance-ab via a final alert-on-failure job gated by
"always() && github.event_name == 'schedule' &&
contains(needs.*.result, 'failure')", with issues:write scoped to that
job only. e2e-s3tests and mint previously had no permissions key; they now
get workflow-level contents:read, reducing every other job's token.

Add a dispatch-only drill workflow that forces a job failure and runs the
action through the exact consumer wiring, for end-to-end verification.
The ci-7 e2e nightly does not exist yet; it is recorded as a pending
consumer in the action README.

Refs: rustfs/backlog#1149 (ci-8), rustfs/backlog#1155
2026-07-10 20:53:44 +08:00
Zhengchao An fd18b1df49 ci(perf): fix nightly warp A/B startup failure, make it diagnosable, add small-object + 10MiB cells (#4669)
Both nightly runs of the warp A/B rig failed at server bring-up:
`endpoint http://127.0.0.1:9000/health did not become healthy` — exactly 60s
after start. The server binds its public listener only after startup converges
(erasure-format load + IAM); its own budget
(RUSTFS_STARTUP_READINESS_MAX_WAIT_SECS) defaults to 120s, so the rig's 60s
health poll gave up before a slow cold start on the shared sm-standard-2 runner
finished. The rustfs.log lived under /tmp (not the uploaded target/hotpath-ab/),
so the root cause was invisible in the artifact.

Root-cause + diagnosability (scripts/run_hotpath_warp_ab.sh):
- Health poll is configurable via --health-timeout, default 180s (> the 120s
  server startup budget). Fails fast if the local server process exits before
  becoming healthy instead of polling out the full window.
- Server logs + a startup-env file now write under OUT_DIR/server-logs/ so they
  ride along in the CI artifact. On a health failure the rig dumps the last 50
  log lines to the job log.

Workflow (.github/workflows/performance-ab.yml):
- On every run, write gate.md (or, on a pre-gate failure, the failing phase's
  server-log tails) into $GITHUB_STEP_SUMMARY; short artifact retention.
- Short measurement matrix (--duration/--rounds/--cooldown) so the expanded
  matrix fits; timeout-minutes 90 -> 120 as a Phase-0 stopgap until perf-3
  caches the baseline binary (the ~65min double build dominates).
- TODO(ci-8/perf-2) hook comment for the failure-alert composite action.

Workload cells (absorbed perf-4): add put-4kib/get-4kib (the #4221 fsync
regression size, ~-10% @4KiB, previously invisible) and get-10mib (historical
large-GET EOF size) to both the PR-labeled and nightly matrices — 6 workloads
x 2 phases x 2 drive-sync = 24 cells. A 1KiB cell is deferred to protect the
budget until perf-3.

Refs rustfs/backlog#1152 (perf-1, absorbed perf-4), rustfs/backlog#1155.
2026-07-10 20:52:25 +08:00
Zhengchao An 7cfd08d4f5 ci(mint): restore multi-SDK pipeline on ubuntu-latest + pin mint digest (#4668)
The mint multi-SDK compatibility pipeline had exactly one recorded run
(28730530597, 2026-07-05), which died at "Enable buildx": the self-hosted
sm-standard-4 pod it landed on had no /var/run/docker.sock, so
`docker buildx create` failed to connect to the Docker API before any test
ran. Same heterogeneous-fleet root cause ci-1 fixed for the weekly s3-tests
sweep.

- Pin the job to GitHub-hosted ubuntu-latest, which reliably ships a running
  Docker daemon + buildx + python3. Header note records the diagnosis and the
  dind-sm-standard-2 tradeoff.
- Pin MINT_IMAGE default to the linux/amd64 digest for minio/mint:edge
  resolved 2026-07-10 (was the rolling :edge tag); workflow_dispatch can still
  override. Header comment records the refresh command.
- Quote shellcheck-flagged vars and drop an unused loop var so actionlint
  passes with zero findings.
- Left report-only (ci-3 adds baseline gating later, per adjudication G4) and
  a TODO(ci-8) alerting hook on the scheduled job.

Refs: rustfs/backlog#1149 (ci-2), rustfs/backlog#1155
2026-07-10 20:51:27 +08:00
houseme 5f1a475c56 perf(ecstore): stop zeroing pooled shard buffers on the GET path (backlog#1159) (#4681)
`ShardBufferPool::take` handed out a `resize(len, 0)`-ed buffer, and the
reader then overwrote every byte of it. CPU profiling of a cached 1 MiB
GET (device reads = 0, so all cost is CPU) attributed 4.81% of the whole
server to that memset — a buffer pool exists to reuse an allocation, and
memsetting it gives the saving straight back.

The zeroing was load-bearing only because `BitrotReader::read` takes
`&mut [u8]`, which must be initialized. But the reader never reads what
the caller put there, and never returns a partially filled buffer: both
the hashed and the no-hash path either fill the whole shard or fail with
UnexpectedEof, and a hash mismatch is an error rather than a short read.
So the initialization bought nothing observable.

Add `BitrotReader::read_appending(&mut Vec<u8>, want)`, which appends into
the buffer's spare capacity instead of demanding initialized bytes:

  * hashed path — unchanged single copy, `extend_from_slice(data)` in place
    of `copy_from_slice` into a pre-zeroed buffer, and only after the hash
    verifies, so corrupt bytes never reach the caller's buffer;
  * no-hash path — `read_buf` writes straight into the spare capacity and
    advances the length only over bytes the reader actually wrote, so an
    uninitialized tail can never be exposed.

`ShardBufferPool::take` now yields an empty buffer with capacity, and
`read_shard` no longer needs to `truncate`. `read` keeps its old signature
for the remaining callers.

Four tests gate the contract rather than the call:
  * `read_appending` is byte-for-byte identical to `read` on both paths;
  * a truncated shard is UnexpectedEof, never a partially filled buffer;
  * bytes that fail the bitrot hash never reach the caller's buffer;
  * `want > shard_size` is rejected;
plus the pool test now asserts the allocation is reused (same pointer) and
never zeroed.

Verified: `erasure::` 213 passed, 0 failed; on a real Linux host
`erasure::` 209 and `disk::local::` 143 pass serially, and the failures
seen in a parallel full-suite run reproduce identically on unmodified main
(they are ENOSPC from a full root filesystem plus pre-existing flakes).

Not claimed: an end-to-end throughput number. The A/B on the bench host was
too noisy to attribute (one rep pair was not fully cached, and its root
filesystem filled mid-run); what is measured is that the removed memset was
4.81% of GET CPU in the pre-change profile.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 12:34:45 +00:00
Zhengchao An 80ddd8fa7e fix(ecstore): roll back delete on disks that staged then errored (#4676)
fix(ecstore): roll back delete on disks that staged then errored (backlog#1158)

#4300 rolls back a failed delete only on disks that returned Ok, skipping any
disk that staged its rollback backup, applied the delete, and then errored --
leaving that disk deleted while its peers are restored. On rollback, fan the
undo out to every online disk instead; the disk-side restore_delete_rollback
is already idempotent (Ok no-op when nothing was staged), so unstaged disks
are unaffected. The err.is_some() skip now applies only to the success/cleanup
path. Covers both single-object and batch delete.

Refs backlog#1158.
2026-07-10 20:34:19 +08:00
houseme b604074217 perf(object-data-cache): take fill off the GET path and fix the metrics (#4678)
* fix(object-data-cache): make cache metrics one-increment-per-GET

Rework the object data cache observability so each counter carries one
clear meaning, and drop dead per-entry state.

ODC-17 (backlog#1122): split requests_total, which was incremented by
both plan_get and lookup_body, into plan_total{decision,reason,size_class}
(emitted only by plan_get) and lookup_total{result,size_class} (emitted
only by lookup_body). Each is now incremented exactly once per GET per
layer; help text states this.

ODC-18 (backlog#1123): add a JoinedInflightFill result (label
joined_inflight) so a singleflight waiter is no longer counted as an
insert. record_fill_result now always counts the outcome but records
fill bytes only when non-zero and the duration histogram only when
present, so joined_inflight / skipped_by_mode / skipped_size_mismatch
no longer inflate fill_bytes_total or add non-fill duration samples.
The waiter->JoinedInflightFill mapping lives in MokaBackend (owned by a
concurrent branch); cache.rs handles the variant already.

ODC-29 (backlog#1134): stop refreshing the cache-state gauge on every
lookup, and debounce it on fill/invalidate to at most once per second
via an AtomicU64 millis timestamp, since moka's entry_count is a
settling approximation.

ODC-36 (backlog#1141): give invalidations_total an outcome label
(removed|noop) and skip the gauge refresh on the no-op path. Extend
ObjectDataCacheInvalidationResult with Removed{keys}/NoOp (Success kept
as a transitional variant until MokaBackend reports the removal count);
NoopBackend now reports NoOp.

ODC-30 (backlog#1141): drop the dead content_length/etag/inserted_at
fields (and getters) from ObjectDataCacheEntry, which are redundant with
the moka key identity; the constructor keeps its arity so the
out-of-scope MokaBackend caller still compiles. Gate is_null_version
behind cfg(test).

Tests use a thread-local metrics recorder to avoid the global-singleton
flakiness. Fill-enabled test configs set min_free_memory_percent=0 so
the cgroup gate does not refuse fills in CI pods.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): move fill off GET path, bound & harden the fill

Implements five object-data-cache audit findings.

ODC-15 (backlog#1120): the GET response no longer blocks on cache fill.
The buffered/materialized body is already in hand, so the fill now runs
in a detached task and the response is built immediately. Singleflight is
made non-blocking: `try_acquire` returns Leader or Busy, and a non-leader
skips its own fill (SkippedSingleflightBusy) instead of waiting on another
request's leader. The inner fill spawn is KEPT: once the index key is
registered, the recheck/undo must complete even if the enclosing fill
future is aborted, so removing it would weaken the cancellation-safety
guarantee (ODC-13 test) and the invalidation-race undo.

ODC-11 (backlog#1116): enforce the fill-concurrency knobs. MokaBackend
now holds a Semaphore sized min(per_cpu * parallelism, max), acquired
after winning leadership and before the memory gate. On saturation it
rejects (try_acquire_owned) with SkippedFillConcurrency rather than
queueing, so the fill path never reintroduces GET-latency coupling.

ODC-14 (backlog#1119): the memory gate no longer does a blocking sysinfo
refresh under a mutex on the async fill path. A dedicated periodic
refresher (tokio interval + spawn_blocking) updates an atomic snapshot
every 5s; allows_fill is now lock-free. The min_free_memory_percent == 0
short-circuit still runs before any snapshot read. No runtime at
construction (sync unit tests) simply keeps the seed snapshot.

ODC-32 (backlog#1137): singleflight no longer emits metrics while holding
the map mutex. try_acquire/remove_entry capture the map length under the
guard, drop it, then emit the gauge/counter.

ODC-07 (backlog#1112): the materialize read is bounded via
take(capacity + 1) so an over-long stream cannot grow the buffer past the
in-memory GET threshold, and any length mismatch is now a hard error
matching the direct-memory GET path, instead of warn-and-serve.

cache.rs is owned by a concurrent branch; this commit only appends the
SkippedFillConcurrency and SkippedSingleflightBusy variants + label arms.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): report JoinedInflightFill and invalidation outcome

Reconciles the moka_backend half of two metrics-semantics findings after
merging fix/odc-metrics-semantics (which landed the cache.rs half).

ODC-18 (backlog#1123): the singleflight non-leader path now returns
JoinedInflightFill instead of counting as an insert, so N concurrent GETs
of one cold key record one insert, not N. This composes with the ODC-15
redesign: the non-leader does NOT wait for the leader (it already owns the
body and never re-serves from the fill result), so no blocking wait is
reintroduced. Drops the redundant local SkippedSingleflightBusy variant in
favor of the canonical JoinedInflightFill (mapped to no bytes / no duration
by the facade). SkippedFillConcurrency (ODC-11) is retained.

ODC-36 (backlog#1141): MokaBackend::invalidate_object now returns
Removed { keys } / NoOp instead of the transitional Success, so the facade
labels invalidations_total{outcome=removed|noop} and skips the cache-state
gauge refresh on the no-op path.

Tests: duplicate-fill test asserts JoinedInflightFill (bites when reverted
to Inserted); new no-op invalidation test; matching-identity test asserts
Removed { keys: 1 }.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): drop unused mut on materialize stream binding

The ODC-07 change moves `final_stream` into `AsyncReadExt::take`, so the
`build_get_object_body_with_cache` parameter is no longer mutated in place.

Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor(object-data-cache): close the cross-branch coordination seams

Merging the metrics and hot-path branches left four transitional shims that
only existed because each side could not edit the other's files. With both
sides present they are dead weight, and two of them actively misreport.

- ObjectDataCacheEntry::new took content_length and etag only to keep the
  caller in moka_backend.rs compiling, then discarded both. Drop them; the
  entry is a Bytes wrapper and the caller now passes only the body.

- SkippedSingleflightClosed lost its only producer when the waiter model was
  replaced by Leader/Busy election. Remove the variant.

- The fill-accounting match ended in a wildcard that charged fill_bytes and a
  duration to every non-Inserted outcome, so a fill rejected by the memory
  gate or the concurrency semaphore — which never touched the backend and
  wrote nothing — still inflated fill_bytes_total. Enumerate every variant
  explicitly: only outcomes that wrote the body report bytes and duration.
  Exhaustiveness also forces a future variant to state its accounting rather
  than inherit a wrong default.

- InvalidationResult::Success mapped a no-op to outcome=removed, the exact lie
  backlog#1141 set out to fix, and its doc comment claimed MokaBackend still
  returned it after that backend had been widened. It has no producer left.
  Remove it, and tighten the app-layer test from "any successful variant" to
  the real contract: the pre-mutation call reports Removed{keys:1}, the
  post-delete call NoOp.

Refs: backlog#1123, backlog#1141, backlog#1135

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 12:25:23 +00:00
houseme 8039a4ceae test(cache): end-to-end regressions for the body-cache hook P0s (#4675)
fix(ecstore): make body-cache hook re-registrable + add e2e regressions

ODC-21 (backlog#1126): the GET body-cache hook lived in a first-wins
OnceLock. When AppContext is rebuilt (config reload, test re-init) a fresh
ObjectDataCacheAdapter is constructed and re-registered, but the OnceLock
kept ecstore's GET probe pointed at adapter #1 while every usecase-layer
fill and invalidation targeted adapter #2 — silently degrading the feature
to a 0% hit rate with no error, log, or metric, and stranding entries in the
unreachable cache until their TTL.

Replace the slot with RwLock<Option<Arc<dyn GetObjectBodyCacheHook>>> so
re-registration atomically swaps to the newest adapter, and log at WARN when
a swap replaces a *different* instance (Arc::ptr_eq). RwLock over
ArcSwapOption because arc-swap's RefCnt is impl<T> (Sized, thin *mut T) and
cannot hold an Arc<dyn Trait> without a sized newtype wrapper; the probe
reads the slot once per full-object GET but only clones an Arc, negligible
next to the metadata quorum fan-out already done before the probe. Add a
test-only clear_get_object_body_cache_hook so tests register/unregister
deterministically.

With the hook now re-registrable, add true end-to-end regressions that drive
get_object_reader (not the full_object_plaintext_len predicate) against a
real erasure-coded, genuinely-compressed object via the blackbox
make_local_set_disks harness, with a stand-in hook playing the app-layer
cache (the injection point production uses; the adapter itself lives above
ecstore). These close the gap the predicate-only tests left — a caller that
opens a new shortcut serving the cached body directly, the original form of
both P0s:

- backlog#1108: a raw_data_movement_read must yield the STORED (compressed)
  bytes, never the cached plaintext.
- backlog#1109: a compressed cache hit must publish the DECOMPRESSED length
  as object_info.size (the UploadPartCopy invariant), with the streamed
  length matching.
- backlog#1146: a restore read (restore_request.days) must serve STORED
  bytes, not the cache.

Mutation-verified each e2e test bites: dropping the raw_data_movement_read
gate serves plaintext (fails #1108); removing the hit-site size republication
publishes 2972 vs 660000 (fails #1109); dropping the restore gate serves
plaintext (fails #1146).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 12:17:20 +00:00
Zhengchao An d26e06bf46 docs: make CONTRIBUTING and Makefile help match real verification gates (#4667)
CONTRIBUTING.md and the Makefile HEADER advertised nonexistent targets
(make clippy, make check) and claimed 'make pre-commit' runs
format + clippy + check + test, while .config/make/pre-commit.mak
actually runs fmt-check + guard scripts + quick-check with no clippy
and no tests. CONTRIBUTING.md also claimed the repository ships an
automated pre-commit git hook, but no hook is versioned.

- Replace phantom targets with the real ones (clippy-check,
  quick-check, compilation-check)
- Document exactly what pre-commit and pre-pr run, gate by gate,
  with an explicit note that pre-commit runs no clippy and no tests
- Point contributors to 'make pre-pr' as the full pre-PR gate
- Rewrite the git-hooks section to say hooks are optional and not
  versioned; setup-hooks only marks an existing hook executable

No make targets were added or changed; docs/help truth only.

Refs: rustfs/backlog#1153 (infra-9), rustfs/backlog#1155
2026-07-10 20:08:37 +08:00
Zhengchao An 89ea931ee1 test(ci): strict nextest ci profile with quarantine + flake policy (#4666)
test(ci): add strict nextest ci profile with quarantine + flake policy

Formalize the existing ecstore-serial-flaky mechanism into a strict CI gate
(ci-10, absorbs infra-15; backlog#1149).

- .config/nextest.toml: add [profile.ci] with global retries=0 (never mask a
  new race's first occurrence), fail-fast=false, and JUnit output at
  target/nextest/ci/junit.xml. Add a quarantine section where flaky tests get
  retries=2 under the ci profile only; each entry links one OPEN issue. First
  members are the two backlog#937 ecstore groups
  (concurrent_resend_same_part_commits_one_generation and
  store::bucket::tests::bucket_delete_*), which keep their existing
  ecstore-serial-flaky test-group serialization. Local default profile still
  never retries.
- .github/workflows/ci.yml: run the main test step with --profile ci and upload
  the JUnit report (if: always(), 3-day retention, run-number in name). The
  migration-proof step stays on the default profile to avoid clobbering the ci
  JUnit artifact (its tests are not quarantined).
- docs/testing/README.md: new skeleton (owned by backlog#1153 infra-11) holding
  the flake policy: discover -> open issue within 24h -> quarantine with issue
  link -> fix or delete within 30 days. AGENTS.md points to it.

Refs: rustfs/backlog#1149, rustfs/backlog#937, rustfs/backlog#1155
2026-07-10 20:08:31 +08:00
Zhengchao An f0b1202b65 ci(s3tests): fix weekly sweep runner infra (pin ubuntu-latest + setup-python) (#4665)
The weekly full ceph/s3-tests sweep (e2e-s3tests.yml, schedule path) has
failed every recorded run. The 2026-07-05 run (rustfs/rustfs #28727691591)
died at "Install Python tools" with `No module named pip`: the self-hosted
`sm-standard-4` label is served by heterogeneous runners and the scheduled
job landed on a minimal pod with neither `pip` (bare python3) nor
`docker.sock`, so no test ever executed.

Make the infrastructure assumptions explicit instead of trusting drifting
self-hosted runner state:
- Pin the job to GitHub-hosted `ubuntu-latest`, which reliably ships Docker,
  docker compose, python3 and pip.
- Add an explicit actions/setup-python step before any pip usage so the
  workflow stays correct across runner-image drift.
- Document the fix and rationale in the workflow header comment.

Also quote the shell variables flagged by shellcheck (SC2086) in the
Docker build/run steps and drop the unused loop variable (SC2034) so
actionlint passes with zero findings on the changed file.

The PR gate (ci.yml s3-implemented-tests) is unaffected: it avoids Docker
via DEPLOY_MODE=binary and defers pip setup to run.sh's self-bootstrap.

Refs: rustfs/backlog#1149 (ci-1), rustfs/backlog#1155
2026-07-10 20:08:26 +08:00
Zhengchao An 2d1323d2f0 ci(fuzz): narrow PR triggers and cover all fuzz targets (#4664)
Re-enable the Fuzz workflow (disabled_manually) with two changes that
address the congestion that likely caused it to be disabled:

- Narrow the PR trigger paths to fuzz/**, scripts/fuzz/** and
  fuzz.yml only. The broad crate paths (crates/ecstore|filemeta|utils)
  queued a ~45min fuzz-build on nearly every PR; coverage of those
  crates moves to the nightly schedule, which fuzzes against main.
- Expand the smoke matrix, nightly matrix, run.sh default set and the
  fuzz-build staging/upload steps from 3 to all 5 buildable targets:
  archive_extract, bucket_validation, local_metadata, path_containment,
  policy_ingress. archive_extract and policy_ingress were previously
  in no matrix.

The two *_storage_api.rs files are `mod` submodules of their parent
targets (bucket_validation, path_containment), not standalone bins, so
fuzz/Cargo.toml registers exactly 5 fuzz bins.

Smoke jobs run one target per parallel matrix job (-max_total_time=60),
so wall-clock stays per-target, well under the 15min budget for a
fuzz-only PR. A TODO(ci-8) hook marks where scheduled-failure alerting
will attach on the nightly job.

Refs rustfs/backlog#1149 (ci-9, absorbed sec-13), rustfs/backlog#1155
2026-07-10 20:08:03 +08:00
Zhengchao An 36428bc490 fix(rustfs): allow drop_non_drop on dial9 guard for non-Drop feature sets (#4679)
Under feature sets where the dial9 session guard carries no Drop impl (e.g.
--features sftp, after #4663), `drop(dial9_guard)` in the startup entrypoint
trips clippy::drop_non_drop and fails the sftp Test and Lint job. The drop is
an intentional flush point where the guard does implement Drop; annotate it so
it compiles clean across all feature combinations.
2026-07-10 20:00:22 +08:00
houseme 00536da80c refactor(obs): make dial9 telemetry opt-in and actually record events (#4663)
* refactor(obs): make dial9 telemetry opt-in and actually record events

The dial9 Tokio-runtime profiler was disabled by default, yet every build
paid for it, and enabling it produced trace files with no events in them.

Recorded empty traces
---------------------
`build_traced_runtime` called `TracedRuntime::builder()...build(..)`, but dial9
only starts recording in `build_and_start*`. `build` still returns a live guard
whose `is_enabled()` reports true, and still creates and seals segment files —
they just contain a header and no events. It also skipped `with_trace_path`, so
the background worker driving the segment pipeline was never spawned.

Measured on the new smoke example: 310 bytes of bare segment header, against
5640 bytes for the same workload once recording actually starts.

Switch to `with_trace_path(..).build_and_start(..)`.

Cost was unconditional
----------------------
`--cfg tokio_unstable` was a global `[build] rustflags` entry and `rustfs-obs`
depended on `dial9-tokio-telemetry` unconditionally, so all builds depended on
Tokio's non-semver API. Worse, an environment `RUSTFLAGS` replaces (never
appends to) the config-file value, so any caller exporting their own RUSTFLAGS
silently dropped the flag — the long comment in build.yml was a scar from that.

dial9 is now an opt-in feature (`dial9`, plus `dial9-s3` and `dial9-taskdump`),
the global rustflag is gone, and `crates/obs/build.rs` fails the compile if the
feature is on without the flag. Telemetry builds go through `make build-profiling`.

Metrics that could not lie
--------------------------
`rustfs_dial9_{events_total,bytes_written_total,rotations_total,cpu_overhead_percent}`
were hard-coded to zero — a Counter pinned at 0 reads as "nothing happened".
Removed. `rustfs_dial9_enabled` was sourced from the environment, so it read 1
even when the traced runtime failed and the process fell back to a standard
runtime; it is replaced by `rustfs_dial9_supported` (compile-time),
`rustfs_dial9_configured` (intent) and `rustfs_dial9_active_sessions` (reality).

No `writer_healthy` gauge is exported: dial9's `RotatingWriter` can enter its
`Finished` state and stop writing, but exposes no way to observe that, so the
gauge could only ever be hard-coded to 1. Documented as a known gap instead.

Final events were lost
----------------------
The `TelemetryGuard` lived in a `static OnceLock`, which is never dropped, so
buffered events were never flushed at exit. `build_tokio_runtime` now returns
the guard and `run_process` drops it before any exit path.

Also
----
- `disk_usage_bytes` was a `read_dir` + per-file `stat` on the metrics
  collection path. It is now sampled by a background task into an atomic.
- `SAMPLING_RATE`/`S3_BUCKET`/`S3_PREFIX` were parsed, warned about, and
  discarded. S3 upload is now wired to dial9's `with_s3_uploader` behind
  `dial9-s3`; `SAMPLING_RATE` has no upstream equivalent and is removed.
- Wire `with_task_dumps` (async backtraces of stalled tasks), configurable via
  `RUSTFS_RUNTIME_DIAL9_TASK_DUMP_{ENABLED,IDLE_THRESHOLD_MS}`.
- Split `telemetry/dial9.rs` into `config`/`state`/`enabled`/`disabled`; the
  stub keeps the public API identical so callers need no `#[cfg]`.
- Drop four print-only examples and the manual test bin that exercised the
  removed `init_session` scaffolding.

Verified: cargo check/clippy/test across default, `dial9`, and `dial9-s3`;
build.rs correctly rejects `dial9` without `--cfg tokio_unstable`;
`make pre-commit` passes.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(obs): document dial9 as an on-demand profiler

scripts/run.sh advertised a `SAMPLING_RATE` knob that was never passed to dial9,
and claimed "CPU overhead < 5% (with sampling rate 1.0)" and "lower values reduce
CPU overhead" on the strength of it. The knob is gone; the guidance built on it
had to go too.

Replace it with what is actually true: dial9 needs a `make build-profiling`
binary, its disk budget evicts oldest-first (so a high poll rate can overwrite
the incident you are chasing), and it cannot be toggled without a restart.

Add docs/operations/dial9-runtime-profiling.md covering the build variants, an
investigation walkthrough, the configuration table, how to read the three
supported/configured/active_sessions gauges against each other, and the upstream
gap that makes writer death only indirectly observable.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(obs): add a dial9 smoke example that proves events are recorded

The bug this guards against is invisible to every existing signal: with
`build` instead of `build_and_start`, dial9 creates the trace file, seals
segments, and reports `TelemetryGuard::is_enabled() == true` — it simply
records no events. Only the segment's byte count tells the two apart.

Measured on this workload: 5640 bytes when recording, 310 bytes (a bare
segment header) when not. The example asserts >= 2048 bytes, and was verified
to fail with the `build` call restored.

Also correct the comment on the `is_enabled` check in `finish_traced_runtime`.
It claimed to catch "recording silently off"; it does not. It only rejects the
inert guard a lenient config yields after a build failure. Recording is
guaranteed by `build_and_start`, not by that check.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(rustfs): accept Unsupported runtime telemetry capability

A binary built without the `dial9` feature now reports the runtime-telemetry
capability as `Unsupported` rather than `Disabled`. The distinction matters to
operators: `Disabled` implies the capability can be switched on by setting an
environment variable, which is not true here — telemetry needs a rebuild.

Widen the assertion and pin the new semantics: when `dial9::is_supported()` is
false, the state must be exactly `Unsupported`.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs): drop the dial9-s3 feature, its TLS stack is vulnerable

CI's Dependency Review and `cargo deny` both reject the branch: dial9's
`worker-s3` feature depends on aws-sdk-s3-transfer-manager 0.1.3, which pins
aws-smithy-http-client onto hyper-rustls 0.24 and rustls-webpki 0.101.7. That
webpki carries RUSTSEC-2026-0098, -0099 and -0104.

0.1.3 is the latest release of the transfer manager, and 1.2.0 the latest of the
smithy client, so there is nothing to upgrade to. Cargo's feature unification can
add features but cannot drop a transitive dependency, so it cannot be worked
around from here either — the rest of the workspace already resolves to the safe
rustls-webpki 0.103 / hyper-rustls 0.27.

Remove the `dial9-s3` feature and the `with_s3_uploader` wiring. The two S3
environment variables stay parsed and warned about, now naming the real reason
rather than a missing build feature. Trace segments are collected from the output
directory instead. Tracked as D9-14 in rustfs/backlog#1157.

With this, Cargo.lock is byte-identical to main: the PR no longer touches the
dependency graph at all.

Also correct the `dial9-taskdump` documentation. It claimed the feature "compiles
to a no-op elsewhere"; in fact `tokio/taskdump` raises a `compile_error!` on any
target other than linux/{aarch64,x86,x86_64}. Verified by trying to build it on
macOS, which is how the claim was found to be wrong.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 10:52:48 +00:00
houseme f83f9ada13 fix(ecstore): reclaim page cache after io_uring reads (#4662)
fix(ecstore): io_uring reads must reclaim the page cache like StdBackend (backlog#1145)

`StdBackend::pread_bytes` calls `fadvise(DONTNEED)` over the range it just
read whenever `should_reclaim_file_cache_after_read(length)` holds —
`RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE` (on by default) above a 4 MiB
threshold. Large object reads are usually cold, and leaving them resident
evicts everything else, so this is a deliberate policy rather than a side
effect of how StdBackend happens to read.

`pread_uring` and `pread_uring_direct` never did it. Enabling io_uring on a
disk therefore turned the policy off silently: shard reads at or above the
threshold stayed resident and grew the page cache without bound. This is the
same class of mistake as the O_DIRECT loss fixed earlier — copying the read
itself while dropping the policy attached to it.

It also badly distorts any benchmark. An end-to-end warp GET A/B on a
16-core host (4 MiB objects, conc 64) showed io_uring at 8782 MiB/s against
StdBackend's 116 MiB/s. That is not a 76x speedup: with io_uring the run
issued *zero* device reads and grew the page cache, while StdBackend read
2950 MiB from the device and shrank it. The two legs were not running the
same policy. (Disabling the mmap read path changed nothing — 1.01x — so the
mmap-vs-pread difference was not the cause either.)

Both io_uring read paths now reclaim the range they read, with the same gate,
the same range, and the same error handling as StdBackend. O_DIRECT should
leave nothing resident anyway, but a filesystem that quietly buffered the read
still has to honour the policy, so `pread_uring_direct` reclaims too.

The test measures the policy, not the call: it reads an 8 MiB file through
each backend and asks `mincore(2)` how much stayed resident. Crucially it also
asserts the reclaim-disabled case leaves the range resident — without that
gate, a backend that never reclaimed would pass silently. Verified: with the
fix removed, the test fails with "uring: reclaim is on ... 2048/2048 pages
remain"; with the fix it passes.

Verified on a real Linux host (16-core, real io_uring): clippy --tests
-D warnings clean; disk::local tests 143 passed, 0 failed.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 18:50:47 +08:00
Henry Guo 99eef032c6 fix(ecstore): restore Windows async read trait import (#4652)
* fix(ecstore): restore Windows async read trait import

* fix(ecstore): gate async read import to non-Unix
2026-07-10 18:28:29 +08:00
Henry Guo f16878ef23 fix(table-catalog): pass PyIceberg live smoke (#4637)
* fix(table-catalog): pass PyIceberg live smoke

* fix(table-catalog): satisfy live smoke clippy

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-10 18:28:15 +08:00
houseme 904554b417 fix(object-data-cache): make memory container-aware and bound cache config (#4655)
* fix(object-data-cache): bound cache config and make memory container-aware

Harden the object data cache configuration path so a single bad input
degrades to the disabled adapter instead of OOM-killing pods, panicking
at boot, or silently mis-sizing the cache (backlog#1110/1113/1114/1115/
1127/1140/1130).

- ODC-05: resolve capacity and the fill gate from the effective
  (container-aware) memory. Prefer sysinfo cgroup_limits() and use
  min(host, cgroup) as the total plus cgroup-derived availability, falling
  back to host values when no cgroup limit constrains. Log the resolved
  capacity and its basis once at startup.
- ODC-08: cap ttl/time_to_idle at 30 days in validate() so an unbounded
  Duration can no longer trip moka's ~1000-year builder assertion.
- ODC-09: drop the max_entry_bytes floor from the derived-capacity clamp so
  MAX_ENTRY_BYTES can no longer inflate total capacity above the safety
  clamp; reject an entry larger than the resolved capacity instead.
- ODC-10: require an explicit max_bytes to clear max_entry_bytes plus the
  weigher overhead so a fillable-but-unretainable cache is rejected.
- ODC-22: reject max_entry_bytes at/above the u32 weigher boundary.
- ODC-35: warn (not reject) when time_to_idle exceeds ttl and document the
  min(ttl, time_to_idle) expiry interaction.
- ODC-25: give numeric env overrides two-valued semantics via a new
  rustfs_utils::get_env_parse_outcome; a malformed value now disables the
  whole cache with one aggregated warning instead of silently keeping
  defaults.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): require identity_keys_max of at least 2

The identity index admits a new key by evicting the oldest one, so a
budget of 1 evicts the previous key on every fill and can never hold two
live keys of one object at once — a versioned bucket alternating two
versions then hits a permanent 0% hit rate.

Reject the degenerate value at config validation time, where the adapter
turns it into a disabled cache with a warning, since the bounded-eviction
policy cannot rescue it at runtime.

Handed off from the identity-index batch (backlog#1128), which changed the
overflow policy but could not touch validate().

Refs: backlog#1115, backlog#1128

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): let a zero free-memory floor opt out of the gate

Making the memory gate container-aware turned it live in CI: the runners
are Kubernetes pods, so the gate now reads the pod's cgroup free memory,
finds it below the 20% floor, and refuses the fill. Tests that assert a
fill succeeds then failed on CI while passing on a developer host, which
has no cgroup and falls back to host memory.

The gate is behaving correctly — the tests were the ones depending on a
live memory reading. Treat min_free_memory_percent = 0 as a deliberate
opt-out rather than an invalid value: allows_fill returns early before it
touches any snapshot, so admission becomes independent of where the suite
runs. Operators gain the same escape hatch.

Every test that requires a fill to succeed now sets the floor to 0. The
tests that exercise the gate keep it enabled via memory_gated_config, so
the ODC-05 coverage they provide is preserved rather than short-circuited.

Refs: backlog#1110

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(object-data-cache): opt the usecase fill tests out of the memory gate

The previous commit exempted the fill-dependent tests it could find, but
missed the six adapters built inside object_usecase.rs. Fixing the first
batch moved nextest's fail-fast point forward and CI surfaced them:
build_get_object_body_with_cache_materializes_once_and_hits_later and
..._uses_cached_body_without_reader_preread both assert a fill lands, and
both ran with the default 20% free-memory floor.

Set the floor to 0 on all six, matching the sibling test modules. Verified
by pinning the gate's snapshot to 0% available — harsher than any CI pod —
and confirming these tests still pass, which shows the exemption path never
reads memory at all.

An exhaustive sweep over every fill-enabled ObjectDataCacheConfig in the
tree now shows no remaining site: the only unexempted ones are
fill_enabled()'s matches! arm and two adapter tests that assert the adapter
is disabled and therefore never fill.

Refs: backlog#1110

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 09:17:32 +00:00
houseme 15b8b13698 feat(ecstore): cache part-file descriptors for io_uring reads (#4658)
* feat(ecstore): run one io_uring ring per shard on each disk (backlog#1145)

A buffered read that hits the page cache completes inline inside
`io_uring_enter`, so the thread driving a ring performs that read's
memcpy. One ring per disk therefore capped cache-hit reads at a single
core's memory bandwidth: measured on a 16-core host, one driver thread sat
pinned at 100% CPU while throughput stayed flat at ~5 GB/s regardless of
read size, against 50 GB/s for the blocking-pool baseline.

rustfs/uring#6 taught the driver to hold N independent rings, each with its
own thread, pending table, backpressure semaphore, and eventfd. Wire it up:
`UringBackend::try_new` now calls `probe_and_start_sharded`, and
`RUSTFS_IO_URING_SHARDS` selects the count per disk.

The default is a quarter of the available parallelism clamped to `1..=4`,
because the cost is `disks × shards` driver threads (each normally blocked
in `poll(2)`). Any override is clamped to `1..=16`, so a mistyped value can
neither disable the driver (0) nor spawn threads without bound; an
unparseable value falls back to the default.

Effect (warm page cache, 16-core, rustfs/uring's concurrent_pread_bench):

  1 MiB, conc 8:    1 shard  4911 MB/s -> 8 shards 47361 MB/s (9.6x);
                    the blocking-pool baseline is 50662 MB/s
  64 KiB, conc 32:  StdBackend 153678 IOPS, p999 3030 us
                    8 shards   345402 IOPS, p999  897 us
  64 KiB, conc 128: StdBackend 135155 IOPS, p999 10716 us
                    8 shards    389047 IOPS, p999  4092 us

Sharding removes the throughput deficit *and* keeps io_uring's tail-latency
advantage, rather than trading one for the other.

Unchanged: io_uring read stays gray-off by default
(`RUSTFS_IO_URING_READ_ENABLE`), reads are byte-for-byte identical to
StdBackend, the per-disk degradation latches and probe cache (backlog#1101)
and the O_DIRECT tiered fallback (backlog#1102) all still apply. Rings stay
per-disk, so a stalled disk cannot starve another disk's rings
(backlog#1055). Bumps the rustfs-uring pin to the merged #6 commit.

Verified on a real Linux host (16-core, real io_uring): cargo clippy
--tests -D warnings clean; disk::local tests 132 passed, 0 failed —
including the existing io_uring and O_DIRECT cases now running on the
sharded driver, plus a new test covering the shard-count default, override,
and clamping.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(ecstore): cache part-file descriptors for io_uring reads (backlog#1145)

`pread_uring` opened the file on the blocking pool for every read, so each
read paid a `spawn_blocking` round trip — the very thread hop io_uring
exists to avoid. Sharding the driver (backlog#1145) removed the previous
ceiling and left this as the binding cost. Measured on a 16-core host with
a 4-shard driver, warm page cache:

  64 KiB, conc 8:   143942 -> 263054 IOPS (+83%),  p999 240 -> 65 us
  64 KiB, conc 32:  150128 -> 204876 IOPS (+36%),  p999 2508 -> 871 us
  64 KiB, conc 128: 129172 -> 361287 IOPS (+180%), p999 15329 -> 3046 us
  1 MiB,  conc 32:   33875 ->  42301 IOPS (+25%)

At 64 KiB / conc 128 the open is what masked io_uring entirely: with it,
io_uring beat StdBackend by 3.5%; without it, by 189%.

Add a bounded per-disk descriptor cache used by the io_uring read path.
A hit takes no `open` and no `spawn_blocking`, so the read never leaves the
runtime worker.

Why caching a part-file descriptor is safe:
  * only `<object>/<data_dir>/part.N` reaches this backend's `pread_bytes`;
    `xl.meta` — the one path replaced in place — is read through `read_all`
    / `read_metadata` and never gets here;
  * part files are never rewritten in place. A replacement is always
    write-new-tmp then `rename`, which swaps the inode, so a cached
    descriptor can never observe a torn shard.

Why invalidation is nevertheless REQUIRED: heal reuses the existing
version's `data_dir` and renames a rebuilt shard onto the SAME part path.
A cached descriptor would keep serving the pre-heal (corrupt) inode,
defeating the heal and eroding read quorum. `delete` likewise unlinks a
part that a cached descriptor would keep readable. So `rename_data`,
`rename_file`, and `delete` all call the new
`LocalIoBackend::invalidate_cached_fds` after they mutate, and a 5s TTL
bounds the blast radius should a future mutation path forget to.

Two preamble checks the miss path runs are not silently lost on a hit:
  * bounds — the driver only short-reads at EOF (it resubmits otherwise),
    so `bytes.len() != length` is exactly the old `meta.len() < end_offset`
    check, and now yields the same `FileCorrupt`;
  * volume access — skipped while an entry is live. An unreachable disk
    keeps serving already-open descriptors for at most the TTL, after which
    the re-open re-runs the check. Disk health is tracked independently of
    this per-read probe.

Scope: buffered io_uring reads only. The O_DIRECT path keeps opening per
read (its reads are >= 4 MiB, so the open is a small fraction), and
StdBackend is untouched — it must take the blocking hop for the pread
regardless, so caching would buy it only 2-6% while carrying the same
invalidation risk. `RUSTFS_IO_URING_FD_CACHE=false` restores open-per-read.

Verified on a real Linux host (16-core, real io_uring): clippy --tests
-D warnings clean; disk::local tests 135 passed, 0 failed. The new
heal-staleness test first asserts a read still returns the PRE-heal bytes —
proving the cache is live and the hazard real — then that invalidation makes
the healed shard visible. A second test drives `rename_file` and `delete`
through `LocalDisk` to prove those paths actually invalidate, and a unit
test pins prefix invalidation to component boundaries (`a/b` must not drop
`a/bc`).

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 08:33:50 +00:00
Zhengchao An aaffd3ede8 fix(ecstore): align Deep verifier shard geometry (#4656) 2026-07-10 08:06:38 +00:00
houseme d29e637890 fix(object-data-cache): close identity-index races in fill/invalidate (#4657)
fix(object-data-cache): close identity-index races in fill/invalidate (backlog#1117/#1118/#1125/#1128/#1136)

The object body cache's fill-vs-invalidation contract (index.insert before
cache.insert, then re-check index membership and undo the fill on mismatch)
had several races where the identity index and the cache could drift, either
poisoning the race metric, breaking invalidation silently, or letting a
principal evict a hot body on demand.

ODC-12 (backlog#1117) — generation-aware index removal.
The eviction listener removed (identity, key) by key equality with no way to
tell an evicted old entry from a freshly refilled one under the same key.
Verified against moka 0.12.15: an insert over a TTL/TTI-expired but not yet
purged entry runs do_insert_with_hash inline during insert().await, and
do_post_update_steps awaits notify_upsert INLINE with cause Expired
(was_evicted() == true), so the listener deleted the index key the refill just
registered and the post-insert recheck then self-destructed the fresh entry
(SkippedInvalidationRace). A deferred Size notification for an old generation
had the same effect on a cleanly refilled key.
Chose Option A (generation token) over Option B (re-insert after
cache.contains_key): B removes the key first and only reconciles afterward,
which reopens a narrow window where a concurrent invalidate_object misses the
key while it is transiently absent from the index. A never removes a key whose
generation does not match, so the decision is atomic under the shard lock. The
token is the cache entry's Arc pointer captured at fill time (readable from the
value moka hands the listener), so no change to the entry type is needed.

ODC-13 (backlog#1118) — cancellation-safe insert/recheck/undo.
The recheck+undo sat after two awaits inside the S3 GET future; a client
disconnect could cancel the fill at the recheck await after cache.insert made
the entry visible, stranding a stale body with no index entry. The
register/insert/recheck/undo sequence now runs in a spawned task whose
JoinHandle the leader awaits: cancelling the await detaches the task, which
still finishes the recheck and undo. The dropped leader unblocks waiters as
before (SkippedSingleflightClosed), so no waiter is stranded.

ODC-20 (backlog#1125) — do not prune in-flight sibling keys.
prune_missing could remove another in-flight fill's index key (a different key
of the same identity that had registered but not yet published its cache
entry), making that fill's recheck fail and delete its own valid entry. Added a
read-only ObjectDataCacheSingleflight::has_inflight and consult it in the prune
predicate alongside cache.contains_key.

ODC-23 (backlog#1128) — bounded eviction instead of clear-and-reject.
Exceeding identity_keys_max drained the whole key set and rejected the incoming
key, so a principal able to GET identity_keys_max+1 distinct versions could
evict an object's hot body on demand, and identity_keys_max=1 on a versioned
bucket meant a permanent 0% hit rate. The key set now evicts only the oldest
key(s) (the Vec preserves insertion order) and inserts the new key. The insert
result carries evicted_keys so the backend removes exactly those from the cache
and still caches the newest body. The Overflow variant and the now-unused
SkippedIdentityOverflow construction are gone.
NOTE: the audit also asks for identity_keys_max >= 2 validation; that lives in
config.rs (out of scope here) and is deferred to the config batch.

ODC-31 (backlog#1136) — deterministic race coverage.
Added a #[cfg(test)] fill barrier between the index insert and the cache insert
so the fill-vs-invalidation recheck can be driven deterministically, plus a
companion test asserting the identity-budget eviction drops the evicted keys'
cache entries.

New tests:
- index: key_set_bounded_eviction_evicts_oldest_and_inserts_new,
  key_set_removes_only_matching_generation_token,
  key_set_duplicate_refreshes_generation_token
- starshard_index: identity_index_bounded_eviction_evicts_oldest,
  identity_index_stale_eviction_preserves_refreshed_key,
  identity_index_matching_eviction_removes_key
- moka_backend: moka_backend_refill_after_expiry_without_maintenance_inserts,
  moka_backend_concurrent_fills_same_identity_both_insert,
  moka_backend_identity_budget_evicts_oldest_and_caches_newest,
  moka_backend_fill_loses_race_to_invalidation,
  moka_backend_cancelled_fill_still_undoes_lost_race

cargo fmt --all, cargo check/clippy/test -p rustfs-object-data-cache all pass
(44 tests). Verified the refill-after-expiry and concurrent-fill tests fail
without their respective fixes.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 08:04:57 +00:00
houseme 8c76efead2 fix(cache): stop the GET body-cache hook from bypassing ReadPlan (#4654)
* fix(cache): gate GET body-cache hook to preserve ReadPlan output

The ecstore GET body-cache hook serves cached full-object plaintext
directly, bypassing ReadPlan/ReadTransform. That is only sound when the
normal read path returns that same plaintext byte-for-byte. Two probe
conditions were missing, plus two usecase-layer planner gaps.

ODC-01 (backlog#1108): raw/data-movement reads. ReadPlan::build returns
the STORED representation for raw_data_movement_read (e.g. compressed
bytes, length = oi.size), but the cache holds the post-decompression
body. Decommission (raw_data_movement_read: true) would receive
decompressed plaintext where raw compressed bytes are required, silently
corrupting the destination pool.

ODC-02 (backlog#1109): compressed objects. ReadTransform::Compressed
rewrites object_info.size to the decompressed length; on a hook hit
object_info is returned unchanged, so object_info.size is the compressed
size while the stream carries the decompressed body. UploadPartCopy then
uses src_info.size as the copy length and truncates the part.

Fix: gate the hook probe with should_probe_body_cache_hook, refusing
raw_data_movement_read, data_movement, and compressed objects, mirroring
the conditions get_small_object_direct_memory_decision already applies.

ODC-33 (backlog#1138): build_get_object_body_cache_plan lacked the
is_remote() exclusion the ecstore hook enforces; add it so transitioned
(remote-tier) objects are excluded uniformly.

ODC-C1 (backlog#1142): zero-length bodies save no I/O (ecstore returns an
empty body before the hook probe) yet the planner admitted them; change
the guard to response_content_length <= 0 so they plan Skip, mirroring
should_buffer_get_object_in_memory_with_threshold.

Tests: body_cache_hook_gate_tests (4) cover plain-probe plus
raw/data-movement/compressed skips; planner gains
plan_skips_remote_transitioned_objects and plan_skips_zero_length_objects.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(cache): allow compressed bodies via a fail-closed read allow-list

The body-cache hook probe was gated by a deny-list that refused compressed
objects outright, which cost the cache every compressed body — a growing
share of stored data. Replace it with an allow-list that returns the exact
plaintext length a hit may serve, or None.

full_object_plaintext_len() answers a single question: would the normal
ReadPlan produce this object's complete plaintext, and under which size?
Compressed objects now qualify, and the hit site publishes the returned
length as object_info.size, reproducing the contract ReadTransform::
Compressed establishes. A hit whose body length disagrees is refused and
falls through to the erasure read.

This also closes a gate the deny-list only covered by accident: a restore
read forces ReadPlan down the Plain branch, so a compressed object yields
STORED bytes under its compressed size. Refusing compressed objects hid
that; admitting them exposes it, so restore reads are refused explicitly.

Being fail-closed, a newly added ReadPlan branch bypasses the cache by
default rather than silently serving the wrong representation — the
structural defect behind both backlog#1108 and backlog#1109.

Refs: backlog#1108, backlog#1109

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 08:01:10 +00:00
GatewayJ a269f8df05 fix(ecstore): require write quorum for metadata early stop (#4300) 2026-07-10 15:57:09 +08:00
Zhengchao An 40cf94f243 docs(tier-ilm): note local-first invariant for expire/GET race fix (#4651) 2026-07-10 15:56:54 +08:00
houseme fe682e16e9 feat(ecstore): run one io_uring ring per shard on each disk (backlog#1145) (#4653)
A buffered read that hits the page cache completes inline inside
`io_uring_enter`, so the thread driving a ring performs that read's
memcpy. One ring per disk therefore capped cache-hit reads at a single
core's memory bandwidth: measured on a 16-core host, one driver thread sat
pinned at 100% CPU while throughput stayed flat at ~5 GB/s regardless of
read size, against 50 GB/s for the blocking-pool baseline.

rustfs/uring#6 taught the driver to hold N independent rings, each with its
own thread, pending table, backpressure semaphore, and eventfd. Wire it up:
`UringBackend::try_new` now calls `probe_and_start_sharded`, and
`RUSTFS_IO_URING_SHARDS` selects the count per disk.

The default is a quarter of the available parallelism clamped to `1..=4`,
because the cost is `disks × shards` driver threads (each normally blocked
in `poll(2)`). Any override is clamped to `1..=16`, so a mistyped value can
neither disable the driver (0) nor spawn threads without bound; an
unparseable value falls back to the default.

Effect (warm page cache, 16-core, rustfs/uring's concurrent_pread_bench):

  1 MiB, conc 8:    1 shard  4911 MB/s -> 8 shards 47361 MB/s (9.6x);
                    the blocking-pool baseline is 50662 MB/s
  64 KiB, conc 32:  StdBackend 153678 IOPS, p999 3030 us
                    8 shards   345402 IOPS, p999  897 us
  64 KiB, conc 128: StdBackend 135155 IOPS, p999 10716 us
                    8 shards    389047 IOPS, p999  4092 us

Sharding removes the throughput deficit *and* keeps io_uring's tail-latency
advantage, rather than trading one for the other.

Unchanged: io_uring read stays gray-off by default
(`RUSTFS_IO_URING_READ_ENABLE`), reads are byte-for-byte identical to
StdBackend, the per-disk degradation latches and probe cache (backlog#1101)
and the O_DIRECT tiered fallback (backlog#1102) all still apply. Rings stay
per-disk, so a stalled disk cannot starve another disk's rings
(backlog#1055). Bumps the rustfs-uring pin to the merged #6 commit.

Verified on a real Linux host (16-core, real io_uring): cargo clippy
--tests -D warnings clean; disk::local tests 132 passed, 0 failed —
including the existing io_uring and O_DIRECT cases now running on the
sharded driver, plus a new test covering the shard-count default, override,
and clamping.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 07:41:11 +00:00
houseme baf7e899b9 fix(ecstore): bound every walk filesystem call by the stall budget (#4650)
The listing path no longer has a wrapper-level total timeout, so any
filesystem call the walk makes without a stall bound would have no liveness
bound at all. The first pass covered list_dir and read_metadata but left four
reads unbounded: the volume access() probe and fs::metadata() in walk_dir, the
xl.meta read in its dir-object branch, and is_empty_dir() in scan_dir.

Split the helper so reads that do not yield a Result go through the same
budget, and route the four remaining calls through it. A hung drive now fails
those reads with DiskError::Timeout instead of parking the walk until the
merge consumer's own stall timeout aborts it.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 05:16:55 +00:00
houseme 8b233cdd94 fix(ecstore): keep O_DIRECT for eligible reads via native io_uring (backlog#1102) (#4649)
fix(ecstore): keep O_DIRECT for eligible reads via native io_uring (rustfs/backlog#1102)

Wire ecstore's io_uring read backend to rustfs-uring's native aligned
O_DIRECT read (`read_at_direct`, merged in rustfs/uring#3), so an
O_DIRECT-eligible read keeps BOTH io_uring's async submission AND
O_DIRECT's page-cache bypass instead of trading one for the other.

`UringBackend::pread_bytes` now selects the read shape by a tiered,
per-disk-latched ladder — never a blanket downgrade:

  1. io_uring latched off for this disk (backlog#1101) -> StdBackend.
  2. O_DIRECT-eligible + native path usable -> pread_uring_direct: open
     the file O_DIRECT, probe the device alignment once (cached), and
     read via read_at_direct. Best path: async + no cache pollution.
  3. O_DIRECT-eligible but the fs refused O_DIRECT earlier (latched)
     -> StdBackend aligned path (which itself degrades to buffered).
  4. non-O_DIRECT read -> buffered pread_uring.

Latching keeps a failure from being re-attempted per-read: an fs that
refuses O_DIRECT (EINVAL/EOPNOTSUPP on open) latches the native direct
path off for that disk; a restriction-class errno from the read latches
io_uring off wholesale (backlog#1101). Any other error falls back for
that one read without masking a genuine data problem as a permanent
downgrade. The alignment comes from the existing probe_direct_io_align
(statx STATX_DIOALIGN, default 4096) and is cached in a per-disk
OnceLock so the probe runs at most once.

This replaces the interim guard that routed every O_DIRECT-eligible read
to StdBackend (which disabled io_uring on the hottest shard reads); the
native path is now the default and StdBackend is only a fallback tier.
Bumps the rustfs-uring pin to the merged #3 commit.

Verified on a real Linux host (16-core, real io_uring + O_DIRECT):
cargo check --tests, clippy -D warnings, and disk::local tests all pass
(128 passed, 0 failed), including uring_preserves_o_direct_for_eligible_reads
across unaligned ranges.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 04:47:46 +00:00
houseme 399461c33e fix(ecstore): bound listing walks by drive stall, not total duration (#4647)
Foreground S3 listings wrapped the entire walk_dir stream in a single
wall-clock timeout (RUSTFS_DRIVE_WALKDIR_TIMEOUT_SECS, default 5s). That
budget measured how much data the walk had to produce rather than whether
the drive was still answering, so a healthy but large prefix on slow media
returned 500 InternalError / "Io error: timeout" to the client. The timer
also kept running while the walk was blocked writing to a slow consumer,
charging merge-side backpressure to the producer.

WalkDirOptions::stall_timeout_ms already carried the right semantics but was
only honored by the remote-disk RPC walk; LocalDisk::walk_dir ignored it
entirely. A single-drive deployment therefore had no way to distinguish a
hung drive from a big directory.

Teach LocalDisk::walk_dir to bound each individual drive read with the stall
budget, defaulting it from RUSTFS_DRIVE_WALKDIR_STALL_TIMEOUT_SECS when the
caller does not pin one, and let the foreground listing path skip the
wrapper-level total timeout. A walk that keeps making progress now runs to
completion; a drive that stops answering still fails with DiskError::Timeout.
Time spent blocked on the consumer stays outside the budget.

Heal and rebalance walks already skipped the total timeout and previously ran
unbounded on local drives. Give them an explicit, generous 60s stall budget so
this change does not tighten them from "no bound" to the 5s default.

Fixes #4644
Refs #2999, #3001

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 12:20:54 +08:00
houseme 0ad4a26056 fix(ecstore): keep O_DIRECT for eligible reads when io_uring is enabled (backlog#1102) (#4645)
fix(ecstore): keep O_DIRECT for eligible reads when io_uring is enabled (rustfs/backlog#1102)

UringBackend::pread_uring opens the file buffered, so with io_uring enabled an
O_DIRECT-eligible read (RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE + length >= the
threshold) silently lost O_DIRECT and polluted the page cache — a behavior
change the moment io_uring was turned on for a disk that uses O_DIRECT. The
io_uring backend is gray-off by default, so no deployment is affected, but the
gap had to close before it can be enabled anywhere O_DIRECT is in use.

Route O_DIRECT-eligible reads back through StdBackend's aligned path (which
itself falls back to buffered when the filesystem rejects O_DIRECT). A native
aligned O_DIRECT read in the driver remains part of rustfs/backlog#1102.

Adds uring_preserves_o_direct_for_eligible_reads: with BOTH flags on and the
threshold at 1, unaligned ranges still return exactly the requested bytes.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 03:43:47 +00:00
Zhengchao An dc3099bf0f feat(ecstore): run bucket operations on the store's own instance context (#4642)
backlog#1052 S7 — the final piece: full bucket-namespace isolation
between embedded servers in one process.

Server B's requests resolved B's own ECStore (per-server dispatch landed
earlier), but the store's bucket operations still went through ambient
process facades, so both servers effectively operated on the FIRST
server's disks and metadata:

- LocalPeerS3Client::local_disks_for_pools() called all_local_disk()
  (the ambient disk registry = the first published store's context), so
  list/make/delete/heal bucket scanned and wrote the wrong volumes.
- BucketMetadata::save() persisted through the ambient object handle, so
  a second server's bucket metadata landed in the first server's
  .rustfs.sys; set/remove/get/created_at all used the ambient metadata
  system.

Now the whole chain is bound to the owning store's InstanceContext:

- S3PeerSys/LocalPeerS3Client gain *_with_instance_ctx constructors and
  operate on that context's registered disks; ECStore::new (and the test
  store builder) pass the store's context. The legacy constructors keep
  the bootstrap default.
- BucketMetadata::save_with_store persists through an explicit store;
  BucketMetadataSys::persist_and_set uses the system's own api handle.
- metadata_sys gains instance-scoped variants (get_in / created_at_in /
  set_bucket_metadata_in / remove_bucket_metadata_in) that resolve the
  context's metadata system and fall back to the ambient default before
  the instance cell is initialized (early startup, unchanged behavior).
- The store's bucket handlers (make/get_info/list/delete + the
  table-bucket delete guard and emptiness check) use the per-context
  variants and this instance's disks.

Acceptance (e2e): two embedded servers with different credentials are now
isolated end to end — each authenticates only its own key, neither sees
the other's buckets or objects, and both data planes stay intact. The
embedded module doc drops the shared-IAM caveat.

579 ecstore bucket/metadata/peer regressions plus the embedded basic and
deferred-IAM e2e stay green.
2026-07-10 10:52:51 +08:00
houseme e6e4aef45b test(ecstore): cover the io_uring per-disk probe cache hit (backlog#1101) (#4641)
test(ecstore): cover the io_uring per-disk probe cache hit (rustfs/backlog#1101)

Follow-up to #4635. The per-disk negative probe cache was implemented but had
no dedicated test. Add uring_probe_cache_skips_known_unsupported_disk: a root
recorded in URING_UNSUPPORTED_DISKS makes try_new return None without a fresh
probe. Completes the #1101 acceptance ("cache hit does not re-probe").

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 02:08:56 +00:00
houseme 8ffff47529 chore(deps): update workspace dependencies (#4643)
Run `cargo update` followed by `cargo upgrade --exclude ratelimit` on the
latest main to pull the newest compatible versions.

Manifest bumps (Cargo.toml):
- md5 0.8.0 -> 0.8.1
- regex 1.12.4 -> 1.13.0
- sysinfo 0.39.5 -> 0.39.6

Lockfile-only bumps: der 0.8.1, dial9-tokio-telemetry 0.3.14, lru 0.18.1,
regex-automata 0.4.15, symbolic-common/symbolic-demangle 13.9.0, and
zlib-rs 0.6.6. Resolver re-selection moves crypto-common 0.1.7 -> 0.1.6
and generic-array 0.14.7 -> 0.14.9 (both semver-compatible). `ratelimit`
is held at 0.10.1 as requested; `pollster` 1.0.0 is an incompatible major
bump and is intentionally not taken.

Verification: cargo check --workspace --all-targets, cargo clippy
--all-features -D warnings, cargo nextest run --all --exclude e2e_test
(8467 passed), cargo test --all --doc, and cargo deny check
advisories/sources/bans/licenses all pass.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 01:59:02 +00:00
houseme d1daa97ac5 fix(ecstore): keep local path startup fail-fast under Kubernetes (#4640)
The startup topology convergence auto-detection (added in #4631) checked
Kubernetes before endpoint style, so a local path deployment (single or
multi-drive, non-distributed) running inside Kubernetes was classified as
orchestrated with an effectively unbounded wait window. Path-style
endpoints have no hostnames to resolve, so the wait never actually
triggers and runtime behavior is unchanged, but the startup log then
advertised mode=orchestrated / wait_timeout=MAX for a purely local
deployment, which is misleading and contradicts the documented policy
(local path endpoints -> fail-fast).

Order the auto-detection by endpoint style first: only distributed URL
endpoints, which resolve hostnames, pick orchestrated (Kubernetes) or
bounded (otherwise); local path endpoints stay fail-fast regardless of
the platform. Update the doc comment to match and add a policy case
locking Kubernetes + local path to fail-fast.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 01:34:10 +00:00
Henry Guo 24d0c09347 fix(admin): keep datausage live refresh stable (#4630)
fix(ecstore): keep fuller data usage overlay

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-10 09:17:35 +08:00
houseme 7dc03603da feat(ecstore): add io_uring degradation latch and per-disk probe cache (#4635)
feat(ecstore): io_uring runtime degradation latch + per-disk probe cache (rustfs/backlog#1101)

Follow-up to #1104. Add the runtime half of the io_uring degradation contract:

- Runtime latch: UringBackend gains an `active` flag (mirrors
  DirectIoReadState::supported). A read that returns a restriction-class errno
  (EPERM/EACCES/ENOSYS/EOPNOTSUPP) — io_uring became unusable on this disk —
  trips the latch, and every further read goes straight to StdBackend with no
  more per-read io_uring attempts. Data errors (EIO), missing files, and
  parameter errors do NOT latch, since StdBackend would hit them too.
- Per-disk probe cache: a process-wide negative cache of disks whose probe
  failed, so try_new skips re-probing (creating a ring + driver thread) on
  reconnect.

Tests: the errno classifier (restriction-only) and a latched-off read that
still returns correct bytes via StdBackend; the #1104 differential test still
passes under real io_uring.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 08:54:59 +08:00
Zhengchao An cefbe2ccc9 fix(test): allow InstanceContext bucket_metadata_sys reinitialization for tests (#4638)
The OnceLock introduced in 5fbd49a80 for bucket_metadata_sys panics when
multiple test modules initialize their own ECStore against the shared
bootstrap context. Switch to Mutex<Option> so test fixtures can
reinitialize safely.

Extract shared gating test environment to gating_test_env.rs to avoid
duplicate ECStore setup across delete_objects_stat_gating_test and
put_prelookup_gating_test.

Fixes 8 test failures:
- capacity_dirty_scope_test (2 tests)
- delete_objects_stat_gating_test (3 tests)
- put_prelookup_gating_test (3 tests)
2026-07-10 04:03:26 +08:00
Zhengchao An 5e80980698 test(ecstore): pin sorted scan_dir stream for dir-marker with children (backlog#1068) (#4626) 2026-07-10 03:34:21 +08:00
唐小鸭 7ab1abee80 fix(admin): allow site replication peer edits (#4623)
* fix(site-replication): align IAM and bucket metadata replication

* fix(admin): allow site replication peer edits
2026-07-10 02:09:42 +08:00
Zhengchao An f403ed1c2e feat(embedded): allow multiple embedded servers to coexist in one process
* feat(embedded): allow multiple embedded servers to coexist in one process

backlog#1052 S5: turn the embedded startup guard into a sequential lock
and make every write-once shared cell tolerate a second embedded start.
Two RustFSServers on different ports and volumes now start, run, and
shut down independently in the same process.

- EMBEDDED_SERVER_STARTED is released once each startup hands off; a
  second startup that runs after the first is no longer rejected.
- The second embedded server constructs its own InstanceContext instead
  of adopting the process bootstrap one, so region/endpoints/deployment
  id land on that context (the first server keeps adopting bootstrap to
  keep single-instance ambient facades unchanged).
- Startup-time tolerant paths:
  - action_credentials publish treats AlreadyInitialized as success
    (per-server ActionCredentialHandle already holds the real creds).
  - GLOBAL_RUSTFS_PORT warns instead of panicking on a second set.
  - Observability install returns Ok when the process subscriber is
    already set — the second server reuses it.
- New acceptance test proves two servers start, respond on their own
  ports, and one can be shut down without disturbing the other; the
  survivor keeps serving S3 requests. IAM and root-credential lookup
  still share a process domain (a second server whose creds differ from
  the first will fail signature validation), tracked as a follow-up.
- Embedded doc rewritten: 'Limitations' → 'Multi-instance status'; the
  AlreadyStarted error is now scoped to concurrent startups only.

The remaining work in #1052 is the auth path per-server dispatch and the
matching data-plane routing so two servers with different credentials
serve independent buckets end-to-end.

* feat(app): per-server auth and application context for multiple embedded servers (#4633)

backlog#1052 S6: each embedded server now authenticates against — and
its request path resolves — its OWN application context, so two servers
with different root credentials each accept their own access key and
reject the other's.

- AppContext is per-server: ensure_startup_after_iam constructs a fresh
  context around this server's store + IAM + KMS and installs it into the
  server's own ServerContextSlot, then publishes it as the process
  default first-writer-wins (publish_global_app_context) for legacy
  ambient readers. The old 'reuse the global if present' path is gone.
- FS::check (the S3 data-plane access gate) resolves auth against
  self.server_ctx's context: check_key_valid gains a _with_context
  variant that takes the root credentials and IAM system from an explicit
  context (None = ambient, unchanged for all 140+ existing callers). The
  region and the server context slot are published into the request
  extensions for downstream handlers.
- Each embedded server seeds its own root credentials into its context
  (ActionCredentialHandle.publish) at startup, so credential validation
  no longer falls back to the first server's process-global identity.
- The bucket/object/multipart use-cases resolve their store from the
  server's context (bucket_usecase_for/object_usecase_for/... take &FS).

New acceptance test: two servers with distinct credentials each
authenticate with their own key and reject the other's.

KNOWN FOLLOW-UP: full bucket-namespace isolation still requires threading
the instance context through the lower ecstore data plane (peer_sys /
disk registry / bucket-metadata reads still resolve via the process
GLOBAL_OBJECT_API), so the two servers do not yet present independent
bucket listings even though each holds its own store. That deeper pass —
a continuation of the #939 object-graph ctx threading — is the remaining
work on #1052.

Stacked on the S5 guard change.
2026-07-10 02:00:59 +08:00
houseme 6a81353785 feat(ecstore): converge startup topology under a wait-mode policy (#4631)
Multi-node startups (Kubernetes StatefulSets, bare-metal, VMs) can hit
transient DNS/local-host resolution failures while peers and headless
service records are still coming up. Previously endpoint resolution
retried DNS for a fixed 90s window and then returned an error, which
propagated out of run() and exited the process; in Kubernetes this drove
repeated kubelet restarts even though the cluster eventually converged.

Introduce a startup topology convergence policy with three modes:

- orchestrated: wait effectively indefinitely for recoverable
  DNS/topology errors so the process stays Running (readiness stays
  false) instead of exiting; defer the DNS-IP cross-port validation
  because headless-service records can still be flapping.
- bounded: wait a finite window (default 3m) then fail with an
  action-oriented error listing host/stage/elapsed and remediation hints.
- fail-fast: fail on the first non-transient resolution error.

Mode is auto-detected (Kubernetes -> orchestrated, distributed URL
endpoints -> bounded, local path endpoints -> fail-fast) and can be
overridden via RUSTFS_STARTUP_TOPOLOGY_WAIT_MODE,
RUSTFS_STARTUP_TOPOLOGY_WAIT_TIMEOUT and
RUSTFS_STARTUP_TOPOLOGY_RETRY_MAX_DELAY.

Also normalize divergent local/remote verdicts for endpoints that share
a host:port, throttle retry warnings, and keep the DNS-independent local
same-path checks in every mode. Configuration error handling is
unchanged: malformed volumes, mixed styles/schemes, duplicate or root
paths, and non-transient errors still fail fast.

Read the topology env vars through rustfs_utils::get_env_opt_str for
consistent alias handling, and simplify the log_once poisoned-lock branch
to unwrap_or_else(PoisonError::into_inner).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 17:57:13 +00:00
houseme da755eb66b feat(ecstore): runtime-probed io_uring read backend, gray-off by default (backlog#1104) (#4632)
feat(ecstore): add runtime-probed io_uring read backend, gray-off by default (rustfs/backlog#1104)

Wire the standalone rustfs-uring crate (https://github.com/rustfs/uring) into
LocalIoBackend as an opt-in read backend. When RUSTFS_IO_URING_READ_ENABLE is
set AND the per-disk io_uring probe succeeds, positioned reads go through the
cancel-safe UringDriver; otherwise — default, or on a restricted host, or on
any per-read driver error — reads fall back to StdBackend byte-for-byte, so the
default build is unchanged.

- Cargo.toml: rustfs-uring as a Linux-only git dependency (pinned rev). The
  guard scripts/check_no_tokio_io_uring.sh allows an explicit io-uring
  integration; only the tokio "io-uring" runtime feature is banned.
- UringBackend mirrors StdBackend::pread_bytes's resolution/access/bounds
  preamble and only swaps the raw byte read; the other three trait methods
  delegate to the inner StdBackend.
- Backend selection at LocalDisk construction via build_local_io_backend.
- Differential test uring_backend_reads_match_std: with the flag set, every
  positioned read returns byte-identical data (driver-served when io_uring is
  available, StdBackend fallback otherwise).

Verified: cargo check -p rustfs-ecstore on Linux; the differential test passes
under real io_uring (seccomp=unconfined). The runtime errno degradation latch,
per-disk probe cache, and productionization are tracked in
rustfs/backlog#1101/#1102.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 16:57:17 +00:00
houseme d90e354b11 chore(experiments): remove io_uring spike, migrated to rustfs/uring (#4629)
The io_uring cancel-safety spike (imported in #4625) has been migrated to a
dedicated repository, https://github.com/rustfs/uring (crate `rustfs-uring`),
with the full per-issue commit history preserved.

The standalone repo has independent CI that this workspace cannot run: it
excludes the crate from the build graph, so its checks passed without ever
building or testing it. rustfs/uring CI is green on fmt + clippy, a native leg
that runs the suite with real io_uring and fails on any skip, and a docker leg
exercising both the graceful-degradation and real-io_uring paths.

Removing the in-tree copy keeps rustfs/rustfs clean; the crate will be wired in
later as a git dependency behind runtime probing (rustfs/backlog#1051).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 00:06:15 +08:00
Zhengchao An 6eb238ce75 refactor(app): give each context's credential handle its own copy (#4627)
backlog#1052 S3, fifth slice (also part of what the plan called S4).

ActionCredentialHandle was a unit struct forwarding get to the process
singleton (rustfs_credentials' GLOBAL_ACTIVE_CRED), so every context
served the same credentials regardless of which server it belonged to.
The handle now keeps its own copy and gains publish():

- publish() lands on the owning context and tries to publish the process
  global; readers prefer the owned copy and fall back to the global while
  the initial startup publish still lands there — ambient callers (iam
  token signing, request-signature validation) keep working.
- Single instance: both cells are written together, so reads are
  unchanged; two contexts that both published stay isolated even though
  the global remembers only the first (covered by a new test).
- The publish return signals "took effect": true if this handle newly
  holds credentials OR the global was newly initialized, matching the
  pre-existing init_global_action_credentials fail-fast contract, now
  scoped to the handle.

The interface trait gains a default-body-friendly signature; the test
double implements it as a no-op. The startup publish path (S4) still
calls init_global_action_credentials directly; wiring it through the
handle is a follow-up.
2026-07-10 00:00:47 +08:00
houseme bf81a9bab0 fix(experiments): import io_uring cancel-safety spike and apply backlog#1051 audit remediation (#4625)
* chore(experiments): import io_uring cancel-safety spike as audit baseline (backlog#894)

Import the Spike 0 io_uring cancel-safety prototype from the closed PR #4381
branch (houseme/p2-spike0-uring-cancel-safety) as the baseline for the
backlog#1051 audit remediation. This crate is a standalone workspace and is
deliberately kept out of the main Cargo.lock/build graph (NOT production code).

Subsequent commits apply the fixes tracked in backlog#1051 sub-issues, one
commit per issue.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): drain probe SQE to its CQE before releasing buffer (rustfs/backlog#1053)

The probe path had no pending-table backstop: after pushing the read SQE,
any early return (`submit_and_wait` error, missing CQE) dropped the probe
buffer and file while the read could still be in flight in io-wq, and the
caller dropped/unmapped the ring on the error path. If the kernel then wrote
the 512-byte result into that freed heap block, it was a use-after-free — the
exact bug class this spike exists to prevent, living in its own probe path.

Fix: once the SQE is pushed, drain to its CQE via `drain_probe_cqe`, retrying
the WAIT on EINTR without re-pushing (the kernel consumed the SQE atomically
before the wait). A bounded attempt count prevents a probe against a hung
device from blocking forever; on any drain failure the buffer (and file) are
`mem::forget`-ed ("leak over UAF") so the kernel can never write into freed
memory. Unmapping the ring on its own is safe; only the user buffer must
survive.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): split probe/runtime/transient errno classes, guard offset (rustfs/backlog#1059)

`is_expected_restriction` folded EINVAL into the "environment restricted"
class, but at runtime EINVAL is triple-meaning — offset > i64::MAX (signed
loff_t), O_DIRECT misalignment, and setup entries over the cap. Implementing
the rustfs/backlog#1048 permanent-degradation latch literally against this
class would fault a healthy disk off io_uring on one alignment retry or
offset-arithmetic bug.

Document that the class is probe-time ONLY and that P2 must split errnos into
probe-restriction / runtime-parameter-error / transient (EINTR/EAGAIN). Add a
concrete guard: `submit` rejects offset > i64::MAX with an InvalidInput error
instead of letting it reach the kernel as a runtime EINVAL. The probe EINTR
half of this issue is already handled by the drain loop from rustfs/backlog#1053
(retry the wait, never re-push).

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): abort on driver-thread panic instead of freeing in-flight buffers (rustfs/backlog#1054)

The ownership model's "CQE is the only reclamation point" invariant held only
while the driver thread never unwound. On a panic inside drive(), Rust drop
order freed the `pending` table (every in-flight buffer) before the ring,
while the kernel could still be writing into those buffers → mass UAF.
`catch_unwind` cannot fix this: the destructors run during the unwind, before
the catch boundary.

Move ring + pending + backlog into a `DriverState` whose `Drop` checks
`thread::panicking()` and calls `process::abort()` BEFORE any field destructor
runs — leaving the ring mapped and the buffers allocated (leak over UAF). The
capacity-overflow panic that made this reachable (caller-controlled `len`) is
closed at the source in the len-guard commit (rustfs/backlog#1057).

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): reject reads above MAX_RW_COUNT to stop u32 truncation (rustfs/backlog#1057)

The SQE length field is `len as u32`, so len == 4 GiB became a 0-byte read the
kernel answered with res=0 → an Ok(empty) the caller decodes as a false EOF
(and len > 4 GiB read only the low 32 bits). Silent truncation (CWE-197),
forbidden by the repo's rust-code-quality rules.

`submit` now rejects len > MAX_RW_COUNT (2 GiB - 4 KiB) with InvalidInput; the
`len as u32` cast in the driver is consequently lossless. This also closes the
caller-controlled capacity-overflow panic feeding rustfs/backlog#1054. P2 must
chunk reads larger than the cap.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): resubmit short reads to satisfy the whole-range contract (rustfs/backlog#1058)

CQE res >= 0 was truncated and delivered as final with no resubmit loop, and
Pending did not even store the requested length. io_uring can legally
short-read a regular file (io-wq signal interruption, NOWAIT partial page
cache, O_DIRECT tail blocks), while LocalIoBackend::pread_bytes is a whole-
range contract — a short shard fed to EC bitrot verification surfaces as
intermittent, hard-to-attribute integrity/quorum errors.

Track offset/nread in Pending and drive a resubmit loop: a short non-EOF read
re-queues the remainder into buf[nread..], keeping the entry (and its buffer,
and in_flight) until the FINAL CQE of the logical read; res == 0 is treated as
a real EOF. The resubmitted SQE reuses the op's user_data, so a late
ASYNC_CANCEL from a dropped future still cancels the logical read cleanly.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): bound the shutdown drain and record cancel outcomes (rustfs/backlog#1055)

Shutdown made "drain to in_flight == 0" a hard precondition for unmapping the
ring, but ASYNC_CANCEL is best-effort: it cannot interrupt a regular-file read
already executing in io-wq, so on a D-state/NFS-hung disk the CQE may never
arrive and drain-to-zero never terminates — the driver loops forever and
shutdown()/Drop join blocks the caller (and any tokio worker) permanently.
This is an internal contradiction (safe unmap needs drain; a bad disk makes
drain unbounded) in the very environment io_uring exists to handle.

Add a bounded-drain escape hatch: after DRAIN_TIMEOUT with ops still in flight,
leak the whole DriverState (ring stays mapped, buffers stay allocated — leak
over UAF) and exit so shutdown() returns. Soften shutdown()'s hard assert to a
warning for that degraded path; clean-drain tests still assert in_flight == 0
themselves. Also record the ASYNC_CANCEL three-state result
(succeeded/not-found/already-executing) so the hung-disk signal is observable
instead of discarded.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): assert NODROP, monitor CQ overflow, handle EBUSY (rustfs/backlog#1056)

In-flight had no upper bound and could exceed CQ capacity (entries=64 → CQ=128)
with zero overflow handling: no NODROP check, no overflow read, no EBUSY
handling. A lost CQE means its pending entry is never reclaimed, drain never
completes and shutdown hangs — and the spike only avoided this by accidental
reliance on the io-uring crate's auto-flush + NODROP kernel + poll cadence,
all of which P2's eventfd/AsyncFd reaping removes.

Assert the NODROP feature at probe (degrade via ENOSYS otherwise), monitor the
kernel CQ-overflow counter each turn and surface a non-zero value as fatal, and
handle submit() EBUSY as CQ-overflow backpressure (keep the backlog, reap this
turn) instead of swallowing it. The hard in-flight bound (permits ≤ CQ capacity)
lands with the backpressure work in rustfs/backlog#1060.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): add backpressure with permits released at the CQE (rustfs/backlog#1060)

Submission was unbounded (unbounded mpsc + uncapped pending/backlog), so a
concurrent large-object read storm had no memory ceiling. The subtler trap:
the planned SQ-depth semaphore, implemented the natural RAII way (permit held
by the ReadHandle/future), would release permits at future drop while orphan
buffers stay resident in the pending table awaiting slow-disk CQEs —
decoupling the permit count from resident memory and reopening the DoS surface
exactly in the EC quorum-drop hot path.

Add a `Backpressure` semaphore sized to the SQ depth (entries < CQ capacity, so
CQ overflow is structurally unreachable). `submit` acquires before handing the
op to the driver; the driver releases the permit at the CQE (pending-table
removal), NOT at future drop, tying the in-flight/memory bound to actual kernel
residency. Permits are balanced on the shutting-down reject and send-failure
paths, and the driver wakes all waiters on exit.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(experiments/uring): open the probe file via O_TMPFILE instead of a predictable path (rustfs/backlog#1061)

The probe wrote a predictable temp path (uring-spike-probe-{pid}-{seq}) with
std::fs::write (O_CREAT|O_TRUNC, no O_EXCL/O_NOFOLLOW): a local attacker could
pre-plant a symlink there and have the process — often root — truncate and
overwrite an arbitrary target (CWE-59/377), with a TOCTOU window between write
and open (CWE-367). This probe is the direct blueprint for P2's per-disk
startup probe, so copied verbatim it becomes a production vulnerability.

Open via O_TMPFILE (anonymous inode, no name → nothing to plant a symlink at,
no TOCTOU, no leftover), falling back to O_CREAT|O_EXCL|O_NOFOLLOW + 0600 +
per-process nonce + immediate unlink on filesystems without O_TMPFILE. P2's
per-disk probe should create inside the tested data-disk directory the same
way, which also validates that disk's filesystem + io_uring combination.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(experiments/uring): correct invariant 2 mechanism, add invariants 6/7/8 (rustfs/backlog#1063)

Invariant 2 (the spike's flagship finding) mis-described the fd-reuse hazard:
it claimed the danger window is submission→CQE and that the kernel would
"write into someone else's file". Both are wrong — a submitted op holds a
struct file reference and is immune to fd close/reuse; the real window is SQE
construction (as_raw_fd) → io_uring_enter (backlog residency), and for a READ
the consequence is reading the WRONG file, not writing. A P2 optimization
reasoning from the false premise (drop Arc<File> after submit / registered-file
table) would step straight into it.

Correct the mechanism and add the invariants this audit hardened: driver-thread
unwind safety (6), backpressure permit released at the CQE (7), reused-buffer
content hygiene (8, detailed in rustfs/backlog#1062), plus the errno three-class
contract, bounded-drain escape hatch, and short-read resubmit responsibility.
Mark the now-remediated items in the leftover list.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(experiments/uring): pin the reused-buffer content-hygiene invariant for P3 (rustfs/backlog#1062)

The spike leaks nothing today (fresh zeroed buffer per op + truncate to res),
but rustfs/backlog#1048's P3 constraint mandates a driver-owned aligned slab
whose buffers are reused across requests as dirty memory. Both the SPIKE
invariants and the #1048 constraint address only buffer LIFETIME (UAF), not
content hygiene: once buffers are reused, any path that forgets to bound the
caller-visible bytes to cqe.res (O_DIRECT full-block read sliced upstream,
error path returning the whole buffer) discloses a previous tenant's object
data (CWE-226) in an S3 store.

Pin invariant 8: reused-buffer bytes visible to the caller must be strictly
⊆ [0, cqe.res). Documented in SPIKE.md and marked at the delivery point in the
driver so P3 preserves it; needs a dirty-buffer + short-read regression test.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(experiments/uring): pin fd ownership and orphan-integrity directly (rustfs/backlog#1064)

The memory-safety assertions were all counter proxies, and invariant 2 (fd
owned by the pending table) had zero coverage — deleting Pending.file compiled
and left every test green because each test kept its own Arc<File> alive.

Add two direct observations: pending_table_owns_fd_after_caller_drop drops the
caller's Arc while the op is in flight and asserts F_GETFD still succeeds (only
the pending table's clone keeps the fd open; removing that field would close it
→ EBADF). orphan_in_flight_does_not_corrupt_delivered_reads keeps an orphaned
buffer in flight while 64 delivered reads must return byte-exact, asserting the
orphan buffer is not reclaimed early and its kernel writes corrupt nothing. A
driver-level poison/canary leg is noted as a P2 acceptance-matrix item (ASAN
cannot see a kernel write into a freed buffer).

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(experiments/uring): cover CQ-overflow safety and read boundaries (rustfs/backlog#1065)

The suite never approached CQ capacity and never touched EOF/len boundaries.
Add no_cq_overflow_under_load (300 ops through a CQ of 128 with backpressure
capping in-flight at 64, asserting cq_overflow stays 0 and all deliver),
boundary_reads (len==0, read at EOF, a cross-EOF short read delivered to a
live receiver exercising the positioned resubmit path, and the rejected
huge-len/huge-offset guards), and pipe_half_close_reads_eof (a closed write
end surfaces res==0 EOF).

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(experiments/uring): cover Drop-without-shutdown and de-flake cancel_stress (rustfs/backlog#1066)

All tests ended via explicit shutdown(), so the UringDriver Drop impl's
live-thread branch (send Shutdown before join) was never exercised; add
drop_without_shutdown_drains_and_cancels which drops the driver with ops in
flight and asserts the held futures resolve to ECANCELED (a join-first
regression or unbounded hang makes it hang).

Also de-flake cancel_stress: the exact assert delivered == OPS/2 raced the
driver — an even-i read can complete between read_at returning and drop(handle),
delivering to the still-live receiver and flipping the split. Relax to the
deterministic conservation identity plus delivered >= OPS/2.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(experiments/uring): make run-docker.sh assert each leg's real path (rustfs/backlog#1067)

Both legs ran the identical cargo test and checked only the exit code, and a
skip is indistinguishable from a real pass at that level: leg 1 depended on the
host Docker's default seccomp "usually" blocking io_uring, and leg 2 printed
"both legs passed" even if every test skipped (vacuous pass — zero real
io_uring coverage).

Add an explicit seccomp profile (seccomp-block-uring.json) that returns EPERM
for io_uring_setup/enter/register so leg 1 deterministically hits the
graceful-degradation path regardless of host defaults, and assert leg 1
actually degraded (SKIP lines present) while leg 2 did NOT skip a single test
(io_uring really ran). Either violation now fails the harness.

Co-Authored-By: heihutu <heihutu@gmail.com>

* style(experiments/uring): apply repo rustfmt (max_width=130) to the audit changes

Normalize the formatting of the remediation code to the repo rustfmt.toml.
Pure formatting; no behavior change. clippy --all-targets -D warnings is clean.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 23:00:57 +08:00
Zhengchao An 008b872414 refactor(iam): hand the built IAM system to the AppContext explicitly (#4624)
backlog#1052 S3, fourth slice. The AppContext's IamHandle already owned an
Arc<IamSys>, but the value it held was read back from the process
singleton after init — so a future second server's context would silently
bind to the first server's IAM domain (credentials, policies, users all
belong to a specific store's .rustfs.sys).

- rustfs_iam gains build_iam_sys(store): construct an IAM system bound to
  a store without touching the singleton. init_iam_sys wraps it (build,
  publish first-wins, return the handle — previously Result<()>).
- Startup threads the built handle: the inline bootstrap path passes the
  Arc it just created into ensure_startup_after_iam, which constructs the
  AppContext from the passed value instead of re-resolving the global.
  The deferred-recovery path resolves the freshly published global
  directly (context-first resolution cannot be used there: the AppContext
  does not exist yet — creating it is the finalizer's job; caught by the
  embedded deferred-IAM e2e).
- IamHandle::is_ready() reports the held system's readiness instead of
  consulting the process singleton; the dead global re-resolver is
  removed. token_signing_key stays on the credentials global until S4.

157 iam tests + embedded e2e (basic + deferred recovery) green.
2026-07-09 22:35:00 +08:00
Zhengchao An 5fbd49a800 refactor(ecstore): move the bucket metadata system into InstanceContext (#4622)
backlog#1052 S3, third slice — the hard blocker for a second embedded
server's service stage. GLOBAL_BUCKET_METADATA_SYS was a process-global
OnceLock whose init ran .set().unwrap(): a second instance's service
startup panicked the whole process. The system it guards is inherently
per-store (it holds the store handle and that store's bucket→metadata
cache), so it now lives on the store's own InstanceContext:

- init_bucket_metadata_sys writes the cell of the store it was handed
  (api.ctx) — no signature change — and keeps the double-init fail-fast,
  now scoped to the instance (assert on the per-context set). The
  dist-erasure check for the refresh loop reads the same context.
- get_global_bucket_metadata_sys / the crate-private getter behind the
  ~20 get_*_config helpers resolve through the current_ctx() facade —
  the published store's context, or bootstrap before that — so every
  existing reader keeps its exact single-instance behavior.
- New acceptance test: two real stores in one process each initialize
  their own metadata system (the old global cell panicked right there),
  plus a shared builder extracted from the store-graph test.

201 bucket/metadata regression tests green.
2026-07-09 21:48:50 +08:00
Zhengchao An cae6189744 test(e2e): disable embedded console on raw rustfs spawn sites (#4617) 2026-07-09 12:37:18 +00:00
Henry Guo 52529403a6 test(table-catalog): record live conformance evidence (#4606)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-09 20:15:11 +08:00
Zhengchao An 476352106e refactor(app): per-context server-config handle and Arc-owned notification system (#4621)
* refactor(app): give each context's server-config handle its own copy

backlog#1052 S3, first slice: establish the per-context state pattern on
the smallest subsystem. ServerConfigHandle was a unit struct forwarding
both get and set to the process-global GLOBAL_SERVER_CONFIG, so every
AppContext shared one config regardless of which context a caller held.

The handle now keeps its own copy: a set lands on the owning context and
on the process global (ambient readers — the config loader path, the
scanner — keep working), and a get prefers the owned copy, falling back
to the global while the initial load still publishes there. Single
instance: the owned copy and the global are written together, so reads
are unchanged. Two contexts that both published stay isolated even though
the shared global only remembers the last write (covered by a new test).

The global write in set() is the transitional bridge for ambient readers;
it goes away when those readers migrate and multi-instance flips on
(backlog#1052 S5).

* refactor(notify): hand out the notification system as an owned Arc

backlog#1052 S3, second slice. GLOBAL_NOTIFICATION_SYS stored the
NotificationSys by value and every accessor returned
Option<&'static NotificationSys> — a process-lifetime borrow that a
per-server AppContext can never hold. The global now stores an Arc and
the accessor chain (ecstore runtime sources, ECStore::notification_system,
the iam forwarders, the rustfs shims, NotificationSystemInterface and the
AppContext resolvers) hands out owned Arcs instead.

Call sites are unchanged apart from two borrow adjustments in the
rebalance admin handler (pass &Arc where a reference is expected; borrow
with as_ref() so the handle survives to the second use). Behavior is
identical — same single global instance, first init still wins — but the
type now permits a future per-server context to own its notification
system (backlog#1052 S3 follow-up).
2026-07-09 20:07:09 +08:00
Zhengchao An 90c0deff58 refactor(admin): resolve the object store through the injected server context slot (#4620)
backlog#1052 S2, second slice: admin request dispatch.

Admin operations are registered as &'static dyn Operation, so unlike FS
they cannot carry per-server state. Instead, the (per-server) S3Router now
holds the server's ServerContextSlot and injects it into every request's
extensions at both dispatch points (check_access and call) — the same
extensions channel already used for ReqInfo/RemoteAddr.

- make_admin_route binds the slot; the HTTP builder passes the same slot
  the S3 service (FS) uses, so both paths dispatch to the same server.
- admin/runtime_sources gains object_store_from_req / an extensions-based
  variant for handlers that have already moved request fields out; both
  fall back to the ambient process context when no slot was injected
  (direct handler tests, non-router paths) — single-instance unchanged.
- 27 handler resolution sites across 12 files now resolve per-request;
  helper functions without request access (11 sites: site_replication
  state helpers, quota/table_catalog internals, object_zip_download
  workers) stay on the ambient path until app subsystems become
  per-server (backlog#1052 S3).
- One site (site-replication resync) resolves before the request body is
  consumed, keeping the original error precedence.

New test: the router injects its slot into request extensions by pointer
identity. Embedded e2e (basic S3 + deferred-IAM recovery) still green.
2026-07-09 19:42:52 +08:00
houseme 071a4600bc fix(admin): make server_info retry re-dial and match peer disks by resolved host (#4618)
Two follow-up fixes to #4607 (both verified real bugs, each with a regression
test):

1. The in-call server_info retry did not re-dial on network errors. A
   network-like first failure runs through `finalize_result`, which sets the
   client's `offline` gate; the retry then called `evict_connection` and
   re-invoked `server_info`, but `get_client` short-circuits on that gate and
   returns "temporarily offline" without dialing. Only the async recovery
   monitor would clear it. So the retry re-dialed only in the timeout branch and
   was a no-op in the transport/half-open branch it was meant to cover. Add
   `PeerRestClient::prepare_retry` (evict + clear the offline gate) and use it
   before the retry.

2. Synthesized/degraded drive lists went empty on hostname deployments.
   `PeerRestClient::host` is an `XHost` that `hosts_sorted` builds via
   `XHost::try_from` -> `to_socket_addrs`, so it is the resolved `IP:port`; but
   `synthesized_disks`/`peer_disk_health` compared it against
   `Endpoint::host_port()`, which is the raw `hostname:port`. On hostname
   clusters the compare missed, the drive list came back empty, and
   `unknownDisks` stayed 0 — reproducing the "drives vanish from the summary"
   regression #4607 fixed (also affected the pre-existing offline path). Compare
   through a shared `endpoint_host_matches` helper that canonicalizes the
   endpoint side through the same `XHost` resolution.

Tests: `peer_rest_client_prepare_retry_clears_offline_gate`,
`endpoint_host_matches_direct_and_canonicalized` (uses localhost, no external
DNS).

Follow-up to rustfs/rustfs#4607; tracked in rustfs/backlog#1049.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 10:50:00 +00:00
Zhengchao An f8ca79d54b refactor(server): dispatch S3 requests through a per-server context slot (#4615) 2026-07-09 10:40:14 +00:00
GatewayJ d238b2d24e fix(admin): support external OIDC browser redirects (#4280) 2026-07-09 18:07:49 +08:00
Zhengchao An c8348f8b6f fix(ecstore): merge listing channels in canonical byte order (#4610)
fix(ecstore): merge listing channels in canonical byte order (backlog#1046)
2026-07-09 17:14:57 +08:00
Zhengchao An d7b19131d8 fix(ecstore): read the persisted SSE-C nonce in the GET decrypt resolver (unbreaks S3 CI) (#4612)
fix(ecstore): read the persisted SSE-C nonce in the GET decrypt resolver

#4576 moved SSE-C encryption to a random per-encryption nonce persisted
in object metadata and taught the rustfs-layer decrypt resolver to read
it back — but the byte-level GET decryption resolves its material in
crates/ecstore object_api/readers.rs, a duplicated twin that still
recomputed the deterministic legacy nonce. Encrypt with the random
nonce, decrypt with the deterministic one: the first AEAD block fails
and every SSE-C GET aborts its body after the response headers
(IncompleteRead(0 bytes) on clients; all 8 SSE-C cases in the
implemented s3-tests whitelist), which is what has been driving the
"S3 Implemented Tests" CI job into its 60-minute timeout since
2026-07-09.

Resolve the base nonce like the encrypt side: prefer the persisted IV
(x-rustfs-encryption-iv, then the case-insensitive MinIO interop key),
fall back to the deterministic nonce only for legacy objects with no
stored IV or an undecodable value. Full implemented s3-tests whitelist
is back to 451 passed / 0 failed against the fixed binary.
2026-07-09 17:07:43 +08:00
Zhengchao An 91a5c87132 refactor(startup): thread explicit InstanceContext through the storage startup path (#4611) 2026-07-09 16:53:41 +08:00
Rohmilchkaese 8bcffd8a04 feat(helm): support multiple server pools for capacity expansion (#3325)
* feat(helm): support multiple server pools for capacity expansion

The server already understands multiple pools (RUSTFS_VOLUMES is split on
spaces, one pool expression each; rc admin pool/expand/rebalance/
decommission exist), but the chart could only render a single StatefulSet.

Add an opt-in pools mode:

- pools.enabled=false (default) renders byte-identical output to the
  current chart (verified against five values variants)
- pools.list renders one StatefulSet per entry; entries may set
  replicaCount (4 or 16) and a storageclass block, everything else is
  inherited from the top-level values
- pool 0 keeps the legacy StatefulSet/pod/PVC names and its (immutable)
  selector, so existing single-pool deployments can be expanded in place
  without renaming or data loss; additional pools render as
  <fullname>-poolN with a rustfs.com/pool label in their selector
- RUSTFS_VOLUMES and RUSTFS_SERVER_DOMAINS enumerate every pool
- pod anti-affinity is scoped per pool so pools may share nodes while
  each pool still spreads its own pods across distinct nodes
- template validation fails fast on unsupported replicaCount, an empty
  pools.list, or pools in standalone mode
- pools are append-only by design (list index = identity), documented in
  values.yaml and the README

* fix(helm): truncate pool names DNS-safely, document PDB pool scope

Truncate <fullname>-poolN to 63 chars by shortening the base name rather
than the suffix, so the pool index always survives and two pools of a
max-length release cannot collide. Document that the single
PodDisruptionBudget deliberately spans all pools.

* fix(helm): pool-mode anti-affinity must be preferred, not required

Found by live-testing in-place expansion on a 5-node cluster (4-replica
pool 0 + 4-replica pool 1): with requiredDuringScheduling anti-affinity
the expansion deadlocks in a cycle that cannot self-heal -

  1. the not-yet-rolled pool-0 pods still carry the unscoped required
     rule and repel every rustfs pod from their nodes, so part of pool 1
     stays Pending (no IP, no headless-DNS record);
  2. every pod that already has the new RUSTFS_VOLUMES exits fatally on
     'failed to lookup address information' for those Pending peers;
  3. the StatefulSet rolling update is gated on the crashing pod going
     Ready, so the remaining pool-0 pods never roll and keep repelling.

Because StatefulSets assign revisions per ordinal, even a subsequent
template fix cannot rescue a wedged rollout (Pending ordinals are only
recreated with the new template after the higher ordinals go Ready) -
the new pool's StatefulSet has to be recreated. Shipping the soft
affinity from the start avoids the state entirely; expansion was
re-tested end-to-end with it and converges.

Pool-mode rendering only - pools.enabled=false still renders the
existing required anti-affinity byte-identically.

---------

Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <cxymds@gmail.com>
2026-07-09 07:58:06 +00:00
Zhengchao An 10ba2273c4 test(e2e): align object-lock overwrite tests with AWS semantics (#4605)
test(e2e): align object-lock overwrite tests with AWS new-version semantics

PR #3179 made put-like writes (PUT, CopyObject, multipart create/complete)
on versioned object-lock buckets create a new version instead of being
blocked by the current version's legal hold or COMPLIANCE retention, which
is the correct AWS behavior. Six e2e assertions in object_lock_test.rs
still encoded the old blocking semantics and, because e2e_test is excluded
from the CI test run, stayed red unnoticed since 2026-06-03.

Verified against a real server built from current main: all six failed at
their "should be blocked" assertions. Rewritten to assert the AWS
contract: the write succeeds with a distinct new version id, the new
content is current, the protected version keeps its content and lock
metadata, and version-targeted deletion of the protected version stays
blocked (for COMPLIANCE even with bypass_governance). Full object_lock
module is green again (33/33).
2026-07-09 06:52:51 +00:00
houseme 9152da5206 chore: drop unused wildmatch workspace dependency (#4609)
PR #4468 removed the last use of wildmatch (crates/notify swapped it for a
hand-rolled star-only glob matcher), but the workspace dependency declaration
in the root Cargo.toml was left behind as an orphan. cargo shear flags it as
not used by any workspace member. Drop the stray line.

Also refresh the comments in crates/notify/src/rules/pattern.rs that still
referred to the removed wildmatch crate, so they describe the current
hand-rolled matcher instead of a dependency that no longer exists.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 06:08:49 +00:00
houseme c0c7b9b074 chore(deps): update workspace dependencies (#4608)
Run `cargo update` followed by `cargo upgrade --exclude ratelimit` to pull
the latest compatible versions across the workspace.

Manifest bumps (Cargo.toml):
- aws-config 1.8.18 -> 1.9.0, aws-credential-types 1.2.14 -> 1.3.0
- aws-sdk-s3 1.137.0 -> 1.138.0, aws-smithy-http-client 1.1.13 -> 1.2.0
- aws-smithy-runtime-api 1.12.3 -> 1.13.0, aws-smithy-types 1.5.0 -> 1.6.1
- bytes 1.12.0 -> 1.12.1, jiff 0.2.31 -> 0.2.32
- rust-embed 8.11.0 -> 8.12.0, pyroscope 2.0.6 -> 2.1.0

Lockfile-only bumps include the arrow 59.1.0 and aws-smithy families,
metrique, memchr, zerocopy, and parquet. `crypto-common` 0.1.6 -> 0.1.7
re-pins the transitive `generic-array` 0.14.x line to 0.14.7 (patch,
semver-compatible). `ratelimit` is held at 0.10.1 (2.0.0 available) as
requested; its 2.0 API is a breaking change tracked separately.

Verification: cargo check --workspace --all-targets, cargo clippy
--all-features -D warnings, cargo nextest run --all --exclude e2e_test
(8428 passed), cargo test --all --doc, and cargo deny check
advisories/sources/bans/licenses all pass.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 05:28:48 +00:00
houseme cb977c4c5a fix(admin): report unreachable info members as unknown with balanced drive counts (#4607)
fix(admin): report unreachable info members as `unknown` with balanced drive counts

`GET /rustfs/admin/v3/info` synthesizes an entry for every peer, but a peer
whose properties RPC failed below the offline threshold with no cached snapshot
was reported as `initializing` with an empty drive list. That drive list being
empty meant its drives disappeared from the backend summary entirely, so
`onlineDisks + offlineDisks` no longer equaled `totalDrivesPerSet` (an operator
sees `onlineDisks: 3, offlineDisks: 0` for a 4-drive set), and `initializing` is
a misleading state for a member that has been running for hours — it reads like
a node was ejected when nothing is wrong (upstream rustfs/rustfs#4566).

Changes:
- madmin: add `ItemState::Unknown` ("unknown") and an `unknownDisks` bucket on
  `ErasureBackend` (serde `default`, so older payloads still deserialize).
- ecstore diagnostics: `get_online_offline_disks_stats` now returns a third
  `unknown` bucket instead of folding those drives into `offline`, keeping
  `online + offline + unknown == totalDrivesPerSet`.
- ecstore notification_sys: a probe miss below the failure threshold with no
  cache is now reported as `unknown` (not `initializing`) and carries the host's
  drives from the pool topology (tagged `unknown`) so the summary stays balanced;
  drive synthesis is factored into `synthesized_disks(host, endpoints, state)`,
  shared by the offline and unknown paths.

Tracked in rustfs/backlog#1049 (P1-A + P0-A).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 04:50:20 +00:00
Zhengchao An d8c32d3754 fix(ecstore): retry idempotent reads to survive read-after-write races (#4597) 2026-07-09 04:16:43 +00:00
Zhengchao An 0c040999b1 fix(rustfs): fail GetObject stream on short body instead of truncating (#4594) 2026-07-09 04:14:25 +00:00
Zhengchao An 4f999bb6b8 perf(ecstore): backfill rename_data old size and gate the PUT prelookup (#4598) 2026-07-09 12:08:01 +08:00
Zhengchao An 579cdf52dc fix(ecstore): harden object-prefix probe and parse markers before forward_past (#4600) 2026-07-09 12:07:37 +08:00
Zhengchao An 76306d7b02 chore: rename unparseable to unparsable for the typos gate (#4602) 2026-07-09 12:07:26 +08:00
Zhengchao An 79d6de860c test(e2e): avoid console port 9001 clash and fail fast on dead server (#4603) 2026-07-09 12:07:15 +08:00
houseme 073628e8be feat(utils): make DNS resolver cache TTL configurable via RUSTFS_DNS_CACHE_TTL_SECS (#4604)
* feat(utils): make DNS resolver cache TTL configurable via RUSTFS_DNS_CACHE_TTL_SECS

The resolver cache in crates/utils/src/net.rs had a hardcoded 300s TTL with
no way to tune or disable it. On orchestrated networks (Docker Swarm,
Kubernetes) a peer's DNS name is its only durable identity while its container
IP changes on reschedule, and the platform DNS always serves the current
answer. A fixed 300s application-side cache can keep a member dialing a dead IP
for up to five minutes after a peer restarts. Rust has no runtime DNS cache, so
on musl images this was the only cache in the stack and the only one that could
not be turned off.

Read the TTL from RUSTFS_DNS_CACHE_TTL_SECS (u64, default 300), matching the
existing RUSTFS_INTERNODE_* transport knobs. `0` disables caching entirely so
every get_host_ip consults the system resolver. The resolved value is logged
once at first use, removing an invisible cache from the triage surface. Change
is backward-compatible and confined to net.rs.

Also drop the two per-lookup info! logs on the resolve path: they would flood
the log when caching is disabled and added hot-path noise otherwise.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(helm): derive mTLS secret names from chart fullname (#4601)

* fix(helm): mtls certificate for server and client hardcode

* fix hardcode issue in deployment

* update typo yaml file

---------

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com>
2026-07-09 03:26:06 +00:00
houseme e0eee89a3b fix(ecstore): emit CommonPrefix for object with same-named prefix in non-recursive listings (#4596)
fix(ecstore): emit CommonPrefix for object with same-named prefix in non-recursive listings (backlog#1042)

On single-disk / consistent deployments a non-recursive scan_dir that finds a/xl.meta classifies `a` as an object and does not descend, so the source never emits the prefix `a/`, dropping the CommonPrefix from delimiter listings even when `a/b` exists. This is the single-disk follow-up to backlog#880 (the multi-disk merge path was fixed in #4563).

Fix: in the scan_dir Ok branch, for a non-recursive listing of a plain object, probe whether `a/` holds a listing entry other than the object itself (new object_prefix_has_sibling_listing_entry, reusing directory_has_visible_listing_entry and skipping the object's own xl.meta and data dirs); if so, additionally schedule the prefix dir `a/`. The upstream fold in from_meta_cache_entries_sorted_infos already emits object `a` as Contents and dir `a/` as a CommonPrefix, so nothing changes there. Leaf objects (the common case) pay a single list_dir and return false immediately.

Verification: new scan_dir unit test (positive: `a` + `a/b` coexist; negative: leaf `c` produces no spurious `c/`); new end-to-end e2e (object `a` + `a/b` with delimiter "/" -> Contents `a` + CommonPrefix `a/`); existing list_objects_duplicates e2e (#1797 / #2439 regressions) stays green; set_disk::read 114 passed.

Note: lib-test compilation depends on #4573 (now merged), which added the allow_inplace_legacy_fallback argument to the codec streaming arity tests after #4560 / backlog#879.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 01:45:25 +00:00
houseme cb9edd59f5 test(ecstore): serialize bucket_delete tests to fix cargo-test global race (#4595)
The three store::bucket::tests::bucket_delete_* tests drive make_bucket /
delete_bucket through the process-global local-disk registry
(GLOBAL_LOCAL_DISK_MAP) and lock client, and through Sets::new which reads the
process-global erasure mode. Under `cargo test` (single process, many threads)
they race other global-touching tests and fail deterministically with
InsufficientWriteQuorum.

serial_test's #[serial] with the crate default key serializes them across the
in-process suite. Measured at --test-threads=16: baseline fails all three every
round; with #[serial] all three pass every round. nextest's per-test processes
are covered separately by the ecstore-serial-flaky test-group in
.config/nextest.toml (#4558). Full instance-level isolation remains a backlog
#939 (InstanceContext) concern and is not attempted here.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 01:37:53 +00:00
Zhengchao An 19a0a73fed fix(rustfs): sanitize user-metadata in metadata=true object listing (#4592)
fix(rustfs): sanitize user-metadata in metadata=true object listing (#2743)

The console listing path (`list-type=2&metadata=true`) serializes each
object's user-metadata key as a raw XML *element name* and its value as
XML text without validation. User-metadata keys derived from HTTP
headers can legally contain characters that are illegal in an XML `Name`
(space, `$`, `%`, `#`, a leading digit, control bytes), and values can
contain C0 control characters that are illegal in XML 1.0 text.

A single object carrying such a key or value produced a malformed
`ListBucketResult` document, which the console's XML parser rejected
wholesale — so every object under that prefix vanished from the Web UI,
while plain `ListObjectsV2` (which never serializes user metadata) kept
working. The breakage appeared the moment any one object in a prefix had
XML-unsafe metadata and cleared once that object was removed, matching
the report.

Guard the serialization in `ObjectMetadataExtension::serialize_content`
(shared by the list-objects and list-versions metadata outputs): skip
entries whose key is not a valid XML element name, and strip
XML-1.0-illegal control characters from values. A single poison object
can no longer corrupt the whole listing document.
2026-07-09 09:36:10 +08:00
Zhengchao An e0f04f4621 fix(ecstore): only evict remote lock channel on transport failures (#4591)
fix(ecstore): only evict remote lock channel on transport failures (#4567)

The remote lock RPC path evicted the cached gRPC channel on *any*
`tonic::Status`. Genuine transport failures (connect refused/reset,
keepalive/deadline) surface that way, but so do server-produced
application statuses on a perfectly healthy channel: `Unauthenticated`
/`PermissionDenied` from the signature interceptor, `Internal`
/`FailedPrecondition` when the peer lock service is not ready yet,
`InvalidArgument`, `ResourceExhausted`, `Unimplemented`, and so on.

Evicting the channel for those cannot help — the channel is fine — and
each spurious eviction re-dials the connection and advances the peer
toward the offline threshold via `record_peer_unreachable`, producing
background channel churn on an otherwise healthy cluster.

Classify the tonic status with `is_transport_failure` (underlying
transport source present, or one of `Unavailable`/`DeadlineExceeded`
/`Unknown`/`Cancelled`) and only evict the cached channel when it is a
real transport failure. The client-side timeout arm still evicts. This
mirrors the transport-vs-application distinction already used on the
remote-disk RPC path.
2026-07-09 09:35:48 +08:00
Zhengchao An ce6bc30b26 test(ecstore): harden validation gate and EC coverage tests (#4590) 2026-07-09 07:18:18 +08:00
Zhengchao An 359bdc0f1f refactor(ecstore): migrate the background-services cancel token into InstanceContext (Phase 5 Slice 13) (#4586)
* refactor(ecstore): migrate the background-services cancel token into InstanceContext (Phase 5 Slice 13)

Phase 5 Slice 13 (backlog#939): move the background-services cancellation token
out of the process static into the per-instance InstanceContext, so cancelling
one instance's background workers (scanner/heal/tier/lifecycle) no longer
touches another instance.

- InstanceContext gains `background_cancel_token: OnceLock<CancellationToken>`
  with `init_background_cancel_token` (set-once) and `background_cancel_token()`
  returning an owned clone.
- global.rs `init_/get_/create_/shutdown_` helpers keep their signatures and
  route through the current instance's context; the static is removed. The
  getter now returns an owned `Option<CancellationToken>` instead of a
  `Option<&'static _>`, which is what lets the token live in the context.
- Callers adapt to the owned token: the metadata-refresh loop drops `.cloned()`;
  the lifecycle worker/loops take the owned token (a shared fallback is cloned
  when the token is somehow uninitialized). No Arc cycle is introduced —
  workers hold a token clone, not the instance context.

Single-instance behavior is unchanged: startup creates one token in the
bootstrap context the ECStore adopts, and shutdown cancels that same token.

Tests: the token is set-once and cancelling one instance's token leaves a
distinct instance uninitialized.

Verification: cargo test -p rustfs-ecstore (23 instance-context tests green),
cargo clippy -p rustfs-ecstore --all-targets (clean), make pre-commit (pass).

Refs: backlog#939 (Phase 5, Slice 13)

* test(ecstore): prove multi-instance isolation; document embedded guard retention (Phase 5 Slice 14) (#4588)

Phase 5 (backlog#939) capstone. The prior 13 slices moved every piece of
per-instance runtime state out of process globals into ECStore's
InstanceContext. This slice proves the result and records the remaining work.

- Add `two_instances_isolate_all_migrated_state`: an end-to-end acceptance test
  that constructs two independent InstanceContexts and verifies NONE of the
  migrated state is shared — erasure setup, lock manager, region, deployment id,
  the four service handles (tier/notifier/expiry/transition), the local disk
  registry, the bucket monitor, and the background cancel token. This is the
  object-graph isolation carrier working end to end.
- Document why the embedded single-instance guard (EMBEDDED_SERVER_STARTED) is
  intentionally retained: storage startup still publishes into the process-level
  bootstrap context (write-once region/endpoints/deployment id) and the single
  GLOBAL_OBJECT_API handle, so a second startup would fail-fast on that shared
  state. Lifting the guard requires threading a per-instance context through
  startup — a follow-up beyond migrating the globals. The guard is NOT removed:
  rejecting the second start is safer than the panic it would otherwise become.

Verification: cargo test -p rustfs-ecstore (acceptance test + all instance-context
tests green), cargo clippy -p rustfs-ecstore --all-targets (clean), make
pre-commit (pass).

Refs: backlog#939 (Phase 5, Slice 14). Stacked on Slice 13 (#4586).
2026-07-08 22:06:31 +00:00
Zhengchao An c0e2c02e51 fix(rio): warn once when internode HTTP/2 tuning is inert on plaintext (backlog#805-C3) (#4587)
The internode HTTP/2 window/keepalive tuning (apply_http_client_tuning +
http2_keep_alive_* in build_http_client) only takes effect when the connection
negotiates HTTP/2, which happens via TLS-ALPN. Over plaintext internode
transport the connection is HTTP/1.1 and all the tuning is silently inert,
with no signal to the operator.

Make this honest without changing protocol behavior (no h2c, no
prior_knowledge, no opt-in flag):
- InternodeHttpClientTuning gains h2_tuning_explicit, true when the operator
  set a window size, a non-default tuning profile, or the keepalive env.
- Add pure predicate should_warn_h2_inert(negotiated_is_http2,
  h2_tuning_explicit, already_warned) and maybe_warn_h2_inert, gated by a
  process-lifetime AtomicBool so the warn fires at most once.
- Call maybe_warn_h2_inert at both record_internode_http_version sites using
  the actual negotiated resp.version().
- Document the TLS-ALPN requirement at apply_http_client_tuning and near the
  ENV_INTERNODE_HTTP2_* constants.

Tests cover the should_warn_h2_inert truth table and that h2_tuning_explicit
reflects explicit windows and a non-default profile.
2026-07-09 05:51:07 +08:00
Zhengchao An b06cbd335e fix(rio-v2): detect DARE stream truncation at package boundary (#4585) 2026-07-09 05:44:32 +08:00
Zhengchao An e029b77c09 fix(server): count in-flight HTTP requests with an RAII guard (exactly-once) (backlog#806) (#4583)
fix(server): count in-flight HTTP requests with an RAII guard (backlog#806-35)

The active-requests gauge was maintained with tower-http TraceLayer hooks:
on_request (+1), on_response (-1) and on_failure (-1). With the default
ServerErrorsAsFailures classifier a 5xx response fires BOTH on_response and
on_failure, so every 5xx decremented the gauge twice (net -1); a streaming
200 that failed mid-body did the same. The gauge drifted downward and
saturated at zero, corrupting the readiness busy-protection signal
(alias_busy_threshold_exceeded).

Replace the hook arithmetic with an InFlightGuard (RAII): InFlightLayer wraps
each request future, incrementing on entry and decrementing exactly once when
the future resolves (any status) or errors/cancels. Removed the +1 from both
on_request hooks and the -1 from trace_on_response and both on_failure hooks,
keeping their tracing and failure-metric work intact. Applied to both the
external and internode stacks.

Added a test driving the layer for 2xx, 5xx and a no-response service error,
asserting the gauge nets to zero in every case (the 5xx path is the fix).
2026-07-09 05:44:05 +08:00
Zhengchao An 37c1153c1e fix(ecstore): age LastMinuteLatency samples individually to fix the 1-minute window (backlog#806) (#4581)
fix(ecstore): age LastMinuteLatency samples individually (backlog#806-16)

The rolling one-minute latency window stored bare Duration samples plus a
single, never-updated start_time. Its retain predicate ignored the element
and evaluated the constant now.duration_since(start_time) < 60s, so once the
window had existed for 60s every add() dropped ALL samples and the average
degenerated to the latest single sample.

Each sample now carries its own Instant and is retained by its individual
age. The type backs only in-memory EpHealth + admin display (the wire shape
is target::LatencyStat with curr/avg/max), so Serialize/Deserialize is kept
only to satisfy the enclosing LatencyStat derive; the sample Instant is
serde(skip) and restarts aging on reload.
2026-07-09 05:42:51 +08:00
Zhengchao An ed36d1ed97 fix(io-metrics): drop client-controlled bucket label from s3 ops counter (backlog#806) (#4580)
fix(io-metrics): drop client-controlled bucket label from rustfs_s3_operations_total (backlog#806-22)

The bucket dimension on the rustfs_s3_operations_total counter is
client-controlled and unbounded, letting any client explode metric
cardinality (a Prometheus/OTEL cardinality DoS that grows the in-process
OTEL aggregation store even without a scrape). Drop the bucket label so the
series is keyed by the bounded op dimension only (<=122 variants), matching
MinIO which never labels its default operation counters with bucket.

BREAKING: rustfs_s3_operations_total no longer carries a "bucket" label.

record_s3_op(op, bucket) -> record_s3_op(op); all call sites in
rustfs/src/storage/{ecfs.rs,helper.rs} updated (bucket bindings retained
where still used by audit/notify paths). Add metrics-util debugging recorder
dev-dep and tests asserting the series carries only the op label and that
series count equals distinct-op count.
2026-07-09 05:42:27 +08:00
Zhengchao An e4f75c4856 fix(admin): fail closed when DeleteServiceAccount can't load the target (backlog#806) (#4579)
DeleteServiceAccount looked up the target service account and, on any
get_service_account error OTHER than not-found (e.g. a stored-credential decode
error), set svc_account = None and continued. The non-admin owner guard then
used svc_account.is_some_and(|v| v.parent_user != user), which is FALSE for a
None target, so the guard was skipped and the code fell through to the delete —
a non-owner could delete a service account whose stored credential failed to
decode (authz bypass, fail-open).

Extract a fail-closed helper non_admin_may_delete_service_account(caller, target)
that returns true only when the target loaded AND is owned by the caller; a None
target (unverifiable ownership) or an owner mismatch now denies. Admins (holding
RemoveServiceAccount) are unaffected. + unit test.

Refs rustfs/backlog#806
2026-07-09 05:41:57 +08:00
Zhengchao An da67d6d695 fix(rio): use typed-first internode HTTP error classification (#4578)
fix(rio): typed-first internode HTTP error classification (backlog#805)

classify_reqwest_error matched reqwest/hyper error text by English
substrings, brittle to library/OS-locale wording. Refactor to a pure,
testable classify_transport_error(err, is_timeout, is_connect, is_body)
that trusts structured signals first: caller timeout, then the io::Error
kind found anywhere in the source chain (typed wins over any string/body
signal so a real ConnectionRefused is never mislabeled DnsResolutionFailed),
then a DNS-only string heuristic gated behind is_connect, then body, then
Unknown.

Flip DnsResolutionFailed to retryable, mirroring MinIO IsNetworkOrHostDown
(*net.DNSError => network-or-host-down => retryable); RustFS already retries
transient DNS at endpoints.rs, and bounded retries cost at most one extra
attempt on permanent NXDOMAIN.

Swap the ecstore remote_disk non-retryable exemplar from DnsResolutionFailed
to Unknown accordingly.
2026-07-09 05:41:25 +08:00
Zhengchao An f38006868f docs(agents): add adversarial validation policy and role playbooks (#4589)
Add a default-on multi-role adversarial validation policy to the root
AGENTS.md (risk tiers, six reviewer roles, findings protocol, exit
criteria), a shared adversarial-validation skill with repo-specific
attack probes grounded in shipped bugs, seven audit fixes to the root
instruction files, and an actionlint gate for workflow changes.
2026-07-09 05:40:01 +08:00
Zhengchao An 3ac3c27e39 fix(sse-c): use a random per-encryption nonce and persist it (#4576)
In the default (non-rio-v2) build, SSE-C encrypted with AES-256-GCM using a deterministic nonce derived from MD5("{bucket}-{key}"), and decrypt recomputed it. Overwriting the same object under the same SSE-C key reused an identical (key, nonce) pair, breaking AES-GCM confidentiality and integrity. The base nonce was never persisted.

Generate a fresh random 12-byte nonce for every single-PUT and multipart session, persist it under both x-rustfs-encryption-iv and the MinIO interop key, and resolve it from stored metadata on decrypt and multipart part encryption. Objects written before this change carry no stored IV and fall back to generate_ssec_nonce, so existing objects still decrypt.

Refs rustfs/backlog#1039
2026-07-09 05:33:23 +08:00
Zhengchao An ddb9d8ca3e fix(object-capacity): harden write timing, interval bounds, and scope expiry (#4575)
fix(object-capacity): harden clock handling in write window, background intervals and scope registry

Three low-severity robustness gaps (S32+S33+#35):

- WriteRecord keyed its 60-bucket write window by wall-clock unix
  seconds while the debounce used Instant. An NTP step backwards marked
  recent buckets as future and silently suppressed write-triggered
  refreshes until the wall clock caught up; a forward step aged the
  whole window at once. Bucket keys now derive from a monotonic
  per-record epoch, immune to clock steps.
- Background refresh/metrics intervals were only clamped at zero;
  RUSTFS_CAPACITY_SCHEDULED_INTERVAL=u64::MAX overflowed
  Instant + Duration and panicked the spawned task, silently killing
  scheduled refreshes. Intervals now clamp into [1s, 30 days] via one
  helper with a structured warn.
- record_capacity_scope merged new disks into an expired entry and
  refreshed its recorded_at, resurrecting a scope take_capacity_scope
  would have discarded (and the merge path never pruned). Expired
  entries are now replaced, not merged into.

Ref: rustfs/backlog#1022 (S32+S33+#35 from audit rustfs/backlog#1010)
2026-07-09 05:27:36 +08:00
Zhengchao An c92d47df97 chore(security): exclude test/dev paths from secret scanning (#4584) 2026-07-09 05:23:22 +08:00
Zhengchao An 2ae1e8ad05 ci: right-size pipelines, fix prerelease latest.json overwrite (#4582)
- build.yml: update latest.json only for stable release tags
  (alpha/beta/rc tags previously overwrote the stable pointer with
  release_type "stable"); drop the placeholder .asc files; build the
  Linux targets only on main pushes (tags/schedule/dispatch keep the
  full matrix); remove the unreachable --build clause and a needless
  full-history clone
- ci.yml: event-scoped concurrency so merges stop cancelling the
  weekly scheduled run; drop the redundant skip-duplicate-actions
  gate job; run clippy before tests; trim the unused toolchain from
  the typos job
- ci-docs-only.yml (new): satisfy the required "Test and Lint" check
  on docs-only PRs that ci.yml skips via paths-ignore
- audit.yml: drop cargo-audit (cargo-deny advisories covers the same
  RustSec database); same event-scoped concurrency fix
- docker.yml: job-level short-circuit for per-merge dev builds; send
  the Trivy SARIF to code scanning; unify the upload-artifact pin
- performance-ab.yml: add concurrency; only re-run on labeled events
  when the added label is perf-ab
- stagger the Sunday crons (build 01:00, audit 03:00,
  nix-flake-update 05:00, mint 06:00)
- delete performance.yml: disabled since 2025-07; idle-server
  profiling, no benchmark baseline, stale ecstore package name
2026-07-09 05:22:39 +08:00
houseme 506ded59ad fix(metrics): servers_offline_total counts each peer once (normalize peer-health key) (#4572)
fix(metrics): normalize peer-health key so servers_offline_total counts each peer once

rustfs_cluster_servers_offline_total is the count of distinct keys in the
addr-keyed CLUSTER_PEER_HEALTH map. The data path keys a peer by
endpoint.grid_host() (scheme://host:port) while the lock path keys it by
url::Url::to_string() (scheme://host:port/, trailing slash), so one physical
peer becomes two health entries and a single downed node reports 2 (2N for N).

Normalize the address (trim trailing slashes) at the map boundary in
record_peer_reachable/record_peer_unreachable/cluster_peer_is_offline/
cluster_peer_should_bypass so both forms collapse to one canonical key.
Pure observability fix; peer selection and quorum are unchanged.

Fixes rustfs/backlog#1043

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 20:56:08 +00:00
Zhengchao An cf2da0c44d refactor(object-capacity): remove the decorative SymlinkTracker and its no-op depth knob (#4571)
The tracker never influenced traversal: walkdir's follow behavior is
fixed up front by follow_links(), and should_follow() only gated the
tracker's own bookkeeping. Its 'depth limit' compared tree depth (not
symlink chain depth), so RUSTFS_CAPACITY_MAX_SYMLINK_DEPTH was a
complete no-op while its telemetry claimed symlinks were skipped that
walkdir had in fact followed and counted; record_symlink was always
called with size 0, so tracked_bytes never left zero (S12).

Remove the tracker, its skipped/summary events, the symlink metric and
the depth env knob end to end (the env's 'as u8' truncation goes with
it), and document the real semantics at the walker: follow_links(true)
counts targets with walkdir's ancestor-loop detection breaking cycles,
follow_links(false) — the default — counts no symlink targets. The scan
root itself is pre-resolved since backlog#1015.

Ref: rustfs/backlog#1018 (S12 from audit rustfs/backlog#1010)
2026-07-09 04:55:09 +08:00
Zhengchao An 2dfad181a0 fix(admin): include accountinfo bucket quota
Expose accountinfo bucket quota only when a bucket quota is configured while preserving the existing response shape for buckets without quotas.
2026-07-09 04:52:52 +08:00
Zhengchao An e468648f0f fix(utils): map remaining Linux uapi filesystem magic values
Map the remaining stable Linux uapi filesystem magic constants and keep values without a stable uapi source explicitly unknown.
2026-07-09 04:52:43 +08:00
Zhengchao An 05890d6e25 fix(ecstore): pass the new allow_inplace_legacy_fallback arg in codec streaming arity tests (#4573)
PR #4560 added a 15th parameter (allow_inplace_legacy_fallback) to
build_codec_streaming_part_reader while the negative arity tests from a
concurrently developed branch still call it with 14 arguments — the two
changes merged cleanly textually but broke the workspace test build on
main (E0061 in set_disk/read.rs), failing CI for every open PR.

Pass false at both call sites: these tests assert Err outcomes
(oversized part, missing quorum) that are independent of the fallback
mode, and false preserves the historical whole-request fallback
semantics used by eager first-part reads.
2026-07-08 20:23:29 +00:00
Zhengchao An 90d769167b test(ecstore): pass codec fallback flag in tests
Pass the explicit allow_inplace_legacy_fallback flag in codec streaming reader tests so CI builds compile after the signature change.
2026-07-09 04:22:19 +08:00
Henry Guo 91a23361ee fix(ecstore): tolerate completed metacache producers (#4531)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-09 04:10:57 +08:00
Henry Guo c6d054245f fix(admin): refresh datausage live bucket usage (#4490)
* fix(admin): refresh datausage live bucket usage

* fix(admin): reapply datausage memory overlay

* fix(ecstore): avoid runtime lookup for empty shard costs

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-09 04:09:40 +08:00
Zhengchao An bf287ec5c6 fix(admin): notify site replication after bucket metadata import (#4562)
fix(admin): replicate imported bucket metadata
2026-07-09 04:01:44 +08:00
Zhengchao An 32b1094ec3 fix(object-capacity): resolve a symlinked scan root instead of silently counting zero (#4564)
walkdir defaults to follow_root_links=true precisely so a root that is
itself a symlink still descends, but the scanner overrode it with the
general follow_symlinks flag (default false). For the common indirection
layout (/data/rustfs0 -> /mnt/vol0) the walk yielded a single skipped
symlink entry and returned used_bytes=0, file_count=0, is_estimated=false
with no error and no log — an exact 0 committed straight into the
per-disk baseline (S05).

Canonicalize the scan root before walking (also covering nested
indirection and keeping the dedicated-mount statvfs check on the real
path); resolution failures surface as scan errors instead of a silent
zero. Inner-symlink policy is unchanged.

Ref: rustfs/backlog#1015 (S05 from audit rustfs/backlog#1010)
2026-07-09 03:43:05 +08:00
houseme 7e1f7f242e fix(ecstore): preserve CommonPrefixes when an object and same-named prefix dir coexist in merge (backlog#880) (#4563)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 19:40:13 +00:00
houseme 8fc75e88c8 test(ecstore): multi-disk regression for read-before-write tagging under early-stop (backlog#881) (#4561)
test(ecstore): multi-disk regression for read-before-write tagging under early-stop

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 19:22:06 +00:00
houseme 9636989fef perf(ecstore): in-place per-part legacy degradation for lazy multipart codec reader (backlog#879) (#4560)
perf(ecstore):  in-place per-part legacy degradation for lazy multipart codec reader

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 19:16:32 +00:00
Zhengchao An 787cc77a7e fix(object-capacity): clamp zero capacity env values to safe defaults (#4559)
RUSTFS_CAPACITY_MAX_FILES_THRESHOLD=0 made every file count as
overflow with an empty exact prefix, so disks with fewer files than the
sample rate committed 0 bytes as the cluster total; STAT_TIMEOUT=0 with
dynamic timeout disabled made every scan fail at the first entry. Both
were accepted unvalidated, unlike the already-clamped sample_rate and
interval values.

Zero values for the threshold and the stat/min/max timeouts now fall
back to their defaults with a single structured warn at config load
(S11). The symlink-depth knob is left untouched here: it is removed
entirely by the backlog#1018 fix.

Ref: rustfs/backlog#1019 (S11 from audit rustfs/backlog#1010)
2026-07-09 03:14:11 +08:00
Zhengchao An ed81d2f6b8 test(ecstore): complete EC validation coverage gate
* test(ecstore): complete EC validation coverage gate

* test(ecstore): stabilize validation suite after rebase

* test(ecstore): fix rio-v2 clippy lint
2026-07-09 03:12:48 +08:00
houseme 7c701d9f2c test(ecstore): serialize load-sensitive flaky tests via nextest test-group (#4558)
Two ecstore test groups pass in isolation but flake under the full parallel
nextest suite, producing CI noise on every in-flight PR (backlog #937):

- store::bucket::tests::bucket_delete_* race make_bucket into
  InsufficientWriteQuorum because they share global disk-registry/lock-client
  state with other concurrently running tests.
- bucket_lifecycle_ops::tests::concurrent_resend_same_part_commits_one_generation
  exceeds its already-maxed 60s lock-acquire deadline only when the suite
  saturates disk I/O.

serial_test's #[serial] does not serialize these across nextest's per-test
process boundary. Add a nextest test-group (max-threads = 1) that matches both
groups so they run serialized, removing the load-induced flake. No test or
production code changes; long-term global-singleton isolation is tracked
separately.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 19:12:06 +00:00
Zhengchao An 1bd0f49c7a fix(admin): populate accountinfo bucket details (#4547) 2026-07-09 03:02:19 +08:00
Zhengchao An 68c3278efd test(utils): unignore drive stats platform test (#4549) 2026-07-09 03:02:13 +08:00
Zhengchao An 276a4f24c0 test(utils): extend Linux fs type mappings (#4556)
test(utils): extend linux fs type mappings
2026-07-09 03:01:57 +08:00
Zhengchao An 0ebe00b383 fix(object-capacity): drive scan progress checks by visited entries and remove unreachable stall detector (#4557)
Progress/timeout checks only ran when file_count advanced, so trees of
directory-only or error-dense entries (dirs, traversal errors, symlinks
never increment file_count) bypassed the entire cooperative time budget
and held the blocking thread for unbounded time, while each error entry
logged its own warn — permission-dense trees flooded the log (S13).
The stall detector was structurally unreachable: it fired on 'no file
progress between checks', but checks themselves only ran when file
progress happened, so RUSTFS_CAPACITY_STALL_TIMEOUT never did anything
since its introduction (S09).

- Progress checks (timeout + early-sampling entry) are now driven by a
  visited-entry counter that also advances on directories and errors, so
  every tree shape reaches the budget checks.
- Remove the unreachable stall detector and its plumbing end to end
  (ProgressMonitor fields, ScanLimits, config getter, env const, stall
  metric). Genuine walker wedges are handled by the hard outer
  wall-clock budget from backlog#1017; no decorative protection is left.
- Cap per-entry error warns at 10 per scan with an explicit suppression
  notice; had_partial_errors still records the condition.

Ref: rustfs/backlog#1016 (S09+S13 from audit rustfs/backlog#1010)
2026-07-09 02:47:17 +08:00
houseme d232a46b4d perf(ecstore): wire legacy decode stripe prefetch behind default-off gate (HP-9 step 2) (#4542)
perf(ecstore): wire legacy decode stripe prefetch / bitrot-decode overlap (backlog#930 HP-9 step 2)

The legacy GET decode duplex loop (default GET path + every Range request)
was strictly serial: read a stripe, reconstruct it, emit it, and only then
begin reading the next stripe. Two switches introduced by PR#3972 to overlap
this work — RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT and
RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE — were config-only with zero call
sites since introduction.

Wire both into the loop as a depth-1 stripe prefetch: while the current
stripe is reconstructed and emitted, the next stripe's shard reads (including
bitrot verification) run concurrently, hiding read latency under the emit /
duplex-backpressure stage. ParallelReader::read is inherently serial (it takes
&mut self and advances a shared stripe cursor), so at most one read can be in
flight; a prefetch count above 1 therefore collapses to the same
single-stripe-ahead pipeline rather than reading several stripes ahead. Both
switches feed one gate, legacy_stripe_prefetch_enabled().

Default (count == 1, overlap == false) keeps the loop on the pre-existing
strictly-serial path, byte for byte: the prefetch pipeline is a separate branch
and the serial branch is unchanged. The reconstruct/emit body is factored into
a shared emit_decoded_stripe helper used by both branches so error attribution,
read-quorum handling, reconstruction verification and stage metrics cannot
drift between them.

Correctness guarantees preserved under prefetch:
- A speculatively prefetched read for stripe N+1 is only consumed when the loop
  reaches N+1; if stripe N stops the loop, the in-flight read is awaited and
  dropped, so its errors never surface against stripe N.
- Bitrot (HighwayHash) verification runs inside each stripe read and is not
  bypassed or reordered; a corrupt shard is still rejected and, when
  unrecoverable, the read fails without emitting garbage.
- Shard buffers are recycled only after the overlapping next read has claimed
  its own — one extra stripe of memory (double buffering) with buffer reuse
  preserved at a one-stripe lag.
- Per-stripe exact length advance (backlog#799 B2), lockstep reconstruction
  verification (backlog#832) and the hash_size == 0 pass-through are unchanged.

Adds regression tests exercising serial-default, count>1 and overlap configs:
full/range reads byte-exact, degraded (missing-shard) reconstruction, corrupt
shard rejected-but-recovered, unrecoverable corruption erroring with no output,
late-stripe failure attributed correctly with stripe 0 still emitted,
hash_size == 0 pass-through, and the gate defaulting off.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:35:11 +00:00
houseme e3e5693e60 fix(ecstore): correct heal result drive-record reporting (#4555)
Two report-only defects on the heal result-reporting surface (they do not
affect the data path or heal decisions):

- default_heal_result (set_disk/ops/heal.rs): the offline-disk branch pushed
  an Offline record but fell through into the unconditional push, emitting a
  second (Corrupt) record for the same disk. This grew before/after.drives to
  disk_count + offline_count and misaligned every entry after the first offline
  slot. Add `continue` after the offline push, drive `disk_len` and the loop
  from a single `self.disks` snapshot, and assert `errs.len() == disk_len`.

- Sets::heal_format (core/sets.rs): the before/after drive lists were
  pre-filled with N default placeholders and then N real entries were pushed,
  yielding a 2N list whose healed status updates (indexed 0..N) landed on the
  blank placeholder half. Assign the lists directly from formats_to_drives_info
  (mirroring the set-level heal_format) so the healed updates hit the real
  entries.

Add regression tests covering offline/online record alignment and the
NoHealRequired and heal paths of the pool-level heal_format.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:29:54 +00:00
houseme 608ab14d7d perf(ecstore): fold metadata read open+fstat+read into a single spawn_blocking (HP-12 item 1) (#4554)
Sub-change A only: the two local-disk metadata read paths
(`read_metadata_with_dmtime`, `read_all_data_with_dmtime` in
crates/ecstore/src/disk/local.rs) previously dispatched open, fstat and the
xl.meta read as three separate async fs hops (three spawn_blocking round-trips
under tokio::fs). Each is now folded into a single tokio::task::spawn_blocking
closure using std::fs, cutting per-metadata-read dispatch from 3 to 1.

This does NOT touch sub-change B (removing the entry-point access() call).

Correctness first: this is the hottest metadata read path, so the refactor
preserves byte-for-byte-equivalent Results. Every error mapping is kept
identical:
  - open failure     -> to_file_error
  - is_dir           -> Error::FileNotFound (not to_file_error(EISDIR))
  - metadata failure -> to_file_error
  - xl.meta parse    -> propagated verbatim from the parser
  - try_reserve      -> Error::other
  - read_to_end      -> to_file_error
For read_all_data_with_dmtime the async NotFound -> access(volume_dir) ->
VolumeNotFound fallback (and its warn! event) is preserved on the async side:
the closure returns the raw open error unmapped; only open() can yield ENOENT
once the fd is valid, so gating the fallback on the open error is equivalent to
the original open-arm-only fallback.

To run the parser inside a blocking closure, filemeta gains a synchronous twin
`read_xl_meta_no_data_sync` (+ `read_more_sync`) in
crates/filemeta/src/filemeta/version.rs that line-for-line mirrors the async
version, differing only in std vs tokio read_exact. A new equivalence test
(`read_xl_meta_sync_equivalence_tests`) feeds identical buffers to both the
async and sync readers and asserts equal Ok bytes / equal Err variants across:
v1.0; v1.1/v1.2/v1.3; large meta triggering read_more; header truncation ->
UnexpectedEof; CRC-trailer truncation -> FileCorrupt; unknown major/minor ->
InvalidData; and want boundaries (exact fit, inline-data drop, read_more EOF).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:28:57 +00:00
houseme f7d2b25638 fix(ecstore): propagate disk delete/rename failures instead of swallowing them (#4546)
Two disk-layer correctness fixes in crates/ecstore/src/disk/local.rs.

move_to_trash (#948, ECA-07) previously handled only DiskFull in its
tail error block and let every other rename failure (EIO/EACCES/ENOTDIR/
cross-device) fall through to `return Ok(())`, so a delete that never
landed on a faulty disk was reported as success and the drive fault was
hidden from heal/offline logic. It now keeps the DiskFull in-place-remove
fallback, treats a missing source (FileNotFound) as benign, and
propagates every other error (already mapped by to_file_error, e.g.
I/O -> FaultyDisk, permission -> FileAccessDenied), matching MinIO's
deleteFile.

rename_file and rename_part (#960, ECA-19) directory (trailing-slash)
branch unconditionally removed the destination before rename and
propagated the resulting NotFound, so renaming a directory to a new
location always failed. Both now tolerate ErrorKind::NotFound on that
pre-rename remove and continue, matching MinIO's RenameFile.

Adds regression tests covering benign-missing-source, real-error
propagation, the unchanged move happy path, and directory rename to a
missing destination for both rename_file and rename_part.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:27:11 +00:00
houseme 726f3dc185 fix(ecstore): accept empty remote version_id in tier recovery paths (#4552)
An object transitioned to an unversioned remote tier legally records an
empty remote version_id (per CLAUDE.md: a tier version of `None`/`""`
means the tier bucket is unversioned, so remote GET/DELETE must omit the
versionId). Two recovery paths wrongly treated that empty sentinel as an
incomplete/unrecoverable record while the persist/encode and worker
paths accept it, producing permanent leaks in the crash-recovery window.

- tier_delete_journal::into_jentry rejected entries with an empty
  version_id as "incomplete", so unversioned-tier WAL entries could never
  be decoded during recovery: the remote object was orphaned and the
  journal file leaked forever. Drop the version_id emptiness check; keep
  the obj_name/tier_name checks. Truncated payloads are still rejected at
  JSON deserialization (all fields are required, non-Option, no default,
  with deny_unknown_fields).

- tier_free_version_recovery::is_recoverable_tier_free_version required a
  non-empty version_id, silently filtering out free versions of
  unversioned-tier objects so a first enqueue failure leaked the remote
  object and local free version permanently. Drop the version_id check;
  keep free_version + name + tier checks.

Both downstream paths already issue a versionless remote delete for an
empty version_id, so no further changes are needed. Adds regression
tests covering the empty-version_id case and the preserved guards.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:26:29 +00:00
houseme f96314a1d5 docs(ecstore): pin streaming-only bitrot layout invariant (ECA-18) (#4553)
bitrot_shard_file_size only counts per-block checksum bytes for the two
streaming Highway variants, while BitrotWriter::write interleaves a hash
for any hash_algo.size() > 0 and bitrot_verify's read loop assumes an
interleaved hash per block. The three disagree for non-streaming
algorithms (SHA256/HighwayHash256/BLAKE2b512/Md5), but the divergence is
unreachable in production: every write path hardcodes HighwayHash256S and
ErasureInfo::get_checksum_info defaults to HighwayHash256S.

Per the audit decision (backlog#959), do NOT change the size formula:
it is a byte-for-byte port of MinIO's bitrotShardFileSize and its bare
return for non-streaming algorithms is correct for MinIO whole-file
bitrot; changing it would break legacy interop. Instead, document the
per-algorithm layout contract at bitrot_shard_file_size, BitrotWriter,
and bitrot_verify, and add regression tests that pin the invariants:
get_checksum_info defaults to HighwayHash256S, and the size formula
counts per-block hash bytes for streaming variants only while returning
the bare size for non-streaming ones. No disk layout or formula change.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:25:05 +00:00
houseme 20d61c73bc fix(ecstore): stop reduce_errs leaking nil placeholder as dominant error (#4551)
reduce_errs seeds best_err with a synthetic Error::other("nil") placeholder
via unwrap_or. When the error slice is empty or every error is ignored,
err_counts is empty and nil_count is 0, so both nil tie-break conditions are
false and the else branch returned (0, Some(Error::other("nil"))). That
placeholder leaked to build_write_quorum_failure_summary, where dominant_error
became Some("nil"), skipping the nil_dominated early return and falling back to
the "other_error" metric label. Quorum decisions were unaffected (max_count 0
still fails any positive quorum), but write-quorum failure metric labels and
tracing fields were misclassified.

Add an explicit short-circuit: when best_count == 0 and nil_count == 0, return
the (0, None) sentinel so the placeholder never escapes. Add regression tests
for the all-ignored slice, empty slice, and the summary label path, which the
existing test_build_write_quorum_failure_summary (nil_count > 0) did not cover.

Fixes #957

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:22:55 +00:00
Zhengchao An 72d76dc9ca fix(object-capacity): make dirty-disk lifecycle race-free and prune ghost entries (#4550)
Two dirty-mark lifecycle gaps (S19+S30):

- A commit cleared every dirty mark for the disks it scanned, even marks
  recorded while the walker was already past the written prefix, so the
  next dirty-subset refresh served stale cached bytes as exact until the
  next full scan. Dirty marks now carry the instant they were last
  recorded and a commit only clears marks that predate its scan start
  (CapacityUpdate::scan_started_at, set by refresh_capacity_with_scope).
- The write side marks every disk of an EC set dirty, including remote
  peers, while scans and clearing only cover local disks — remote or
  removed disks stayed marked forever, keeping the dirty-disk gauge
  permanently non-zero with bounded memory stuck behind it. The refresh
  disk selection now prunes dirty entries outside the current local
  topology before consulting them.

Ref: rustfs/backlog#1020 (S19+S30 from audit rustfs/backlog#1010)
2026-07-09 02:22:09 +08:00
houseme c77c5f047a fix(ecstore): defer multipart part.N.meta cleanup until after commit (#4548)
complete_multipart_upload deleted every part.N.meta via
cleanup_multipart_path *before* the authoritative rename_data commit.
If rename_data then failed write quorum, the upload directory was left
with data files but no part metadata, so a retried CompleteMultipartUpload
could never read the parts again and the upload became permanently
uncompletable (client must abort and re-upload all parts).

Move cleanup_multipart_path to run only after rename_data returns Ok,
matching the existing "clean up only after commit" pattern already used
for the old data-dir GC and the upload-dir delete_all. On a failed commit
the staging part.N.meta now survive so the retry can complete.

Add a hermetic regression test that forces rename_data to fail on every
disk (destination bucket dir made read-only) and asserts the staging
part.N.meta are preserved for retry.

Fixes #946.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 18:06:42 +00:00
Zhengchao An d342d33632 fix(tls): poll reloads for watch mode (#4532) 2026-07-09 02:01:47 +08:00
Zhengchao An 7fa3d0d4ba fix(io-core): correct available_buffers pool gauge drift (#4534)
fix(io-core): decrement available_buffers gauge on pooled-buffer reuse (backlog#806)

return_buffer does a fetch_add(1) on push but take_or_allocate_buffer's
available.pop() reuse path had no matching fetch_sub, so the available_buffers
gauge only ever grew and never reflected the real pool size. Decrement it on
reuse; + regression test.
2026-07-09 02:01:42 +08:00
Zhengchao An 16a91c35ec perf(iam): load IAM lists in fixed chunks to avoid O(n^2) startup load (backlog#806) (#4537)
load_all fetched policies/users/user-policies by passing the FULL remaining
list to load_*_concurrent every iteration and then split_off(32), so each item
was re-fetched once per preceding chunk — O(n^2) redundant loads on startup for
>32 items. Replace the split_off loops with chunks(32) so each item is fetched
exactly once. Behavior-preserving (cache inserts were already idempotent).
2026-07-09 02:01:36 +08:00
houseme 47c1e730c7 fix(ecstore): make erasure heal write quorum best-effort per target (#4545)
Erasure heal set write_quorum to the count of available target writers,
so every replacement disk had to succeed writing every block or the whole
object heal aborted. A single flapping replacement disk failing one block
made MultiWriter::write return Err, heal propagate Err, and the ops layer
delete the entire tmp staging dir — leaving the other healthy replacement
disks unhealed and blocking redundancy recovery indefinitely.

Set the heal write_quorum to 1, matching upstream MinIO's writeQuorum=1
for heal. MultiWriter already marks a failed writer as None, and the ops
layer (set_disk/ops/heal.rs) already drops the failed writer while
committing the survivors; the read-side quorum check and parity
cross-checks that guarantee reconstruction correctness are unchanged.

Add regression tests: one healthy-and-one-failing target still heals the
healthy targets and drops the failing one; all-targets-failing still
returns Err so nothing is falsely committed.

Fixes #947

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-09 01:56:03 +08:00
Zhengchao An f73d4121ad fix(object-capacity): close singleflight guard gaps in background refresh and no-runtime drop (#4543)
Two defensive gaps left the refresh singleflight vulnerable to a
permanent wedge (running=true forever: scheduled refreshes silently
stop, admin capacity joiners hang until restart):

- spawn_refresh_if_needed's spawned task had no leader guard, unlike
  refresh_or_join: a panic after the refresh future completes (commit,
  metrics) killed the task before the reset block. The task now holds
  the same RAII RefreshLeaderGuard, disarmed only after the state is
  reset and the result published (S20).
- refresh_fn() was evaluated before AssertUnwindSafe wrapping in both
  paths, so a panic while constructing the future escaped catch_unwind;
  construction now happens inside the wrapped future.
- RefreshLeaderGuard::drop silently skipped the reset when try_lock was
  contended and no tokio runtime was current; it now falls back to a
  blocking reset, which is safe precisely because there is no executor
  to stall in that context (S31).

Ref: rustfs/backlog#1021 (S20+S31 from audit rustfs/backlog#1010)
2026-07-09 01:51:13 +08:00
houseme f262fcfce0 perf(hotpath): add fine-grained PUT-path stage guards (HP-14) (#4541)
Close the ~10ms instrumentation residual on the PUT success path that the
existing coarse writer_setup/encode/rename stage metrics do not attribute.
Adds `hp_guard!` measurement scopes to the previously uninstrumented
sub-stages called out in backlog#935 item 2:

- SetDisks::acquire_read_lock / acquire_write_lock (namespace lock acquire)
- S3::put_object_prelookup (pre-write get_object_info lookup)
- MultiWriter::shutdown (bitrot writer flush/close)
- SetDisks::commit_rename_data_dir (old data-dir reclaim)
- S3Access::put_object (S3 authorization segment)

Instrumentation only: `hp_guard!` expands to nothing without the `hotpath`
feature, so this is a pure-observation change with zero behavior impact and
zero cost in default builds. The pre-lookup site wraps only the lookup call
in a scoped block so the guard measures that slice exactly while preserving
the existing match and control flow.

Verified: cargo check -p rustfs-ecstore (default and --features hotpath) and
cargo check -p rustfs (default and --features hotpath) all pass.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:47:14 +00:00
houseme 0866a8e6a0 fix(ecstore): route rebalance delete markers to store and count lifecycle expiries (#4540)
fix(ecstore): route rebalance delete markers to store and count expiries

Rebalance migrated delete markers via SetDisks::delete_object (single set,
no cross-pool routing), which silently rewrote the marker back onto the
source set. The subsequent source cleanup then deleted the whole entry,
losing the delete marker and reviving logically-deleted objects (#942, P0).
Route delete-marker migration through ECStore::delete_object via a store-
level closure that mirrors the existing transfer closure, so the marker
lands on the cross-pool target honouring data_movement/src_pool_idx/
delete_marker; on failure it is not counted as moved, so the source entry
is not cleaned up. The unused MigrationBackend::delete_object_for_migration
footgun is removed.

Rebalance source cleanup also ignored lifecycle-expired versions: the gate
used rebalanced == total_versions and passed an empty allowed_missing, so
any entry with an expired version could never be cleaned up, leaking the
migrated versions in the source pool (#950). Mirror decommission: gate on
rebalanced + expired == total_versions and feed expired version identities
into the cleanup preflight allowed_missing, plus a source_retained warning
for parity with decommission diagnostics.

Adds regression tests for delete-marker store routing and the expiry-aware
cleanup gate.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:45:54 +00:00
houseme d91f4d4557 fix(ecstore): report truncation when delimiter list re-folds a full page (#4538)
Non-slash delimiter ListObjects/ListObjectVersions force a recursive walk and
defer common-prefix folding to from_meta_cache_entries_sorted_infos. When a page
of max_keys+1 raw candidates collapses into fewer than max_keys common prefixes,
list_objects_paginate left is_truncated=false with no next_marker even though the
walker had filled its raw candidate limit (disk_has_more=true). Clients were told
listing was complete and silently lost every key beyond the scan window, a
data-loss bug for backup/sync/mirror/reconcile consumers (backlog ECA-03 / #944).

Add a delimiter re-fold guard: when disk_has_more is true but the folded visible
entries are fewer than max_keys, set is_truncated=true and continue from the last
RAW scanned key (captured before folding via last_scanned_entry_name). Using the
last raw key rather than a folded prefix is required for correctness: forward_past
advances on name > marker, and a folded prefix such as "data-" is lexicographically
smaller than its own keys ("data-0001"), so it would re-list and re-fold the same
keys forever. A real scanned key strictly advances past the scanned window and
keeps pagination finite. The slash-delimiter and no-delimiter paths fold during
the walk and are unaffected.

Regression tests cover the direct re-fold case, the disk-exhausted no-marker case,
the no-delimiter path guard, and an end-to-end 5000 data-* + 100 other-* paging
loop asserting bounded termination and full common-prefix coverage.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:42:12 +00:00
houseme e0c8b3dca6 fix(obs): evict retired metric cache entries (#4539)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:41:10 +00:00
houseme e0619e355f fix(ecstore): stop treating DiskNotFound as object not-found in listing (#4536)
is_all_not_found and is_all_volume_not_found delegated to
is_err_bucket_not_found, which matches StorageError::DiskNotFound. When
every erasure set in every pool was unreachable, the per-set aggregate
DiskNotFound slice was classified as "all not found", so list_merged
returned Ok(empty) and walk_result_from_set_errors was fed an
availability failure disguised as an empty listing. Clients saw a
successful empty ListObjects and mistook a full outage for an empty
bucket.

Introduce is_err_strict_not_found (FileNotFound / VolumeNotFound /
FileVersionNotFound / ObjectNotFound / VersionNotFound, excluding
DiskNotFound) for is_all_not_found, mirroring MinIO's isAllNotFound and
DiskError::is_all_not_found. Give is_all_volume_not_found its own
is_err_strict_volume_not_found (VolumeNotFound / BucketNotFound, minus
DiskNotFound) so genuine volume/bucket absence still surfaces while a
missing object under an existing volume is not escalated. Add
is_all_disk_not_found and a pre-check in walk_result_from_set_errors so
an all-offline slice surfaces DiskNotFound instead of being swallowed as
an empty listing, while partial outages (a healthy set present) stay
tolerated as before.

Regression tests cover all-DiskNotFound rejection, genuine not-found
acceptance, volume-not-found semantics, partial/empty short-circuit, and
the walk full-offline vs partial-offline paths.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:38:36 +00:00
houseme 15808254d3 fix(ecstore): correct codec-streaming byte accounting and partNumber routing (#4535)
Two correctness defects on the opt-in codec-streaming GET path.

ECA-02 (#943): ErasureDecodeReader only decremented `remaining` for the
main fill buffer. Under the default DualInFlight policy each fill also
produces a queued stripe that is delivered to the client via
`prefetched_bufs.pop_front()` without touching `remaining`, so any object
larger than one erasure block finished with `remaining > 0` and the GET
terminated with LessData despite delivering all bytes. The inflated
`remaining` was also fed back into the fill worker, which used it to trim
the final stripe and to decide whether to read past EOF. Account for the
queued-stripe bytes when they enter the prefetch queue; queued buffers
come only from `Ok(true)` decodes so they are non-empty and bounded by
`remaining - main_buf.len()`, ruling out underflow.

ECA-04 (#945): the codec-streaming gate did not inspect `opts.part_number`.
A partNumber GET carries `range == None`, so it was not classified as a
Range request and reached the full-object codec-streaming reader, which
drops the storage offset/length returned by GetObjectReader::new. A
partNumber >= 2 request would then stream the whole object. Mirror the
direct-memory part_number fallback and route any partNumber request back
to the legacy duplex path, which applies the offset/length correctly.

Regression tests: DualInFlight read_to_end on a multi-block object and on
a non-block-aligned object; SingleInFlight vs DualInFlight byte-identical
output; gate fallback on partNumber requests.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:35:39 +00:00
Zhengchao An 5a372557e5 fix(object-capacity): bound wedged scans with a hard outer timeout and cap joiner waits (#4533)
The whole disk traversal runs inside spawn_blocking with no timeout
anywhere on the async side; the in-scan ProgressMonitor checks are
cooperative and only run between walker entries. A stat/readdir blocked
on a dying disk or hung NFS mount therefore never returns: the blocking
thread leaks, the refresh singleflight stays running forever, every
subsequent scheduled refresh is skipped as inflight, and every admin
capacity query joins an unbounded wait until process restart (S02).

- Wrap each disk scan in tokio::time::timeout with a hard wall-clock
  ceiling of 2x the cooperative budget (min 5s). On expiry the caller
  fails the disk scan (releasing the singleflight through the normal
  error path, where the degraded/partial machinery from backlog#1014
  keeps the failed disk's last-known value) and a shared AtomicBool asks
  the blocking walker to exit at its next entry, bounding the thread
  leak to the single wedged syscall.
- Bound refresh_or_join joiner waits at 5 minutes so admin queries
  degrade into a clear error instead of hanging if the leader wedges in
  a way the drop/panic guards don't cover.

Ref: rustfs/backlog#1017 (S02 from audit rustfs/backlog#1010)
2026-07-09 01:22:22 +08:00
Zhengchao An 05833063c7 refactor(concurrency): remove zero-caller facade modules, fix feature build (#4530)
refactor(concurrency): remove zero-caller facade modules and fix no-default-features build (backlog#1025)

The audit in rustfs/backlog#1010 (consistent with #805) established that most of crates/concurrency was a decorative facade with zero production callers; the real runtime concurrency control lives in rustfs/src/storage/*. This deletes the dead facades and keeps only what the workspace actually consumes.

Deleted (zero callers verified by workspace-wide grep):
- manager.rs: ConcurrencyManager, lifecycle start/stop, misleading 'started' lifecycle logs
- config.rs: ConcurrencyConfig, ConcurrencyFeatures, from_env
- timeout.rs: TimeoutManager, TimeoutGuard, TimeoutManagerPolicy
- lock.rs: LockManager, LockScopeGuard, OptimizedLockGuard
- scheduler.rs: SchedulerManager, SchedulerPolicy, IoStrategy
- deadlock.rs facade: DeadlockManager, RequestTracker
- backpressure.rs facade: BackpressureManager, BackpressurePipe
- the prelude module, unused io-core re-exports, and all feature flags

Kept (real callers in ecstore/heal/rustfs):
- workload.rs admission contract types (unchanged)
- workers.rs Workers pool (unchanged, retained per #4498)
- GetObjectQueueSnapshot (moved from manager.rs to new queue.rs)
- PipeBackpressurePolicy (used by rustfs/src/storage/backpressure.rs)
- DeadlockMonitorPolicy (used by rustfs/src/storage/deadlock_detector.rs)
- OperationProgress re-export (used by rustfs/src/storage/timeout_wrapper.rs)

Removing the feature flags fixes the previously broken cargo check -p rustfs-concurrency --no-default-features (E0432). Docs and the logging guardrail file list are updated to match.

Ref: rustfs/backlog#1025
2026-07-09 01:12:43 +08:00
Zhengchao An a7b9659e75 fix(ecstore): hide encrypted object checksums (#4529) 2026-07-09 01:08:00 +08:00
Zhengchao An 3531abb34a feat(heal): disk-walk UNION enumeration to heal sub-quorum versions (backlog#920) (#4527)
B5 switched heal enumeration to list_object_versions, which only reflects the
read-quorum metadata view: a version present on fewer than read-quorum disks was
never enumerated, so it was never healed. Add a per-erasure-set disk-walk UNION
enumerator (mirrors MinIO global-heal.go objQuorum=1 listPathRaw +
mergeXLV2Versions) that surfaces every (object, version) present on ANY disk and
feeds each to the existing per-version heal_object.

- filemeta: MetaCacheEntries::resolve_union (dir_quorum=1/obj_quorum=1) yields the
  cross-disk version union at one tested seam.
- ecstore: SetDisks::heal_walk_versions_page (list_path_raw fan-out, min_disks=1,
  dual object/version page bound, inclusive-forward de-overlap) + ECStore delegator
  + HealWalkVersion.
- ecstore data-safety guard: before dangling-delete, try_regenerate_recoverable_meta
  physically probes part files via check_parts; when >= data_blocks data shards
  survive (meta lost but data recoverable) it regenerates xl.meta from a surviving
  FileInfo with the correct per-disk shard index instead of dangling-deleting.
  Genuine torn writes (< data_blocks) keep the current behavior — no resurrection.
- heal: dw1: forward-marker cursor codec (reuses ResumeState.resume_cursor,
  idempotent restart on foreign tokens); list_versions_for_heal_page_disk_walk
  trait method (default falls back to the B5 read-quorum path); heal_bucket_with_resume
  selects the disk-walk enumerator when scan_mode==Deep || source==AutoHeal, else
  the unchanged B5 path; anti-loop guard aborts on (empty && truncated).

Closes rustfs/backlog#920
2026-07-09 01:07:54 +08:00
Zhengchao An 2055044cb4 fix(ecstore): paginate ListMultipartUploads across pools (#4522) 2026-07-09 01:07:46 +08:00
Zhengchao An 7b87d4d13b fix(lock): stop waiter-count leak and dead pool recycle (#4520)
Make OptimizedNotify::wait_for_read/wait_for_write cancellation-safe with an
RAII decrement guard so the waiter counter is decremented even when the future
is dropped at the await (capped-wait timeout or task abort), preventing the
counter from leaking upward on a hot lock.

Fix cleanup_expired_batch so recycling into the object pool actually works:
Arc::try_unwrap on a clone could never succeed, so no state was ever recycled.
Collect keys during retain, remove them afterward to obtain the owned Arc, and
try_unwrap that before returning the state to the pool.

Refs rustfs/backlog#1035
2026-07-09 01:07:41 +08:00
Zhengchao An c41062f278 fix(protocols): constant-time FormPost signature check and guard DLO/SLO range underflow (#4519) 2026-07-09 01:07:36 +08:00
Zhengchao An 7ead413599 refactor(ecstore): migrate the local disk registry into InstanceContext (Phase 5 Slice 12) (#4518)
Phase 5 Slice 12 (backlog#939): move the local disk registry — the
path->disk map, the disk-id->endpoint map, and the pool/set/drive layout —
out of the three GLOBAL_LOCAL_DISK_* process statics into the per-instance
InstanceContext.

This is the migration the owned-guard prerequisite (#4501) unblocked.

- InstanceContext gains three `Arc<RwLock<..>>` registry fields, constructed
  eagerly (empty), plus pub(crate) handle accessors.
- The three runtime-source handle functions (`local_disk_map_handle` etc.) now
  return the current instance's registry via `current_ctx()`; every direct
  `GLOBAL_LOCAL_DISK_*` access in runtime::sources (~15 sites) routes through
  those handles instead. Accessors that hold a write guard across `.await`
  (`record_local_disks`, `initialize_local_disk_maps`, `replace_local_disk_id`)
  bind the handle first, so the same locks are held across the same awaits.
- The three statics are removed; the test-reset helper clears the current
  context's registry.

Construction window: the registry is populated during storage startup, before
the ECStore exists, via the bootstrap context that `ECStore::new` then adopts —
so startup writes and post-construction reads share one registry (no
split-brain). Single-instance behavior is byte-for-byte unchanged; the
GLOBAL_LOCAL_DISK_* boundary arch guard passes with the statics gone.

Verification: cargo test -p rustfs-ecstore (32 disk/instance tests green) and
-p rustfs-heal (201 green), cargo clippy -p rustfs-ecstore -p rustfs-heal
--all-targets (clean), make pre-commit (pass).

Refs: backlog#939 (Phase 5, Slice 12; disk-registry migration)
2026-07-09 01:07:28 +08:00
Zhengchao An 87357a0fdd fix(swift): escape static-website listing to prevent stored/reflected XSS (#4515) 2026-07-09 01:07:19 +08:00
Zhengchao An 3c53854345 fix(common): sum forwarded slots in LastMinuteLatency::merge (#4514)
In the self.last_sec > o.last_sec branch, merge forwarded a clone of o to
age out stale ring-buffer slots but then summed the un-forwarded o.totals
instead of the forwarded copy, so aged-out windows leaked into the result
and the forwarded clone was a dead write. Read both operands in forwarded
form and align the summation on wrapping_add to match AccElem::merge.
2026-07-09 01:07:15 +08:00
Zhengchao An afaf8c681a fix(checksums): reject md5 instead of silently returning crc32 (#4513)
The UnknownChecksumAlgorithmError message omitted crc64nvme and advertised
the deprecated md5. Parsing "md5" also returned a Crc32 algorithm and its
into_impl produced a 4-byte CRC32, so a caller asking for MD5 silently got
the wrong digest.

Enumerate the accepted algorithms (crc32, crc32c, crc64nvme, sha1, sha256)
in the error message, remove the unused Md5 enum variant, and make parsing
"md5" fail loudly. Add tests covering the message contents and the md5
parse error.
2026-07-09 01:07:11 +08:00
Zhengchao An 77eb91bf8d fix(s3-types): cover all removed events and fix scanner round-trip (#4512) 2026-07-09 01:07:06 +08:00
Zhengchao An 765e719aaf fix(s3select): configure session partitions (#4511) 2026-07-09 01:07:01 +08:00
Zhengchao An f9f2e5b4b8 fix(zip): validate CRC32 on small-entry extract fast path (#4510) 2026-07-09 01:06:55 +08:00
Zhengchao An 8bfb00bc03 fix(filemeta): guard get_idx bound and fix sorts_before tie-break (#4509) 2026-07-09 01:06:44 +08:00
Zhengchao An d1245ca33c fix(config): default trusted proxies to loopback-only (#4508) 2026-07-09 01:06:11 +08:00
Zhengchao An 5bbbe0008e fix(data-usage): sum all sub-buckets for 1KiB-1MiB histogram rollup (#4507) 2026-07-09 01:06:03 +08:00
houseme e44bece00d fix(audit): harden reload ordering, start race, paused drops, and batch observability (#4497)
Follow-up hardening for the audit control plane. The ABBA deadlock
(backlog#961), dispatch failure propagation (backlog#962), and credential
header redaction (backlog#963) already landed on main; this change addresses
the remaining audit-side findings.

- backlog#970: reload/commit now shuts down existing replay workers and closes
  old targets *before* activating the replacement set, so old and new workers
  never drain the same store concurrently and re-deliver entries. Extracted a
  state-neutral `shutdown_runtime_targets` helper (registry-then-cancellers
  lock order preserved).
- backlog#978: `start()` claims the `Starting` transition atomically under the
  state lock, closing the check-then-act race that could double-activate;
  `dispatch()` no longer returns Ok while paused, it surfaces an explicit
  `AuditError::Paused` so the audit trail is not silently corrupted.
- backlog#984 (audit part): `dispatch_audit_log` performs a single state read
  and interprets not-running/paused as a deliberate skip while surfacing real
  delivery failures; `dispatch_batch` records the same observability signals as
  single dispatch; documented the unordered cross-target fan-out.

Adds unit tests: paused dispatch returns Err, concurrent start does not hang or
double-activate, commit closes old targets before installing new, and
dispatch/dispatch_batch delivery-outcome coverage (all-fail -> Err, partial ->
Ok, all-success -> Ok).

Relates to rustfs/backlog#970
Relates to rustfs/backlog#978
Relates to rustfs/backlog#984

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:05:21 +00:00
houseme e008cc5dae fix(targets): harden Postgres/MySQL SQL backends (pool timeouts, error classification, idempotency) (#4500)
fix(targets): harden Postgres/MySQL SQL backends (pools, error class, idempotency)

Postgres (postgres.rs):
- Add deadpool wait/create/recycle timeouts + tokio-postgres connect_timeout,
  and wrap every pool.get() in a Tokio hard-limit timeout so an unreachable
  broker/DB can no longer block send_body/probe_table/is_active forever;
  checkout timeouts map to TargetError::Timeout to trigger store replay.
- namespace format now DELETEs the row on s3:ObjectRemoved:* events instead of
  UPSERTing, keeping the table consistent with the object lifecycle.
- map_pg_error: SQLSTATE class 40 (serialization_failure 40001,
  deadlock_detected 40P01, transaction rollback) is now transient (Timeout,
  retryable) instead of a permanent Request. Extracted map_pg_sqlstate for
  unit-testable classification.

MySQL (mysql.rs):
- Wrap pool.get_conn() in a Tokio checkout timeout across insert/init/liveness
  paths so an unreachable server cannot block the delivery thread.
- Add an event_id idempotency key: tables created by the target now carry an
  event_id VARCHAR(255) PRIMARY KEY, inserts use ON DUPLICATE KEY UPDATE, and
  replays reuse the stable store key so a lost-ack replay no longer appends a
  duplicate audit row. Legacy two-column tables are detected and fall back to
  the non-idempotent insert with a warning (backward compatible).
- redact_mysql_dsn splits on the last '@' so a password containing '@' no
  longer leaks its tail into Debug output.
- Disconnect the stale pool before dropping it on inline TLS reload instead of
  leaking its connections.
- Cache TLS file mtimes and only recompute the inline fingerprint when a cert
  file changes, avoiding a 3-file read+hash on every checkout.

Adds unit tests for SQLSTATE classification, namespace delete SQL, removal-event
detection, DSN redaction with '@' in the password, and the MySQL insert/DDL
builders.

Relates to rustfs/backlog#976
Relates to rustfs/backlog#973
Relates to rustfs/backlog#983

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 17:00:44 +00:00
Zhengchao An 73a30178f5 fix(object-capacity): make timeout fallback keep accumulated work and enter sampling within the time budget (#4517)
The capacity scan's timeout fallback required the exact prefix
(threshold + sample_rate files, default ~200k) to be fully enumerated
before any sample existed. Cold-cache HDDs cannot stat that many files
inside the 15s budget, so large slow disks — exactly the disks sampling
was designed for — always timed out with sampled_count == 0, discarded
the accumulated exact-prefix work with a hard error, and re-ran 15s of
useless full-disk I/O every 120s (S03). When a sample did exist, the
estimate only extrapolated over files the walker had seen, silently
under-reporting disks whose tail was never reached (S10).

- Enter sampling early: once half the (possibly dynamic) time budget is
  consumed and the exact prefix hasn't filled, freeze the threshold at
  the current position so the remaining budget collects samples and the
  timeout fallback is always reachable.
- Compensate the unseen tail: when the scan root is a dedicated mount
  point (stat st_dev differs from the parent), take the max of the
  seen-files extrapolation and the filesystem-level used bytes. On
  shared filesystems (multi-disk dev layouts) statvfs would overcount,
  so it is not trusted and behavior is unchanged.
- sampled_count == 0 at timeout no longer hard-fails when a dedicated
  mount provides filesystem usage; the exact prefix is kept as a floor.
  Without any estimator the original error still surfaces.
- Extract ScanLimits from env reading so the blocking scanner is
  parameterizable in tests.

Ref: rustfs/backlog#1013 (S03+S10 from audit rustfs/backlog#1010)
2026-07-08 16:53:35 +00:00
houseme 6ab8636734 fix(obs): count scanned versions independent of ILM (#4516)
versions_scanned for both the scanner and ILM collectors was read from
the Lifecycle work source's `checked` counter, which is never recorded on
the production scan path — so rustfs_scanner_versions_scanned_total and
rustfs_ilm_versions_scanned_total sat at zero even while objects_scanned
climbed. The two metrics also have distinct intended meanings that were
conflated: "versions scanned" (all versions, any bucket) vs "versions
checked for ILM actions" (lifecycle-configured buckets only).

Add a lifetime `versions_scanned` counter recorded for every version the
scanner walks (independent of ILM), and record the Lifecycle source's
`checked` counter from the ILM evaluator so the ILM metric reflects the
real checked subset. The scanner collector now reports total scanned
versions; the ILM collector keeps the ILM-checked subset.

Closes backlog#995 (OBS-09).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:50:51 +00:00
Zhengchao An e829430d89 fix(concurrency): rewrite Workers on tokio Semaphore to fix lost wakeups (#4498)
fix(concurrency): rewrite Workers on tokio Semaphore to fix lost wakeups (backlog#1023)
2026-07-09 00:31:06 +08:00
houseme 2df315baf0 fix(ecstore): fsync ancestor dirs for first object writes (#4493)
* fix(ecstore): fsync new object's ancestor dirs to close a power-loss gap

reliable_mkdir_all creates an object's directory (and any missing prefix dirs)
with plain mkdir and never fsyncs the parent chain. The commit-point fsync in
rename_data persists the object dir's *contents* (its xl.meta and data dir),
but not the object dir's own entry in the bucket/prefix directory. So on the
first PUT of an object, a power loss after the write is acknowledged could drop
the whole object directory even though its contents were durable — an
acknowledged write silently lost (rustfs/backlog#922 step 4).

For a new object (no prior xl.meta) under a durability tier that syncs commit
metadata, fsync the ancestor chain from the object dir's parent up to and
including the bucket after the commit rename, so the newly created directory
entries survive power loss. A starts_with guard bounds the walk to the bucket
subtree. Overwrites already have a durable object dir and are unaffected;
relaxed/none accept the wider window like the existing commit fsync.

Durability regressions are invisible to ordinary behavior tests, so the new
tests assert directly (via the fsync_dir recorder) that a first PUT under a new
prefix fsyncs both the prefix and bucket dirs, and that relaxed does not.

Scope: the non-inline (erasure) commit path. The inline branch has the same
gap and is a separate follow-up.

Refs: rustfs/backlog#922 (HP-1 step 4), rustfs/backlog#936

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): fsync new inline object's ancestor dirs too

Extend the backlog#922 step 4 mkdir-gap fix to the inline commit branch. Like
the non-inline path, a first PUT of an inline object creates its directory
(and any missing prefix dirs) whose entry in the parent chain reliable_mkdir_all
never fsynced; the commit fsync persists the object dir's contents, not its own
entry. For a new inline object under a commit-metadata-syncing tier, fsync the
ancestor chain up to and including the bucket after the commit rename, using the
same starts_with-bounded walk (via the synchronous os::fsync_dir_std inside the
inline spawn_blocking closure).

Adds a test asserting a new inline object under a new prefix fsyncs both the
prefix and bucket dirs. rename_data now closes the ack'd-write power-loss gap on
both the erasure and inline paths.

Refs: rustfs/backlog#922 (HP-1 step 4), rustfs/backlog#936

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:31:00 +00:00
houseme 08e44b95f8 fix(targets): make persistent queue store crash-safe and replay lifecycle correct (#4505)
* fix(targets): make persistent queue store crash-safe and replay lifecycle correct

Harden the target notification persistent queue (store.rs) and the replay
worker lifecycle (runtime) against data loss, silent truncation, ordering
drift, orphaned tasks, and a few low-risk robustness gaps.

store.rs
- Atomic, durable writes: write to a per-key temp file, fsync (sync_all),
  then rename into place; best-effort parent-dir fsync. A crash mid-write
  can no longer lose an acknowledged event or leave a half-written payload
  that reads as a valid entry.
- open() now removes leftover .tmp residue and zero-byte files, and only
  indexes files matching the queue extension, so ghosts/foreign files are
  never replayed.
- FIFO ordering is derived from time-ordered UUIDv7 entry names instead of
  coarse, clock-dependent file mtimes, so replay order is stable and
  identical after a restart.
- Clamp HashMap/Vec pre-allocation derived from untrusted inputs
  (entry_limit, batch item_count) to avoid capacity-overflow panics / giant
  allocations.

target/mod.rs
- QueuedPayload::decode validates body length against the recorded
  payload_len, rejecting torn/truncated writes instead of delivering a
  silently truncated body.
- send_from_store purges a NotFound/empty entry (index + file) instead of
  skipping it, so it cannot occupy a queue slot and be replayed forever.
- sanitize_queue_dir_component appends a stable hash suffix when the id was
  lossy, so distinct target ids can no longer collapse onto the same queue
  directory; path-safe ids are unchanged (no migration).

runtime
- Replay backoff, idle waits, and inter-scan pauses are now cancel-aware, so
  reload/shutdown is not blocked for the full retry delay.
- ReplayWorkerManager::stop_all signals cancellation and then awaits each
  worker's exit (bounded, with abort fallback), preventing orphaned tasks
  and overlapping drain of the same store.
- Fix the always-true replay flush condition so batching is real
  (size/timeout based, one semaphore permit per batch) rather than one
  permit per entry; dedup keys already pending in the batch.
- clear_and_close aggregates and reports per-target close failures instead
  of swallowing them; explicit shutdown surfaces them.

Relates to rustfs/backlog#966
Relates to rustfs/backlog#967
Relates to rustfs/backlog#975
Relates to rustfs/backlog#970
Relates to rustfs/backlog#983

Co-Authored-By: heihutu <heihutu@gmail.com>

* style(targets): apply rustfmt to replay batch dedup guard

Fixes the Quick Checks rustfmt failure on the multi-line `.iter().any(...)`
closure in the replay batch dedup guard.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:27:34 +00:00
Zhengchao An c6d07ffc59 fix(object-capacity): mark partial-failure refreshes degraded and merge over disk cache (#4499)
A full refresh with partial disk failures used to commit the surviving
subset's sum as a fresh exact cluster total (no field carried the
partial-failure fact), while the complete disk cache kept the failed
disk's old value — so the reported capacity oscillated between the
partial sum and the cache-merged total on alternating refreshes.

- Add a degraded flag to CapacityUpdate/CachedCapacity, set when the
  scan behind the update had partial errors; expose it in refresh logs,
  admin capacity logs and a new rustfs_capacity_degraded_readings_total
  counter.
- On a degraded full refresh, surface only the disks whose own scan
  fully succeeded and merge them over a complete disk cache, so failed
  disks keep their last-known values and the published total no longer
  dips and bounces back. The disk cache is never replaced from a
  degraded refresh.
- Without a complete cache, keep the partial sum (unchanged #805
  non-pollution behavior) but mark the reading degraded.

Ref: rustfs/backlog#1014 (S06 from audit rustfs/backlog#1010)
2026-07-09 00:23:15 +08:00
houseme 7d93a7596c fix(targets): harden messaging backend delivery and error classification (#4506)
Address a batch of correctness/security audit findings in the messaging
notification backends (mqtt/nats/kafka/amqp/redis/pulsar/webhook).

MQTT (backlog#971): wait for broker acknowledgement via publish_tracked +
wait_completion_async (PUBACK/PUBCOMP for QoS>=1, flush for QoS0) with a
30s timeout before reporting success, instead of treating the enqueue as
delivered. Replace substring error matching with typed ClientError /
PublishNoticeError classification.

NATS (backlog#971, #973, #983): flush() after publish to confirm the broker
received the message before the durable copy is deleted; classify publish
and flush failures by typed error kind; warn when credentials are sent
without TLS.

Kafka (backlog#973, #983): use Error::is_retriable() so transient broker
states (NotLeaderForPartition, LeaderNotAvailable, RequestTimedOut, ...) are
retried instead of dropped as permanent; add a bucket/object message key for
per-object partition ordering; build the producer without holding the cache
lock across the connect await.

AMQP (backlog#973, #980): classify permanent broker protocol errors
(404/403/406, NOT_ALLOWED, ...) as request-level errors rather than
connectivity errors to avoid reconnect storms; bound publish and
publisher-confirm waits with a timeout; check is_enabled in is_active; run
full close() cleanup even when the broker close fails; warn when
mandatory=false may silently drop unroutable messages.

Redis (backlog#982): warn when PUBLISH reaches 0 subscribers; reuse the
cached ConnectionManager for health probes instead of a fresh handshake;
warn when tls_allow_insecure disables certificate verification.

Pulsar (backlog#983): replace std::sync::Mutex + unwrap with parking_lot
Mutex so a panic while holding the guard cannot poison later accesses.

Webhook (backlog#983): drain the response body so the connection can be
reused (keep-alive). The #974 redirect-follow SSRF fix already landed.

Relates to rustfs/backlog#971
Relates to rustfs/backlog#973
Relates to rustfs/backlog#974
Relates to rustfs/backlog#980
Relates to rustfs/backlog#982
Relates to rustfs/backlog#983

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:20:38 +00:00
houseme c9dba2c6c2 fix(notify): close core notify correctness and safety gaps (#4502)
Land the remaining notify-crate audit fixes.

backlog#979(b): remove_target now enforces the same bucket-binding guard as
remove_target_config, refusing to delete a target still referenced by a bucket
rule so notification rules are not left orphaned.

backlog#984:
- event.rs: an unversioned object omits versionId entirely instead of
  serializing versionId:"" (empty object/request versions treated as "no
  version").
- notifier.rs: RUSTFS_NOTIFY_SEND_CONCURRENCY=0 coerces back to the default
  instead of building a zero-permit semaphore that deadlocks every dispatch;
  init_bucket_targets_shared closes the replaced targets instead of dropping
  them without close() (connection leak).
- subscriber_index.rs: store_snapshot uses an atomic compute_if_absent upsert,
  removing the get-then-insert TOCTOU that could clobber a concurrent
  first-writer's snapshot cell.
- pipeline.rs: send_event assigns the history sequence and broadcasts to live
  subscribers under one critical section so broadcast order matches recorded
  sequence order.
- xml_config.rs: filter value length is bounded by character count, not byte
  length, so valid multi-byte keys are no longer wrongly rejected.
- global.rs: a losing initialize() race shuts the just-initialized system down
  instead of leaking its targets/replay workers.

backlog#970 (notify part): reload_config stops the running replay workers
before activating the new ones, so old and new workers do not concurrently
drain the same persisted stores. The full signal+join shutdown lives in the
targets crate under the same issue.

Tests: added regression coverage for each fix.
cargo build -p rustfs-notify, cargo test -p rustfs-notify --lib (98 passed),
cargo clippy -p rustfs-notify --all-targets (clean).

Relates to rustfs/backlog#979
Relates to rustfs/backlog#984
Relates to rustfs/backlog#970

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:20:02 +00:00
houseme cd327c81f5 fix(targets): harden control-plane, TLS reload, and runtime extension points (#4504)
Addresses security/correctness audit findings in the target-plugin control
plane, TLS reload coordinator, and runtime extension registries.

control_plane (backlog#977):
- Install now preserves the currently installed revision as previous_revision
  so Rollback actually restores the prior version instead of a no-op.
- Split the gate: circuit-breaker and runtime-activation checks apply only to
  Install/Enable; Disable and Rollback stay available as break-glass
  remediation while the breaker is open.
- Enforce the sidecar runtime protocol version for every external transport
  (previously skippable by declaring a non-gRPC transport) and validate the
  plugin api_compatibility_version at planning time.
- Download/signature/provenance host allowlisting now matches the full
  host authority, so an allowlisted host never implicitly authorizes a
  different host:port.

TLS reload coordinator/validate (backlog#981, #970-coordinator):
- Compute the fingerprint before building material in register() to remove the
  TOCTOU that could permanently pin an old certificate; rotation self-heals.
- Always start a detection loop when reload is enabled; Watch mode no longer
  returns success without any loop.
- validate_cert_key_pairing now verifies the private key matches the
  certificate's public key instead of only parsing both files.
- Normalize a zero poll interval to a positive minimum to avoid a panic that
  silently killed the poll loop.
- Serialize reload cycles with a per-target mutex; a first-step fingerprint
  read failure now records last_error and a failure metric.
- Duplicate label registration stops and joins the previous loop before
  publishing the replacement (stop-before-start), preventing orphaned loops.

runtime extension points (backlog#983 runtime subset):
- ops_profiler/ops_diagnostics authorize before probing the registry so an
  unauthorized caller cannot learn whether a backend/surface exists.
- s3_hooks dispatch_post_auth actually traverses the registered hooks for the
  point instead of unconditionally returning Continue.
- sidecar send_with_timeout redacts errors and uses the configurable failure
  threshold via policy, and a successful send resets the breaker accounting.

Relates to rustfs/backlog#977
Relates to rustfs/backlog#981
Relates to rustfs/backlog#970
Relates to rustfs/backlog#983

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:18:30 +00:00
houseme c9292688d3 fix(obs): wire dial9 runtime telemetry (#4491)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:16:06 +00:00
Zhengchao An 20447422cf fix: harden io-core and concurrency boundaries against panics (#4503)
fix(io-core,concurrency): harden boundary validation and arithmetic against panics (backlog#1024)
2026-07-09 00:15:36 +08:00
houseme 6cb47049e8 fix(obs): isolate process sampler windows (#4492)
* fix(obs): isolate process sampler windows

Refs rustfs/backlog#1004
Refs rustfs/backlog#986

- add a reusable ProcessSampler so callers can own independent sysinfo refresh windows
- wire separate sampler instances for obs metrics scheduling and memory observability
- keep compatibility helpers while avoiding cross-task CPU and disk delta interference

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs): import process sampler bundle helper

Refs rustfs/backlog#1004
Refs rustfs/backlog#986

- import collect_process_metric_bundle_with in the metrics scheduler
- drop the stale collect_process_metric_bundle import after switching scheduler sampling to independent process samplers

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs): move process sampler into blocking task

Refs rustfs/backlog#1004
Refs rustfs/backlog#986

- move the memory observability process sampler into the spawn_blocking closure
- satisfy the closure static lifetime required by tokio while keeping the isolated sampler design intact

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 15:51:34 +00:00
Zhengchao An 6bdfdd164a refactor(ecstore,heal): return the local disk map as an owned read guard (Phase 5 disk-registry prep) (#4501)
refactor(ecstore,heal): return the local disk map as an owned read guard (Phase 5 prep)

Prerequisite for the Phase 5 disk-registry migration (backlog#939): the disk
map cannot move from the process global into the per-instance InstanceContext
while callers depend on a `'static` read guard borrowed from the global.

Change `local_disk_map_read` to return an owned guard
(`OwnedRwLockReadGuard`) via `Arc::read_owned` instead of
`RwLockReadGuard<'static, _>`:

- ecstore `runtime::sources::local_disk_map_read` now returns
  `OwnedRwLockReadGuard<..>` (holds an Arc clone of the lock), not a `'static`
  borrow of `GLOBAL_LOCAL_DISK_MAP`.
- heal's forwarding accessor and its callers (which hold the guard across
  `.await` while clearing/writing per-disk markers) keep identical behavior —
  the owned guard derefs to the same map, so iteration is unchanged.

This decouples the heal crate from the global's `'static` lifetime so a later
PR can source the map from the current instance's context. Single-instance
behavior is byte-for-byte unchanged; the same read lock is held across the same
awaits.

Verification: cargo test -p rustfs-heal (201 tests green), cargo clippy -p
rustfs-ecstore -p rustfs-heal --all-targets (clean), make pre-commit (pass).

Refs: backlog#939 (Phase 5, disk-registry prerequisite)
2026-07-08 23:47:00 +08:00
houseme 7048a9a3ed test(ecstore): run shard_read_costs empty test under a Tokio runtime (#4496)
shard_read_costs_for_empty_disk_set_are_empty was a plain sync #[test], but
shard_read_costs_for_disks consults process-global topology state
(local_endpoint_hosts_for_shard_costs) whose fast-lock manager lazily spawns a
background cleanup task on first access. When this test was the first in a
process to touch that global — as under nextest's per-test isolation — the
tokio::spawn panicked with a TryCurrentError because no runtime was present,
making the test order-dependent flaky in CI (it passes only when some sibling
tokio test initializes the manager first).

Run it under a Tokio runtime like the sibling reservation tests
(#[tokio::test]), so the lazy init's spawn always has a runtime. Test-only
change; no production behavior change.

Verified failing before the change and passing after under both
`cargo test` and `cargo nextest run` in isolation.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 15:42:59 +00:00
Zhengchao An ca63a6cced refactor(ecstore): migrate background replication pool/stats into InstanceContext (Phase 5 Slice 11) (#4495)
Phase 5 Slice 11 (backlog#939): move the background replication pool and stats —
the last service handles, and the only async ones — out of the process statics
into the per-instance InstanceContext.

- InstanceContext gains `replication_stats: OnceCell<Arc<ReplicationStats>>` and
  `replication_pool: OnceCell<Arc<DynReplicationPool>>` (tokio async OnceCell),
  with sync read accessors (`replication_stats`/`replication_pool`/
  `replication_initialized`) and pub(crate) cell accessors for the async init.
- `init_background_replication` initializes the current instance's cells via the
  same `get_or_init(async {…}).await` (workers still spawned once on first
  init). The lifecycle owner helpers and the runtime-source accessors keep their
  signatures and route through the current instance's context; the two statics
  (and the now-unused lazy_static import) are removed. The replication-boundary
  arch guard still passes.

Single-instance: init materializes one shared pool/stats via the bootstrap
context — byte-for-byte the same as the eager statics.

Tests: replication state is None until set and independent across instances.

Verification: cargo test -p rustfs-ecstore (22 instance-context tests green),
cargo clippy -p rustfs-ecstore --all-targets (clean), make pre-commit (pass).

Refs: backlog#939 (Phase 5, Slice 11). Stacked on Slice 10 (#4494).
2026-07-08 23:15:01 +08:00
Zhengchao An db8ba9e2e5 refactor(ecstore): migrate the bucket bandwidth monitor into InstanceContext (Phase 5 Slice 10, re-submit) (#4494)
refactor(ecstore): migrate the bucket bandwidth monitor into InstanceContext (Phase 5 Slice 10)

Phase 5 Slice 10 (backlog#939): move the bucket bandwidth monitor out of the
process static into the per-instance InstanceContext.

- InstanceContext gains `bucket_monitor: OnceLock<Arc<Monitor>>` with
  `init_bucket_monitor(num_nodes)` (set-once; returns false if already set) and
  `bucket_monitor() -> Option<..>`.
- global.rs `init_global_bucket_monitor` / `get_global_bucket_monitor` keep
  their signatures and route through the current instance's context; the static
  is removed. The ignore-and-warn-on-reinit behavior is preserved (the warn
  stays in global.rs).

Tests: the monitor is None until initialized, set-once (second init is a no-op),
and independent across instances.

Verification: cargo test -p rustfs-ecstore (21 instance-context tests green),
cargo clippy -p rustfs-ecstore --all-targets (clean), make pre-commit (pass).

Refs: backlog#939 (Phase 5, Slice 10).
2026-07-08 22:56:43 +08:00
houseme 83bacd5b84 fix(obs): harden cleaner file matching (#4486)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 22:44:34 +08:00
houseme 651ccac130 perf(ecstore): parallelize tmp xl.meta write and shard fdatasync on commit (#4487)
The non-inline rename_data commit path made the tmp xl.meta durable and then
fdatasynced the shard files in two sequential awaits, each its own blocking
round-trip. The two operations touch disjoint paths (the tmp xl.meta under the
tmp bucket vs the shard data dir) and have no ordering constraint between them:
both only need to be durable before the commit renames that follow.

Run them concurrently with tokio::join!, dropping a blocking round-trip from
the PUT commit critical path (backlog#922 step 2). The commit ordering is
unchanged — both futures complete before any rename, and a failure in either
aborts before the rename exactly as the sequential version did (tmp-meta error
is still surfaced first). Payload and metadata durability semantics are
identical under strict and relaxed tiers.

Validated by the rename_data crash-consistency harness (backlog#935): a crash
at any pre-commit point still reopens as old-or-new, never mixed. Existing
rename_data / durability-tier / disk::os tests are unchanged and pass.

Refs: rustfs/backlog#922 (HP-1 step 2), rustfs/backlog#936

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 22:22:33 +08:00
Zhengchao An 9e162f224b refactor(ecstore): move lifecycle expiry and transition state into InstanceContext (#4488) 2026-07-08 22:15:22 +08:00
houseme 87044b2378 fix(obs): tighten low-risk telemetry correctness (#4483)
Refs rustfs/backlog#1007
Refs rustfs/backlog#986

- respect explicit log-target directives when injecting noisy-crate suppressions
- fix daily rotation day-boundary checks and add a short retry cooldown after failed rotations
- preserve recorder metadata across label variants and serialize gauge updates before export
- correct profiling target_env reporting, trace all=true parsing, cleaner accounting/docs, and legacy system interval rounding

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 14:06:19 +00:00
Zhengchao An dee8e4e639 test(e2e): make security boundary tests assert real outcomes (#4466)
* test(e2e): make security boundary tests assert real outcomes

* fix(e2e): use expect_err to satisfy clippy err_expect lint
2026-07-08 22:01:46 +08:00
Zhengchao An 322b585f71 refactor(ecstore): move deployment id, endpoints, and tier config into InstanceContext (#4465) 2026-07-08 21:50:56 +08:00
houseme 8fc637fb14 perf(ecstore): run the short EC encode inline instead of block_in_place (#4484)
Each erasure block runs its Reed-Solomon encode through
tokio::task::block_in_place on the multi-threaded runtime. That parks the
worker and asks the scheduler to relocate other tasks, but the encode itself
is only ~110µs per 1MiB block (p99 ~542µs) — the profiling in backlog#932
flagged the scheduling disturbance as comparable to the compute it guards.

Call the encode closure inline on the multi-threaded runtime instead. The
CurrentThread (and any other) flavor keeps spawn_blocking so the sole executor
thread is never blocked and block_in_place's multi-thread-only requirement is
respected. Applied to both ingest paths (encode_block / Vec and
encode_block_bytes_mut / BytesMut); no change to encode output, quorum,
shutdown, or error handling.

Adds encode_works_on_multi_thread_runtime to cover the previously-untested
multi-threaded arm for both ingest paths, asserting streaming and batched
encode produce identical shard bytes.

This is the low-risk, correctness-neutral item that backlog#932's adversarial
verification recommended splitting out and doing first; the larger per-writer
pipeline restructure it belongs to stays gated on a Linux multi-disk baseline.

Refs: rustfs/backlog#932 (HP-11), rustfs/backlog#936

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 13:38:00 +00:00
houseme e7cfc510ec fix(obs): correct gpu collector coverage (#4482)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 13:35:49 +00:00
houseme 2157470224 ci(perf): warp A/B relative-budget gate for the hotpath series (#4480)
* ci(perf): warp A/B relative-budget gate for the hotpath series

performance.yml only uploads a samply profile and a cargo-bench artifact with
no pass/fail, so a 26x-class write-path regression like #4221 or the GET perf
churn would sail through CI and only surface in customer load tests. This adds
the missing gate — the acceptance surface every queued HP change (#922/#923/
#925/#927/#930/#932) needs before its default can flip.

- scripts/hotpath_warp_ab_gate.sh: applies the relative budget to the
  baseline_compare.csv that run_object_batch_bench_enhanced.sh already emits
  (it computes deltas but never gates). A metric regressing past --fail-pct
  fails, past --warn-pct warns; reqps/throughput are higher-is-better, latency
  lower-is-better. --allow-regression downgrades a FAIL to an exempted WARN so
  a deliberate correctness cost (e.g. #4221) is recorded, not blocked
  (rustfs/backlog#935 correction 1). Unit-checked across pass/warn/fail/exempt.
- scripts/run_hotpath_warp_ab.sh: single-host baseline-binary vs candidate-
  binary A/B over {put-4mib, get-4mib, mixed-256k} x {drive-sync on, off},
  reusing the enhanced bench as the warp driver and the single-node local-disk
  lifecycle. Has --dry-run; warp is assumed pre-installed as elsewhere in
  scripts/. Drive-sync on/off keeps a sync-semantics change from being masked
  by nosync numbers.
- .github/workflows/performance-ab.yml: nightly on main (post-merge detection)
  plus opt-in pre-merge via the `perf-ab` label; `perf-deliberate-tradeoff`
  runs the gate with --allow-regression; posts the gate table as a PR comment
  and uploads the run. A Linux runner answers "do the macOS conclusions hold".

The gate logic is unit-validated with synthetic compare CSVs; the orchestrator
and workflow are shellcheck- and --dry-run-validated. The first real warp
measurement belongs on the Linux runner (no warp/multi-disk rig locally).

Refs: rustfs/backlog#935 (HP-14 warp A/B gate, item 4), rustfs/backlog#725
(cooled A/B harness precedent), rustfs/backlog#936

Co-Authored-By: heihutu <heihutu@gmail.com>

* ci(perf): pin upload-artifact, add external-cluster A/B mode + runbook

- Pin actions/upload-artifact to the repo-standard full-length SHA (# v6);
  the previous @v4 float tripped the workflow-pin guard.
- Add an external deployment mode to run_hotpath_warp_ab.sh so warp can target
  an already-running cluster instead of a throwaway single-node server:
  --endpoint selects the cluster and --deploy-hook runs between phases to swap
  in the phase's binary and drive-sync config (context passed via
  HOTPATH_AB_PHASE / HOTPATH_AB_BINARY / HOTPATH_AB_DRIVE_SYNC). This maps onto
  the team's ansible harness (cargo zigbuild -> ansible rustfs-manage --tags
  stop,config,binary-copy,start -> warp).
- Restructure the matrix loop to bring a deployment up once per (phase,
  drive-sync) and run all three workloads against it, instead of restarting per
  workload — fewer server starts / cluster redeploys. Baseline medians are read
  from a deterministic path, dropping the bash-4-only associative array so the
  script (and its --dry-run self-check) runs on macOS bash 3.2 too.
- Add docs/operations/hotpath-warp-ab-runbook.md tying local mode, the ansible
  external mode, and the budget/exemption together.

Verification: shellcheck clean; --dry-run in both modes prints the expected
4 deployments x 3 workloads and passes exactly 6 compare CSVs to the gate;
check_workflow_pins.sh, check_doc_paths.sh, and make pre-commit all pass.

Refs: rustfs/backlog#935, rustfs/backlog#936

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 13:22:44 +00:00
houseme f0bf8cfe03 fix(obs): hide unwired request metrics (#4481)
Refs rustfs/backlog#1006

- aggregate request traffic samples per type to avoid future counter collisions
- keep request schema and collector crate-internal until a production stats source exists
- preserve focused regression coverage for the internal request collector logic

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 13:08:57 +00:00
houseme 0cb76e9420 fix(obs): harden metrics scheduler (#4479)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 13:04:42 +00:00
houseme 54872d52dc test(ecstore): deterministic rename_data crash-consistency harness (#4478)
The rename_data commit sequence is the highest-risk durability path in the
store, and the HP-1/HP-4/HP-5 work (fsync coalescing, group commit, relaxed
durability tiers) all need a standing gate that proves a power loss mid-commit
can never leave a mixed or corrupt object. There was one graceful-rollback
failpoint but no crash-consistency coverage.

Add a deterministic harness that models a hard power loss — the commit
sequence stops dead at the armed step with no in-process cleanup — and then
reopens the disk to assert the raw on-disk state is coherent:

- Two pre-commit injection points, RenameDataCrashPoint::{AfterDataRename,
  AfterBackupBeforeMetaCommit}, constructed at the real commit-path call sites
  but gated so the production build compiles them to a const-false no-op
  (mirrors the existing should_fail_before_old_metadata_backup pattern).
- A parameterized scenario over {strict, relaxed} durability x {both crash
  points} x {overwrite, fresh object}: seed, stage a replacement, inject,
  reopen, and assert the object reads back as exactly the old version (or does
  not exist when there was no old version) with its data dir intact, never the
  half-committed new one. The un-injected run asserts the commit makes the new
  version visible. Relaxed is held to the same old-or-new invariant as strict
  (only the durability window widens), which is exactly the property the
  durability-relaxation work must not break (rustfs/backlog#878 hard rule).
- Wire an explicit `ecstore-crash-consistency` gate into the destructive
  profile of run_ecstore_validation_suite.sh so it runs in the standing suite
  (coordinates with the #878 destructive profile rather than building a
  parallel one).

Production behavior is unchanged: the injection guards are no-ops outside
tests, and the existing rename_data rollback tests still pass.

Refs: rustfs/backlog#935 (HP-14 crash-consistency harness), rustfs/backlog#896
(test plan), rustfs/backlog#878 (ECStore validation suite), rustfs/backlog#936

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 20:53:49 +08:00
houseme 4fe0789fb1 fix(obs): honor otlp export contracts (#4477)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 12:41:18 +00:00
houseme 47bee8b314 perf(ecstore): read bitrot hash+data in one pass on the shard path (#4475)
* perf(ecstore): read bitrot hash+data in one pass on the shard path

BitrotReader::read issued two reads per block: one read_exact for the
32-byte hash, then a separate loop for the shard data. On the streaming
disk reader (a raw tokio File whose every read is a spawn_blocking
round-trip) that is two dispatches per block. Since the on-disk layout is
a contiguous [hash][data] run, pull both in a single pass into a reused
scratch buffer and split afterwards, halving the per-block dispatch count
on the streaming path. The no-hash path still reads straight into the
caller buffer with no extra copy, and an in-memory Cursor (inline/mmap)
just does a slice copy.

All existing invariants are preserved: a short read of either the hash or
the data maps to UnexpectedEof before and independent of the hash check
(backlog#799 B2), and the hash-mismatch / InvalidData semantics are
unchanged.

Correctness-only change; the per-dispatch latency win is platform
dependent and its default reliance is left to the warp size-bucket
benchmark gate tracked by backlog#935, per the backlog#933 acceptance
note.

Refs: rustfs/backlog#933 (HP-12 item 2), rustfs/backlog#936

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs: reword bitrot test comment to satisfy typos check

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 11:27:13 +00:00
houseme 1eb393cab1 fix(obs): align metrics schema and collector contracts (#4476)
fix(obs): align schema and collector contracts

Refs rustfs/backlog#1005

- align obs schema descriptors with emitted labels and metric types
- fix bucket traffic help text and TTFB bucket descriptor semantics
- add regression tests for drive, network host, bucket, and node bucket contracts

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 11:23:23 +00:00
houseme 3ddade24f2 fix(obs): validate numeric env settings (#4474)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 10:42:09 +00:00
Henry Guo 757f9b3b7b fix(admin): refresh accountinfo bucket usage (#4472) 2026-07-08 18:34:11 +08:00
Zhengchao An 819e1422fb fix(replication): resolve same-destination rules highest-priority first (#4452) 2026-07-08 18:32:09 +08:00
Zhengchao An 9ffedc0e2b fix(keystone): fail closed on EC2 auth and apply configured timeout (#4464) 2026-07-08 18:31:48 +08:00
Zhengchao An 7a33ba5e21 fix(notify): handle processing_events underflow and literal ? matching (#4468) 2026-07-08 18:30:48 +08:00
houseme 718c051ff3 fix(obs): export cluster usage staleness (#4467)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 09:44:05 +00:00
Zhengchao An 3430a7ead6 fix(object-capacity): don't report dirty-subset bytes as the cluster total (backlog#1011) (#4463)
A dirty-subset refresh scans only the dirty disks, so its raw
`CapacityUpdate` carries just that subset's `total_used`/`file_count`.
`update_capacity` recomputed the correct cluster-wide total into a local
variable and wrote it to the cache, but never wrote it back to the
`CapacityUpdate` that `refresh_or_join`/`spawn_refresh_if_needed` return
and publish to joiners. The admin blocking path (`resolve_admin_used_capacity`
-> `refresh_or_join_admin_disks(allow_dirty_subset=true)`) consumes
`update.total_used`, so a single dirty disk in an N-disk cluster made
admin StorageInfo report only the scanned subset's bytes (a large
transient undercount for that request and same-cycle joiners).

`CachedDiskCapacity` also dropped each disk's `file_count`/`is_estimated`,
so subset refreshes could not recompute a correct cluster file count and
would launder an estimated per-disk value into an exact cluster total.

Fix:
- `update_capacity` now reconciles `total_used`, `file_count`, and
  `is_estimated` from the full per-disk cache and returns the corrected
  `CapacityUpdate`.
- `CachedDiskCapacity` stores `file_count`/`is_estimated` per disk;
  `file_count` sums the cache and `is_estimated` is the OR across disks.
- `refresh_or_join` and `spawn_refresh_if_needed` rebind their result to
  the reconciled update so the leader return value and the value
  published to joiners both carry cluster totals.

Tests: extend the subset-refresh test to assert the returned update's
reconciled `total_used`/`file_count`/`is_estimated`, and add a
`refresh_or_join` dirty-subset test asserting the leader returns the
merged cluster total (not the subset sum) and matches the cache.

Refs: https://github.com/rustfs/backlog/issues/1011
2026-07-08 09:33:35 +00:00
Zhengchao An c138265440 refactor(ecstore): migrate the S3 region into InstanceContext (Phase 5 Slice 4) (#4462)
Phase 5 Slice 4 (backlog#939): move the S3 region — a write-once identity
scalar — out of the GLOBAL_REGION process static into the per-instance
InstanceContext, so two instances can serve different regions.

- InstanceContext gains `region: OnceLock<Region>` with `set_region()` /
  `region()`. `set_region` keeps the write-once fail-fast contract: a second
  write panics, exactly as the process global did (not downgraded to a warn).
- global.rs `set_global_region` / `get_global_region` keep their signatures and
  forward to the current instance's context; the GLOBAL_REGION static is
  removed. Single-instance: startup writes the bootstrap context (which the
  ECStore adopts), so reads are unchanged.

Tests: set/get round-trip, two contexts hold distinct regions, and a second
set_region panics (fail-fast preserved).

Verification: cargo test -p rustfs-ecstore (9 instance-context tests green),
cargo clippy -p rustfs-ecstore --all-targets (clean), make pre-commit (pass).

Refs: backlog#939 (Phase 5, Slice 4). Stacked on Slice 3 (#4417).
2026-07-08 09:31:46 +00:00
Zhengchao An 7aa25387ae fix(heal): gate finalize on transient skips and fix progress failed count (#4460)
The erasure-set finalize block only gated completion on failed_objects, so a pass with transient skips (unmet quorum, DiskNotFound, SlowDown, OperationCanceled) but zero hard failures was marked completed, its resume/checkpoint state cleaned up and the per-disk healing marker cleared. That violated the Transient invariant: the skipped versions were never re-healed on a later pass.

Broaden the finalize gate to failed_objects > 0 || skipped_objects > 0 and reuse the existing bounded-retry path (schedule_retry + checkpoint reset_for_retry, returning Err so the caller preserves state and keeps the healing markers). Transient conditions are deferred to the next heal cycle, never hot-retried in place.

Also fix the object/EC-decode heal success paths, which passed object_size as the failed positional arg to update_progress, corrupting objects_failed and the admin-visible success rate; pass 0 instead.

Add heal tests for the transient-skip finalize behavior and a progress test asserting a successful heal reports zero failures.

Refs rustfs/backlog#1033
2026-07-08 09:26:56 +00:00
Zhengchao An 2b33af6e48 fix(trusted-proxies): validate full Forwarded chain and bare IPv6 (#4461) 2026-07-08 09:25:10 +00:00
Zhengchao An ee6f791100 fix(audit,targets): redact credential request headers from audit/notify entries (backlog#963) (#4459)
extract_params_header copied every request/response header verbatim into
the maps that feed audit entries (requestQuery/responseHeader) and
notification events (req_params). Sensitive headers such as
Authorization and X-Amz-Security-Token were serialized in plaintext and
forwarded to external sinks (webhook/kafka/file), leaking long-lived
credentials.

Redact credential-bearing headers at this single chokepoint: the header
name is kept for correlation, but its value is replaced with the shared
REDACTED_SECRET placeholder. A case-insensitive sensitive-header list
covers authorization, x-amz-security-token, x-amz-content-sha256,
cookie, and set-cookie. Non-sensitive headers keep their existing
behavior.

Refs: https://github.com/rustfs/backlog/issues/963
2026-07-08 09:23:05 +00:00
Zhengchao An afa3935ebb fix(object-data-cache): prune identity index on moka eviction (#4458)
Moka evicts entries by TTL, time-to-idle, and capacity-LRU without going through invalidate_object, so the per-object identity index (by_object) never dropped the evicted keys and grew without bound, bypassing the memory cap. Register an async eviction listener that removes truly evicted keys from the identity index, holding only a Weak reference to avoid an Arc cycle.

Refs rustfs/backlog#1031
2026-07-08 09:14:46 +00:00
houseme dea53616a4 fix(obs): correct versions scanned metrics (#4443)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-08 09:12:02 +00:00
houseme a30a9c0aba fix(obs): align cluster capacity semantics (#4457)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 09:11:01 +00:00
Henry Guo 506cd156bb fix(scanner): scope long walk timeouts (#4376)
* fix(scanner): scope long walk timeouts

* fix(scanner): bound IAM config walks

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-08 17:06:51 +08:00
Zhengchao An 7fb95d4fc0 fix(multipart): lock upload metadata reads and aborts (#4428) 2026-07-08 17:02:42 +08:00
Zhengchao An 686b39bbef fix(notify): report cursor gap when history is evicted instead of silently dropping events (#4446)
`LiveEventHistory::snapshot_since` resumed from the oldest retained event
whenever a consumer's cursor pointed before it, without signaling that the
in-between events had been evicted from the ring buffer. Cursor-based
consumers therefore assumed the returned batch was contiguous with their
cursor and silently lost events.

Add a `gap` flag to `LiveEventBatch`, set when the consumer's cursor is
older than the oldest retained sequence, so consumers can detect the loss
and trigger a full re-sync or alert. Add a regression test that fills and
evicts past the ring-buffer capacity and asserts a lagging cursor reports
`gap = true` (and that contiguous/caught-up cursors do not).

Refs rustfs/backlog#969
2026-07-08 17:02:36 +08:00
Zhengchao An fefa70b31e fix(ecstore): stop ListMultipartUploads from returning one upload past max-uploads (#4447)
fix(ecstore): stop ListMultipartUploads from returning one upload past max-uploads (backlog#954)

The result-collection loop in `SetDisks::list_multipart_uploads` pushed an
upload into the page and only then checked `ret_uploads.len() > max_uploads`,
so each page returned up to max_uploads + 1 uploads and pointed
`next_upload_id_marker` at that surplus entry, violating the S3
ListMultipartUploads max-uploads contract.

Move the cap check before the push (MinIO-style) so a page never exceeds
max_uploads, and derive `is_truncated` from the post-marker cursor position
(`upload_idx < uploads.len()`) rather than comparing the page length against
the full listing length. The old length comparison mis-reported truncation on
the final page whenever a marker had skipped earlier entries, which prevented
marker-based pagination from terminating.

Add a regression test that starts more in-progress uploads than a page holds
and asserts a single page returns exactly max_uploads with a correct
next-marker, that the exact-boundary case is not marked truncated, and that
paginating one upload at a time enumerates every upload once with no loss,
duplication, or non-termination.

Incidental: add the missing `ctx` field to the `new_multipart_lock_test_store`
cfg(test) helper so the ecstore test build compiles after the #4413/#4437
merge collision.

Refs: https://github.com/rustfs/backlog/issues/954
2026-07-08 17:02:28 +08:00
Zhengchao An d5687e1693 fix(policy): compare NotResource in Statement equality (#4454)
Statement equality drives merge/dedup of policy statements. Omitting NotResource let semantically-distinct statements be treated as duplicates and dropped, which can shrink Deny coverage and escalate privileges. Compare not_resources too and add regression tests.

Refs rustfs/backlog#1028
2026-07-08 17:02:20 +08:00
Zhengchao An 04bf2e7b99 fix(lifecycle): use total order to select eval_inner event (#4455) 2026-07-08 17:02:09 +08:00
Zhengchao An 7e2e553981 fix(signer): use real timestamp and standard base64 for SigV2 (#4456) 2026-07-08 17:02:01 +08:00
houseme 92c8c6db75 perf(ecstore): Linux O_DIRECT shard-write path off the commit critical section (#4411)
`create_file` writes erasure shard / multipart part data through the page
cache, so the commit-point `sync_dir_files` fdatasync in `rename_data` has to
flush the whole shard's dirty pages (profiling: create_file avg 67.52ms vs
1.51ms nosync; BitrotWriter::write p95 40.86ms), and concurrent PUTs on one
device stall each other's writeback inside the rename critical section.

Add an opt-in Linux O_DIRECT streaming writer that reuses the direct-io read
infrastructure (statx DIOALIGN probe, aligned bounce buffer, supported latch,
one-time fallback log). Incoming bytes are staged into an aligned buffer;
full aligned batches and the shutdown tail are flushed on the blocking pool
(same offloading posture as the buffered writer it replaces). The
sub-alignment tail is written buffered after clearing O_DIRECT via fcntl
(MinIO's recipe). Shard bytes reach the device during the write phase, so the
unchanged commit-point `sync_dir_files` fdatasync degrades to a cheap
metadata/device FLUSH instead of flushing ~2MiB of dirty pages.

Gated by `RUSTFS_OBJECT_DIRECT_IO_WRITE_ENABLE` (default false), Linux-only,
and durability-neutral: `sync_dir_files` still fdatasyncs every shard at the
commit point, so the strict/relaxed/none durability tiers keep their exact
guarantees. Filesystems that reject O_DIRECT (tmpfs, overlayfs, 9p) latch the
path off and fall back to buffered writes; an O_DIRECT open/write EINVAL is
never surfaced as `InvalidInput` (which `to_file_error` maps to `FileNotFound`
and would masquerade as a missing shard, triggering a spurious EC rebuild).

Unit tests cover the env gate, the alignment/staging-capacity and tail-split
math, an end-to-end `create_file` round trip across sizes crossing the
alignment and multi-batch boundaries (buffered fallback on macOS / O_DIRECT on
a block device), and the writer state machine over a plain file on Linux. Real
O_DIRECT throughput on Linux NVMe (ext4/xfs) is deferred to the azure 4-node
bench per the issue's GO/NO-GO gate; the development host is macOS and cannot
exercise O_DIRECT.

Refs: rustfs/backlog#927, rustfs/backlog#936

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:39:43 +08:00
houseme 021c955c21 fix(obs): report live replication backlog (#4448)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:39:06 +08:00
houseme 13e48d93aa perf(ecstore): support per-bucket durability tier overrides (#4407)
perf(ecstore): per-bucket durability tier overrides (HP-5 phase 2)

Let a bucket override the process-wide RUSTFS_DURABILITY_MODE with its own
strict/relaxed/none tier, stored as a durability.json extension entry in the
bucket metadata file and resolved at commit points via effective_durability.
System-critical buckets (.rustfs.sys, .minio.sys) can never carry an override
and stay pinned to strict; the legacy full-off switch keeps its historical
semantics and per-bucket overrides do not apply under it. Overrides are
published and cleared through the existing bucket metadata cache-invalidation
path, and an admin GET/PUT handler exposes the configuration. Default behavior
is unchanged: with no override a bucket follows the global mode, which defaults
to strict and stays byte-for-byte identical to before.

Refs: https://github.com/rustfs/backlog/issues/938, https://github.com/rustfs/backlog/issues/936

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:33:49 +08:00
Zhengchao An 09e6983175 fix(object-capacity): use u128 intermediate to stop sampling estimate overflow (#4444)
fix(object-capacity): use u128 intermediate to stop sampling estimate overflow (backlog#1012)

The three sampling extrapolation sites in scan.rs computed
`overflow_sampled_bytes.saturating_mul(overflow_count) / sampled_count`,
multiplying in u64 before dividing. On very large disks the product exceeds
u64::MAX and saturates to a constant, so dividing by a sampled_count that grows
with disk size yields an estimate that monotonically shrinks — bigger disks
report less capacity. It only sets is_estimated, with no alert.

Extract a shared `estimate_overflow_bytes` helper that performs the
multiplication in u128, divides, then clamps to u64::MAX, so the intermediate
product can never overflow. It also guards division by zero with `.max(1)` even
though callers only enter the branch when sampled_count > 0.

Adds unit tests covering the realistic ~105 TB regression scenario (asserting
the inputs actually overflow u64 and the fixed estimate dwarfs the saturated
value), monotonic scaling across disk sizes, extreme-value clamping, small
non-overflowing inputs (unchanged), and the zero-sampled-count guard, all
compared against a u128 reference implementation.

Refs: https://github.com/rustfs/backlog/issues/1012
2026-07-08 16:30:43 +08:00
Zhengchao An a84d7ca0b3 fix(targets): supervise and restart the MQTT event loop after fatal errors (#4445)
The MQTT target spawned its rumqttc event loop exactly once through a
`OnceCell`. When the loop exited on a fatal protocol error, the finished
`JoinHandle` stayed in the cell, so `init()` never respawned it. The target
then went permanently silent: `publish` calls kept enqueuing but nothing was
ever delivered, while the target still looked healthy.

Replace the one-shot spawn with a supervisor task. The `OnceCell` now guards
a single supervisor that runs one event-loop session at a time and, when a
session exits, rebuilds the client and event loop from the latest
`MqttOptions` after an exponential backoff (1s..30s). A session that connected
resets the backoff so a transient drop reconnects promptly; repeated immediate
failures back off to avoid a reconnect storm. Cancellation from `close()` is
handled by the supervisor dropping the in-flight session future, so no
per-session cancel channel is needed.

Rebuilding options per session also keeps TLS hot-reload working across
reconnects. The backoff policy is a pure function and the supervisor loop is
generic over the session runner, so both are unit tested without a broker:
one test asserts the session restarts repeatedly until cancelled, another that
a live session stops promptly on cancel.

Refs: https://github.com/rustfs/backlog/issues/972
2026-07-08 16:30:16 +08:00
houseme f6433ebb8b fix(obs): drop placeholder drive series (#4440)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:29:48 +08:00
yihong bef276611b fix: make precommit all happy (#4451)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2026-07-08 16:28:44 +08:00
houseme c1cc0d189f fix(obs): retire stale bucket and target series (#4434)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:23:25 +08:00
houseme acb1b765db fix(obs): export resettable metrics as gauges (#4432)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:23:11 +08:00
houseme d7b55c9762 fix(obs): honor profiling export contracts (#4433)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:22:47 +08:00
houseme 5aa25a3047 fix(obs): reuse network snapshots across ticks (#4431)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:22:35 +08:00
houseme 8df474b41b fix(obs): shut down global telemetry guard (#4430)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:22:23 +08:00
houseme e0a46e940c fix(obs): drain cleaner results inside scope (#4429)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 16:21:52 +08:00
Zhengchao An 183c1d8cb4 fix(targets): disable HTTP redirects on webhook delivery client (#4420)
fix(targets): disable HTTP redirects on webhook delivery client (backlog#974)

The webhook reqwest client used the default redirect policy (follows up to
10 redirects). Even though the configured endpoint is validated against
outbound-egress rules, a malicious or compromised endpoint could return a
3xx response to bounce the outbound request to an internal address (e.g. the
cloud metadata service at 169.254.169.254), bypassing that validation (SSRF).

- Set `.redirect(reqwest::redirect::Policy::none())` on the delivery client
  so redirects are never followed.
- Treat a 3xx response as a delivery failure (error carries the status code)
  instead of silently succeeding.
- Add a regression test that stands up a loopback server returning a 302 to
  an internal metadata address and asserts the client surfaces the 3xx rather
  than following it.

Refs: https://github.com/rustfs/backlog/issues/974
2026-07-08 16:05:37 +08:00
houseme f968129945 fix(obs): stop exporting fake cpu categories (#4439)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-08 16:05:11 +08:00
Zhengchao An b06f3df6b6 fix: repair main build (swift clippy assert + ecstore multipart test ctx) (#4441)
* fix(swift): replace assert!(false) with panic! in expiration_worker test

clippy::assertions_on_constants fails the swift clippy CI job under -D warnings.

* fix(ecstore): add missing ctx field to multipart lock test store

The multipart list-parts lock test (#4437) constructs ECStore without the
ctx field added by the Phase 5 InstanceContext work (backlog#939). Both
landed on main independently, leaving a semantic conflict that breaks the
ecstore lib-test build (E0063). Adopt the process bootstrap context,
matching the existing bootstrap_ctx() test in store/mod.rs.
2026-07-08 08:01:12 +00:00
Zhengchao An 93ffbdb9bd fix(ecstore): lock multipart part listings (#4437) 2026-07-08 15:02:26 +08:00
Zhengchao An a48bc89cdc fix(ecstore): lock batch object deletes (#4435)
* fix(ecstore): lock batch object deletes

* fix(ecstore): honor no_lock in batch deletes
2026-07-08 15:02:22 +08:00
Zhengchao An df9cbc4ed1 fix(ecstore): validate erasure distribution values to avoid shuffle index panic (#4427)
fix(ecstore): validate erasure distribution values to avoid shuffle index panic (backlog#949)

The element values of `erasure.distribution` read from `xl.meta` were never
range-checked. `FileInfo::is_valid()` and `MetaObjectV1::valid()` only verified
`distribution.len()` and the `erasure.index` bound, not that each distribution
value is a valid 1-based slot in `[1, N]`. The metadata shuffle helpers then use
these values directly as `distribution[k] - 1` indices, so a corrupt or
adversarial `xl.meta` carrying a `0` (usize underflow) or a value greater than N
(out-of-bounds) triggers a panic in the shuffle path, turning bad-disk metadata
that erasure coding is meant to tolerate into a request/task crash.

Fix, two layers:
- Validate distribution values at metadata acceptance: `is_valid_distribution`
  now requires the distribution to be a permutation of `1..=N` (correct length,
  every value in range, no duplicates). `FileInfo::is_valid()` and
  `MetaObjectV1::valid()` use it, so `find_file_info_in_quorum` rejects corrupt
  metadata and it surfaces as a clean `ErasureReadQuorum` error instead of an
  index path.
- Defensive indexing in the shuffle helpers (`shuffle_disks_and_parts_metadata`,
  `_by_index`, `_by_index_owned`, `shuffle_parts_metadata`, `shuffle_disks`,
  `shuffle_check_parts`): out-of-range distribution values are skipped via
  `checked_sub(1)` + bounds-checked slot access instead of a bare `idx - 1`
  index, matching the existing pattern in
  `collect_inline_data_shard_fileinfos_by_index`.

Regression tests: `is_valid_distribution`/`is_valid`/`valid` reject distributions
containing `0`, values greater than N, duplicates, and wrong length while
accepting valid permutations; the shuffle helpers no longer panic on corrupt
distributions and preserve output length.

Refs: https://github.com/rustfs/backlog/issues/949
2026-07-08 15:02:16 +08:00
Zhengchao An 2490d4ee22 fix(notify): serialize persisted config read-modify-write to prevent lost updates (#4425)
fix(notify): serialize persisted config read-modify-write to prevent lost updates (backlog#968)

The notify config write path performed a read -> modify -> write over the
persisted server config with no serialization: two concurrent updates could
both read the same base config, apply disjoint changes, and race their
full-config writes. The later write silently overwrote the earlier one,
losing updates (e.g. adding two targets concurrently could drop one) with no
error surfaced.

Guard the whole read -> modify -> write with a process-global tokio Mutex
(NOTIFY_CONFIG_RMW_LOCK). The persisted config is a single process-global
resource, so serializing the RMW makes concurrent updates apply in sequence
and every change is preserved. The lock is only acquired inside
update_server_config and is released before the in-memory reload, so it never
nests with the per-manager config RwLock and introduces no lock-ordering risk.

The RMW sequence is extracted into serialized_read_modify_write with injected
read/save operations so the exact production serialization path is exercised
by a regression test that concurrently adds 32 distinct targets and asserts
all of them survive.

Refs: https://github.com/rustfs/backlog/issues/968
2026-07-08 15:02:10 +08:00
Zhengchao An dbc628f169 fix(audit): propagate dispatch delivery failures instead of swallowing them (#4424)
fix(audit): propagate dispatch delivery failures instead of swallowing them (backlog#962)

AuditPipeline::dispatch and dispatch_batch accumulated per-target save()
errors, logged them, and then unconditionally returned Ok(()). When every
configured target failed the audit entry was lost outright (for store-backed
targets a failed save() means the event was neither delivered nor persisted
for replay), yet callers such as dispatch_audit_log saw success and assumed
the audit trail was intact. Audit is a compliance-critical path, so a total
delivery failure that reports success is a silent data-loss bug.

Both methods now distinguish three outcomes: all targets succeeded (Ok),
partial failure where at least one target accepted the event (log a warning
and return Ok, since the entry is not lost), and total failure where no
delivery succeeded while errors were recorded (record the failure metric,
log an error, and return Err(AuditError::Target) carrying the first target
error). This lets the caller react instead of assuming success.

Adds regression tests with a FailingTarget mock asserting that dispatch and
dispatch_batch return Err on total failure and Ok on partial failure.
2026-07-08 15:02:05 +08:00
Zhengchao An 2adf33fcd2 fix(targets): surface truncated store batches instead of returning partial entries (#4423)
fix(targets): surface truncated store batches instead of silently returning partial entries (backlog#967)

get_multiple deserializes a persisted batch by pulling item_count items
from a concatenated JSON stream. When the stream ended early (a truncated
or corrupted batch file), the partial-read branch only logged a warn! and
broke out of the loop, returning Ok(partial). Callers then treated the
batch as fully delivered, deleted the store entry, and permanently lost
the remaining events with no error.

Return StoreError::Deserialization as soon as the stream ends before
item_count items are read, so the caller keeps the entry on disk and can
retry rather than silently dropping events. The empty-file case already
returned an error and remains covered by this path.

Add a regression test that writes a 3-item batch, truncates the file at a
clean boundary after two items, and asserts get_multiple returns Err and
leaves the batch file in place.

Refs: https://github.com/rustfs/backlog/issues/967
2026-07-08 15:02:02 +08:00
Zhengchao An e0972f19ac fix(scanner): cache object lock config (#4422)
* fix(scanner): cache object lock config

* test(scanner): update lifecycle scanner items
2026-07-08 15:01:57 +08:00
Zhengchao An c0d5f938f2 fix(audit): resolve ABBA lock-order deadlock between registry and stream_cancellers (#4421)
* fix(audit): resolve AB-BA lock-order deadlock between registry and stream_cancellers (backlog#961)

`AuditSystem::runtime_status_snapshot` acquired `stream_cancellers.read()`
before `registry.lock()`, while every other path that holds both locks
(`clear_runtime_targets`, `AuditRuntimeFacade::replace_targets`) acquires
`registry` first. Under concurrency (e.g. admin status polling racing a
config reload) this AB-BA ordering could deadlock the whole audit control
plane with no self-recovery.

Reorder `runtime_status_snapshot` to acquire `registry` before
`stream_cancellers`, unifying the global lock order to
`registry -> stream_cancellers` across all double-lock paths, and add a
multi-threaded regression test that hammers both critical sections with a
timeout guard to fail fast if the ordering regresses.

Refs: https://github.com/rustfs/backlog/issues/961

* docs(audit): reword AB-BA to ABBA to satisfy typos check (backlog#961)
2026-07-08 15:01:53 +08:00
Zhengchao An cf89291898 fix(notify): strip rustfs/minio internal metadata from event userMetadata (#4419)
fix(notify): strip rustfs/minio internal metadata from event userMetadata (backlog#964)

Notification event construction only stripped keys prefixed with
`x-amz-meta-internal-`, which is not a prefix RustFS actually uses. Real
internal metadata lives under `x-rustfs-internal-*` / `x-minio-internal-*`
(xl.meta compat, incl. `x-minio-internal-server-side-encryption-*`) and
server-side-encryption details under `x-rustfs-encryption-*` /
`x-minio-encryption-*`. None of these were filtered, so they leaked into
`s3.object.userMetadata` sent to downstream notification targets
(webhook/MQ) — an information disclosure of SSE/replication/healing
internal state.

Filter via the shared classifiers `rustfs_utils::http::is_internal_key`
and `is_encryption_metadata_key` (case-insensitive, covering both key
flavors), while retaining the legacy `x-amz-meta-internal-*` strip for
backward compat. Genuine user metadata (`x-amz-meta-*`, content-type, ...)
is preserved unchanged.

Adds a regression test asserting both internal prefixes, the SSE internal
key, both encryption prefixes (incl. mixed-case) and the legacy prefix are
stripped while user keys survive.

Refs: https://github.com/rustfs/backlog/issues/964
2026-07-08 15:01:49 +08:00
Zhengchao An f64f0ca91a fix(ecstore): report bitrot verifier mismatches as corrupt (#4414) 2026-07-08 15:01:44 +08:00
Zhengchao An 91dec123d9 refactor(ecstore): add per-instance InstanceContext, migrate erasure setup type (#4413)
* refactor(ecstore): add per-instance InstanceContext, migrate erasure setup type

Phase 5 of the global-singleton consolidation (backlog#939): begin moving
runtime identity state out of process globals so multiple ECStore instances
can coexist in one process. Isolation is carried by the object graph
(ECStore -> Sets -> SetDisks holding an Arc<InstanceContext>), not a
task-local, which does not propagate across the many internal tokio::spawn
boundaries in the data/background paths.

This first slice migrates the erasure setup type -- previously three
independent process-global bools -- into a single per-instance
RwLock<SetupType> that derives is_erasure / is_dist_erasure / is_erasure_sd,
removing a triple source of truth that could drift out of sync.

- New runtime::instance module: InstanceContext + process bootstrap context.
- The legacy free-function facade (is_erasure/update_erasure_type/...) keeps
  its signatures and forwards to the current instance's context, falling back
  to the bootstrap context before a store is published.
- ECStore gains a pub(crate) ctx field and setup_is_* accessors; its
  constructors adopt the bootstrap context (never mint a fresh one) so startup
  writes and post-construction reads share one cell -- single-instance
  behavior is byte-for-byte unchanged.

Tests: erasure predicate derivation vs the legacy behavior, object-graph
carrier isolation across two ECStore instances, and bootstrap adoption.

Refs: backlog#939 (Phase 5, Slice 1), backlog#653 (item 8)

* refactor(ecstore): thread InstanceContext down the object graph (Phase 5 Slice 2) (#4415)

* refactor(ecstore): source the namespace lock manager per-instance (#4417)

refactor(ecstore): source the namespace lock manager per-instance (Phase 5 Slice 3)

Phase 5 Slice 3 (backlog#939): give each instance its own lock namespace by
sourcing SetDisks' lock manager from the instance context instead of the
process singleton. This removes the false cross-instance mutual exclusion (and
attendant ABBA risk) that a shared GlobalLockManager would cause once multiple
instances coexist.

- InstanceContext gains a `lock_manager: Arc<GlobalLockManager>`. `new()` mints
  a fresh manager (independent per-instance); `bootstrap_ctx()` aliases the
  process singleton via get_global_lock_manager(), so a single-instance
  deployment keeps exactly one shared namespace.
- SetDisks::new sources `local_lock_manager` from `ctx.lock_manager()` (the ctx
  it already adopts), not `runtime_sources::global_lock_manager()`. Single
  instance: same Arc as before, so behavior is unchanged.
- Remove the now-unused `runtime_sources::global_lock_manager()` wrapper.

Tests: bootstrap lock manager aliases the process singleton; two fresh contexts
own distinct managers; a SetDisks' lock manager is the one from its context and
aliases the global singleton in a single-instance build.

Verification: cargo test -p rustfs-ecstore (10 Phase 5 + set_disk locking
regressions green), cargo clippy -p rustfs-ecstore --all-targets (clean),
make pre-commit (pass).

Refs: backlog#939 (Phase 5, Slice 3). Stacked on #4415 (Slice 2).
2026-07-08 15:01:40 +08:00
Zhengchao An cda7688909 fix(multipart): clean temp part data on failure (#4412)
fix(multipart): clean failed part temp data
2026-07-08 15:01:37 +08:00
Zhengchao An 80cc3b1fcf fix(replication): preserve SSE-C checksum state (#4410)
* fix(replication): preserve ssec checksum state

* fix(replication): route ssec checks through boundary
2026-07-08 15:01:33 +08:00
Zhengchao An 9a7255540b fix(replication): add resync metrics (#4408) 2026-07-08 15:01:28 +08:00
Zhengchao An 1e6207c08e fix(lock): fence write commit on lock loss (#4406)
fix(lock): fence write commit on lock loss (backlog#899 Phase 2)

Phase 0+1 (#4388) made object write locks refreshable and marks the guard
lost when the heartbeat can no longer refresh a quorum, but does not act on
it. Under a partition a long write's lock can expire on an unreachable node
and be reclaimed, letting a third party re-acquire it; the original writer
keeps going and both commit -- a double write.

Expose the loss signal through NamespaceLockGuard::is_lock_lost() and
ObjectLockDiagGuard::is_lock_lost(), and fence the commit in put_object and
complete_multipart_upload: immediately before rename_data (the atomic commit
point), abort with a retryable NamespaceLockQuorumUnavailable (503) if the
lock was lost. In multipart the check precedes cleanup_multipart_path so a
lost lock leaves the upload intact and retryable. A write that already
reached rename_data Ok is durable and never aborted.

The loss criterion is unchanged (reacts to Phase 1's signal). Heal and the
long-GET read side are deferred follow-ups.
2026-07-08 15:01:24 +08:00
Zhengchao An f327707321 fix(ecstore): clear lifecycle metadata cache (#4416) 2026-07-08 07:01:09 +00:00
Zhengchao An 7cd7c84e71 feat(swift): track object expiration (#4409)
fix(swift): wire object expiration tracking
2026-07-08 14:30:46 +08:00
Zhengchao An 976a5d9713 fix(s3-types): give internal event names a mask base case to stop infinite recursion (backlog#965) (#4418)
EventName::mask() recursed forever for the three internal leaf events
(ObjectRemovedAbortMultipartUpload, ObjectCreatedCreateMultipartUpload,
ObjectRemovedDeleteObjects): their discriminants fall past
LAST_SINGLE_TYPE_VALUE so mask() took the compound branch, but expand()
returns vec![*self] for them, so mask() called itself with the same
value and overflowed the stack.

Detect the self-expanding leaf case (expand() yields exactly self) and
give it a dedicated bit derived from its discriminant. Those bits sit
above the single-type bits, so they never collide with each other or
with any compound 'All' mask.

Add exhaustive regression tests: every variant's mask() terminates, the
three internal masks are non-zero and mutually distinct and disjoint
from the single-type bits, and Everything covers all single-type bits.

Refs: https://github.com/rustfs/backlog/issues/965
2026-07-08 05:57:47 +00:00
Zhengchao An ea49d5686b fix(replication): mark failed targets offline (#4405) 2026-07-08 12:08:22 +08:00
Zhengchao An 1acd47f15e fix: SSE crash-loop DoS + credential reserved-char bypass (backlog#806) (#4404) 2026-07-08 10:05:51 +08:00
cxymds d25ddb0e1e fix(logging): reduce listing cancellation error noise (#4372)
* fix(logging): reduce listing cancellation error noise

* fix(ecstore): preserve filemeta io error kind

* fix(ecstore): avoid redundant clone lint in test

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-08 09:35:24 +08:00
Zhengchao An 3c3113619e fix(protocols): use constant-time secret comparison in FTPS and WebDAV auth (#4403)
The FTPS and WebDAV authentication handlers compared the client-supplied secret
key against the stored secret with `String::eq`, which short-circuits on the
first differing byte. A network attacker who knows (or enumerates) a valid
access key can recover the secret key byte-by-byte via response-timing analysis;
neither path is rate limited.

Switch both to a constant-time comparison using `subtle::ConstantTimeEq`, the
same primitive the SFTP handler and `rustfs/src/auth.rs::constant_time_eq`
already use. `subtle` is added to the `ftps` and `webdav` feature dependency
sets (it was previously gated on `sftp` only).

Addresses GHSA-3p3x-734c-h5vx.
2026-07-08 09:31:51 +08:00
Zhengchao An 7b20554056 fix(credentials): fail closed when deriving RPC secret from default credentials (#4402)
The internode RPC HMAC secret is derived from the S3 credential pair via
`derive_rpc_secret` when `RUSTFS_RPC_SECRET` is unset. The derivation uses the
secret key as the HMAC key, so when the default secret key (`rustfsadmin`) is in
effect the derived RPC secret is a fixed, publicly computable value. Any network
peer can then forge valid `x-rustfs-signature` headers and invoke internode RPC
routes (e.g. `read_file_stream`), bypassing S3 IAM entirely.

`normalize_rpc_secret` already rejected the literal default when it was supplied
directly, but `resolve_rpc_secret` still derived a secret from the default
credential pair. Make the derivation path fail closed: refuse to derive while
the default secret key is in effect, forcing operators to set `RUSTFS_RPC_SECRET`
(or a non-default `RUSTFS_SECRET_KEY`). A default access key paired with a
non-default secret key remains safe and is still allowed.

Addresses GHSA-68cw-96m3-h2cf (incomplete-fix follow-up to CVE-2026-45039).
2026-07-08 09:31:25 +08:00
Zhengchao An 0fad356450 fix(replication): send versionId on version-purge deletes to generic S3 targets (#4401)
Replication deletes dropped the S3 `versionId` query parameter for every
replication request, conveying the version only via the internal
x-*-source-version-id header. A generic (non-MinIO/RustFS) S3 target ignores
that header, so a version purge degenerated into a delete-marker creation while
the source still stamped VersionPurgeStatus=Complete — a silent divergence.

Only omit the query `versionId` when propagating a delete-marker CREATION
(the target must mint its own marker); version purges, delete-marker purges and
force deletes now address the exact version. Extracted into
resolve_delete_api_version_id with unit coverage.

Fixes rustfs/backlog#857
2026-07-08 09:29:58 +08:00
Zhengchao An f8ee0e7071 fix(crypto): reject plaintext fallback without crypto (#4391)
* fix(crypto): reject plaintext fallback without crypto

* test(crypto): gate no-feature regression under cfg
2026-07-08 09:29:37 +08:00
Zhengchao An 2b063b0c4a fix(heal): repair all object versions during disk-replacement heal (#4400)
Disk-replacement heal previously repaired only the latest version of each
object and never enumerated objects whose latest version is a delete marker,
so old versions were left unrepaired on a replaced drive.

Switch heal enumeration from list_objects_v2 (latest-only) to
list_object_versions (every version incl. delete markers), thread the concrete
version_id into the existing per-version heal_object, and make resume
cursor-based instead of positional: an opaque (marker, version_marker) paging
token persisted in ResumeState, a length-prefixed injective per-version dedup
key, schema_version bumps (v2) migrated independently in each of the two
persisted files, and a retry that resets both managers together (fixing a
latent rescan-skips-everything defect). Adds a real-disk-wipe e2e regression
suite proving old versions and delete-marker-latest objects are physically
restored.

Fixes rustfs/backlog#918
Fixes rustfs/backlog#919
Closes rustfs/backlog#854
2026-07-08 08:58:52 +08:00
houseme 4715dbed01 fix(ecstore): stop rejecting recoverable degraded reads in codec parity verification (#4399) 2026-07-08 08:46:01 +08:00
houseme a413729b16 perf(delete): gate and parallelize DeleteObjects per-object stat fanout (#4398) 2026-07-08 08:45:47 +08:00
houseme eaff17cade perf(ecstore): add strict/relaxed/none durability modes over the drive-sync switch (#4397) 2026-07-08 08:45:33 +08:00
houseme 92bf55ce62 perf(ecstore): right-size BytesMut encode ingest capacity to the EC-expanded block (#4396) 2026-07-08 08:45:21 +08:00
Zhengchao An afc7f1d6f9 fix(ecstore): make post-commit old data dir cleanup best-effort (#4386)
* fix(ecstore): make post-commit old data dir cleanup best-effort (backlog#898)

A write is authoritatively committed once rename_data returns Ok (the new
version is durable on >= write_quorum disks and immediately readable). The
subsequent reclamation of the now-dereferenced old object/<data_dir> is pure
space reclamation, yet commit_rename_data_dir propagated a below-quorum GC
failure via `?` into ErasureWriteQuorum -> 503, producing a false-negative
ACK for an already-persisted write. This is a deliberate divergence from
MinIO (erasure-object.go:1577), which couples the two; the divergence is
justified by durability semantics, not parity.

Changes:
- commit_rename_data_dir now returns a structured OldDataDirCleanup receipt
  and never returns Err. Adds an old==committed-dir anti-misdelete guard and
  a committed_data_dir parameter. Classification is extracted into pure
  functions (classify_old_data_dir_cleanup / map_cleanup_join_result /
  is_cleanup_not_found) so it is unit-testable. Task panic/cancel is mapped to
  a non-ignored DiskError::other (never DiskNotFound), and not-found is
  normalized to reclaimed.
- object.rs / multipart.rs consume the receipt instead of `?`. The result
  reverts to Ok, so the invalidate_get_object_metadata_cache self-heal and the
  capacity/compression accounting that a `?` early-return previously skipped
  now run on the cleanup-failure path too.
- On residue, report_old_data_dir_cleanup emits leak metrics and enqueues an
  object heal over the existing heal channel (disk-health signal replacing the
  503). heal_object -> reclaim_orphan_data_dirs already reclaims unreferenced
  local data dirs, closing the loop end to end.
- Adds rustfs_old_data_dir_* counters (attempted/reclaimed/leaked/below_quorum)
  as the operator-visible backstop for leaked residue.
- Adds a test-only (#[cfg(test)]) delete fault-injection seam; in production it
  inlines to a no-op None and has no behavioral effect.

Tests: pure-function A/C group + join-error mapping + actions decision; A5/A5b
real-disk guard/reclaim integration; end-to-end overwrite returning 200 while
old-data-dir cleanup fails. #864 rollback guard test remains green.

* fix(ecstore): resolve merge conflicts with origin/main in io_primitives.rs

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-07-08 08:24:50 +08:00
Zhengchao An 07324e268b fix(protocols): allow clippy::type_complexity in expiration worker tests (#4395)
fix(protocols): allow clippy type_complexity in test mock struct

The MockExpirationObjectBackend test struct uses a nested generic type
that triggers clippy::type_complexity. Add #[allow(clippy::type_complexity)]
since this is test-only code where the type is inherent to the mock design.

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-08 08:01:13 +08:00
houseme 3f13d098b4 feat(observability): feature-gated hotpath instrumentation for the data path (#4394)
Merge the hotpath-rs wall-time instrumentation from the backlog#936 analysis worktree behind an opt-in 'hotpath' cargo feature, keeping the default build at zero overhead and zero dependency.

- hotpath is an optional dependency everywhere (dep:hotpath feature syntax); the default dependency tree contains no hotpath crate at all
- 40+ measurement points across S3 handlers, ECStore/SetDisks object and multipart ops, erasure encode/decode, bitrot, LocalDisk I/O, FileMeta codec, and HashReader
- attribute sites use #[cfg_attr(feature = "hotpath", hotpath::measure)]; async_trait bodies use per-crate hp_guard! macros (ecstore + rustfs bin); rio gates measure_block! behind hp_measure_block!
- feature chain: rustfs -> rustfs-ecstore -> rustfs-rio / rustfs-filemeta, each crate owning its own gate
- hotpath-alloc is intentionally not wired up (hotpath 0.21.x TLS panic on cross-thread guard drop under tokio, see backlog#935); mimalloc stays the unconditional global allocator
- docs/development/hotpath-profiling.md documents building, HOTPATH_* env vars, SIGTERM report flow, and how to reproduce the backlog#936 timing reports

Refs: https://github.com/rustfs/backlog/issues/935 (HP-14, item 2), https://github.com/rustfs/backlog/issues/936

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 07:39:47 +08:00
houseme bd5d3c5d92 perf(ecstore): data-shards-only lockstep GET reads with stripe-aligned deferred parity engagement (opt-in) (#4392)
* feat(ecstore): add stripe-advance handles for deferred bitrot readers

Give DeferredObjectReader a shared pending state and expose a
DeferredReaderStripeHandle that advances the still-unopened source by whole
bitrot blocks using the same bitrot_encoded_range geometry the reader was
created with (identity mapping when hash_size == 0). This lets the GET decode
path open a parity shard aligned to the stripe where a data shard failed
instead of reading every parity shard on every stripe (backlog#923).

An already-opened (or failed) reader rejects the advance so callers retire it
rather than engage it out of alignment; bitrot verification after an advance
checks the advanced stripe's block against that stripe's stored hash.

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): read only data shards on healthy lockstep GET behind opt-in gate

PR #4289's lockstep fix made every reconstruction-verifying GET read all
data+parity shards per stripe; the parity blocks are read, bitrot-hashed and
then discarded, a deterministic 2x read-bytes/IOPS/hash-CPU amplification on
healthy 2+2 objects (backlog#923). With the new opt-in gate
RUSTFS_GET_LOCKSTEP_DATA_SHARDS_ONLY_ENABLE=true (default: false, behavior
identical to main):

- read_lockstep keeps only the data slots engaged while the object is
  healthy; parity slots stay unopened deferred readers.
- When a data shard is missing or dies at stripe k, parity readers are
  engaged mid-object by advancing their deferred stripe handle to stripe k,
  preserving the lockstep alignment invariant from backlog#832.
- Degraded stripes engage one parity beyond the decode quorum so
  reconstruction verification keeps an extra source to check against
  (erasure.rs only verifies when available > data shards); an engaged parity
  reader that errors is retired for the rest of the object like any other,
  and a parity reader that cannot be realigned is retired instead of being
  read out of position.
- fill_deferred_bitrot_readers records stripe handles for deferred slots and,
  gate-on only, swaps eagerly opened parity readers for unopened deferred
  ones so they remain engageable mid-object; ready/error bookkeeping used by
  quorum decisions is untouched.
- Both GET paths (legacy duplex via Erasure::decode_with_stripe_handles,
  codec streaming via ParallelReader::with_deferred_parity_handles) carry the
  handles from reader setup.

Short-read -> UnexpectedEof -> whole-object retirement and the
inconsistent-source rejection are unchanged in both gate modes; tests lock
the healthy-path data-shards-only call counts, the default read-all-shards
behavior, mid-object parity engagement for streaming and hash_size==0
formats, and mid-stream inconsistent-parity rejection.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 07:15:49 +08:00
Zhengchao An 65953bfdb3 fix(ecstore): reduce rename data version signatures (#4383) 2026-07-08 06:01:50 +08:00
Zhengchao An ddf197ba57 fix(lock): refresh object write locks with a heartbeat (#4388)
fix(lock): renew distributed locks via heartbeat; wire server refresh (backlog#899)

A held distributed write lock had a fixed 30s TTL and was never renewed, so
any operation exceeding it got its per-node lease reclaimed and stolen by a
contender, causing split-brain writes. This implements Phase 0 + Phase 1 of the
#899 design (Phase 2 abort-on-loss is deferred and tracked in code comments).

Phase 0 (server refresh wiring, P0):
- node_service handle_refresh was a no-op stub that parsed args then always
  returned success=true. Extract a testable `refresh_lock` free function that
  actually delegates to the node's lock backend. Not-found maps to
  success=false with no error_info, so RemoteClient::refresh keeps its
  Ok(resp.success) semantics and yields a real not_found signal to the
  coordinator heartbeat. Without this, client heartbeats were silently no-ops.

Phase 1 (client heartbeat + safe interval + observability):
- DistributedLockGuard spawns a heartbeat that refreshes every per-client lease
  on a derived interval; interval is derived without Duration::clamp
  (entries<=1 or interval>=ttl => no spawn), fixing the sub-second-ttl panic.
- Add LockLostSignal: declare the lock lost when not_found exceeds
  entries.len() - refresh_quorum; RPC errors are not counted (absorbed by the
  ttl > interval margin). Expose is_lock_lost()/lock_lost() for observers.
- disarm(), release(), and Drop now abort the heartbeat before releasing so no
  refresh races the unlock (refresh only extends, never creates, a lease).
- Reclaim path stays behaviorally unchanged but now warns with owner/resource/
  lease age and records a metric (#698 scavenger preserved).
- Add DEFAULT_LOCK_REFRESH_INTERVAL, LockRequest.refresh_interval (serde default
  for RPC back-compat) + builder, and lock lifecycle metrics.

Open questions adopt the design's documented defaults (marked TODO in code):
refresh not-found -> Ok(false)/error_info=None; lost-quorum base entries.len();
DEFAULT_LOCK_REFRESH_INTERVAL=10s.

Tests: heartbeat keepalive/quorum-loss/jitter/boundary/disarm (lock crate),
server refresh delegation (rustfs), and end-to-end survives-past-ttl plus
crashed-owner-reclaim regressions (namespace).
2026-07-08 06:01:45 +08:00
Zhengchao An 45435d83ab fix(keystone): accept Swift storage tokens (#4390)
fix(keystone): accept swift storage tokens
2026-07-08 06:01:40 +08:00
Zhengchao An f021e4f321 fix(protocols): simplify swift expiration test types (#4385) 2026-07-08 05:15:30 +08:00
houseme e7cc719c17 perf(ecstore): move speculative PUT-tail tmp cleanup off the hot path (#4389)
* perf(ecstore): move speculative tmp cleanup off the PUT hot path

On a successful PUT, rename_data has already moved the data dir out of the tmp workspace, so the delete_all(RUSTFS_META_TMP_BUCKET) at the end of SetDisks::put_object is a speculative no-op safety net. It was awaited inline on the response path, where profiling (backlog#924 / HP-3) showed the same-disk queueing behind fsync-heavy load turns a ~49us no-op into ~9ms average (p99 77ms, macOS F_FULLFSYNC amplified) added to every PUT.

Run that cleanup on a spawned task instead, keeping it as a real backstop (rename_data's remove_std only removes empty dirs and silently ignores failures). The failure path (quorum loss / rollback) keeps the cleanup inline so a failed PUT never returns with tmp shards still on disk. If the process dies before the spawned task runs, cleanup_stale_tmp_objects (24h expiry, 5-minute loop) reclaims the entry.

Scope note: ops/multipart.rs delete_all on RUSTFS_META_MULTIPART_BUCKET is intentionally untouched; it removes real leftovers and deferring it would widen CompleteMultipartUpload/Abort races.

Regression tests (hermetic SetDisks on formatted local disks, no global state): PUT success drains the tmp workspace (polling the spawned task), and PUT failure (missing bucket volume, rename_data quorum error after tmp shards were written) cleans the workspace inline before returning.

Ref: https://github.com/rustfs/backlog/issues/924

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): do not retry NotFound in reliable_rename_inner

reliable_rename_inner blindly retried the rename once on any error. A NotFound retry cannot succeed: nothing recreates the missing source or parent directory between attempts, so the second rename fails identically and speculative cleanup renames (e.g. move_to_trash on an already-removed tmp path) always paid for two syscalls.

Extract the retry decision into should_retry_rename: NotFound returns immediately, any other error keeps the existing single retry. This helper is shared by the rename_data commit path via rename_all, so behavior there is covered by a new rename_all success regression test alongside the retry-predicate tests.

Ref: https://github.com/rustfs/backlog/issues/924

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 04:01:37 +08:00
houseme 062a68d151 perf(ecstore): skip tmp parent dir fsync for write-then-rename paths (#4387)
Step 1 of 4 for rustfs/backlog#922 (HP-1): write_all_internal previously
coupled file-content durability (fdatasync) with directory-entry
durability (parent dir fsync) behind a single bool. For tmp files that
are immediately renamed away, the tmp parent dir fsync contributes
nothing to crash consistency: the safe-rename recipe only needs file
content fdatasync -> rename -> fsync of the destination parent, because
the rename removes the tmp directory entry and a crash before the
rename means the PUT was never acknowledged.

Replace the bool with a SyncMode enum (None / FileAndDir / FileOnly)
and use FileOnly at exactly the two write-then-rename tmp write points:
the tmp xl.meta write in the non-inline rename_data path and the tmp
write inside write_all_meta. write_all_public (format.json etc.) and
the old-metadata rollback backup keep FileAndDir since those files stay
where they are written. The rest of the commit sequence (shard
sync_dir_files, rename, destination parent fsync) is untouched, and
behavior with drive sync disabled is unchanged.

This saves one directory fsync per disk per non-inline PUT (4 on a
4-drive set). Unit tests assert, via a test-only fsync-dir recorder,
that tmp write points no longer fsync their parent while the public
write point, the rollback backup, and the commit-rename destination
parent still do.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 04:01:32 +08:00
houseme 9ae4ca5f99 test(ecstore): fit concurrent-resend guard inside lock acquire ceiling (#4384)
Under a full nextest run on loaded machines the legitimately serialized
cross-disk commits in concurrent_resend_same_part_commits_one_generation
exceed the acquire deadline by themselves: observed at the 5s default,
at the 30s production default (#4370), and on CI even at 60s — which is
a hard ceiling, because fast_lock clamps every requested timeout to
MAX_ACQUIRE_TIMEOUT (60s) while the Timeout error still reports the
requested value, so raising the env override higher is a no-op.

Request the full 60s ceiling explicitly and cap the queue depth at three
concurrent resends, bounding the last waiter to two serialized commits
(~12s each on the slowest observed CI runner). Three resends still race
the streaming phase and contend on the commit lock, which is all the
generation-mixing regression (backlog#853) needs; every correctness
assertion is unchanged.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 04:01:27 +08:00
houseme 86940a9452 fix(protocols): resolve clippy type_complexity in swift expiration worker tests (#4393)
fix(protocols): factor swift expiration mock result type into alias

The swift feature clippy matrix on main fails with clippy::type_complexity on the MockExpirationObjectBackend test struct introduced with the expiration worker tests, blocking CI for every open PR. Introduce a MetadataResult type alias in the test module; no behavior change.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-08 03:29:46 +08:00
Zhengchao An 7efacbdf95 fix(filemeta): validate part array lengths in into_fileinfo (#4382)
MetaObject::into_fileinfo indexed part_sizes[i]/part_actual_sizes[i] by
part_numbers.len() without checking the arrays are the same length,
unlike the adjacent part_etags/part_indices which are length-guarded.
decode_from pushes the three arrays independently and the xl.meta CRC
only covers bytes, so a CRC-valid but internally inconsistent xl.meta
(foreign writer / MinIO interop) triggers an out-of-bounds panic on the
GET/HEAD/LIST decode path.

Guard the three arrays for equal length and return Err(FileCorrupt) so a
divergent shard is skipped and quorum uses the other disks, instead of
panicking the request task. Cascade into_fileinfo to Result across its
callers, and fix io_primitives early-return to derive the version id from
the merged header and fall into the per-disk loop (single-disk survival +
heal). The 2118 merge-first path is left as a documented follow-up.

Refs backlog#900 (filemeta-01).
2026-07-08 02:01:28 +08:00
Zhengchao An a91d9cefc6 test(interop): add real MinIO read and migration parity tests (#4377)
test(interop): real-MinIO read + migration parity, Phase 1/2 (backlog#580)

Capture authentic on-disk fixtures from MinIO RELEASE.2025-07-23 (a bucket with
versioning, object-lock, lifecycle, tagging, quota, a public policy, SSE-S3
encryption, a webhook notification target, and a replication rule, plus inline /
versioned / multipart objects and a delete marker) and prove RustFS reads and
migrates them losslessly:

- filemeta parses_real_minio_object_xlmeta: small inline, two-object-version +
  delete marker, and multipart object xl.meta parse to the expected FileInfo.
- ecstore parses_real_minio_bucket_metadata_blob_without_loss: the MinIO
  .metadata.bin msgpack decodes via the PascalCase field names and
  parse_all_configs loads all ten config types present (policy, lifecycle incl.
  <ExpiryUpdatedAt>, object-lock, versioning, tagging, quota, notification,
  encryption/SSE-S3, replication incl. DeleteMarkerReplication /
  ExistingObjectReplication) without loss.
- ecstore reads_minio_inline_bucket_metadata_via_bitrot: MinIO inlines an object
  body as [HighwayHash256 32B][body]; RustFS's BitrotReader with HighwayHash256S
  verifies and yields the exact blob (the "inline_data 前缀不同" is that prefix).
- ecstore migrates_real_minio_bucket_metadata_end_to_end: on a throwaway 4-drive
  local ECStore, a real MinIO .metadata.bin seeded under .minio.sys is migrated
  into .rustfs.sys byte-identically for every config, exercising the Phase 2
  source adapter (MIGRATING_META_BUCKET = ".minio.sys") through the object layer.

All four run as ordinary crate tests (nextest CI). Phase 4 (MinIO re-reading a
RustFS drive) is documented as out of scope for one-way migration.

Refs rustfs/backlog#580
2026-07-08 01:12:49 +08:00
Zhengchao An 62a31e4ec4 fix(storage): address pending metadata and health gaps (#4380) 2026-07-08 00:15:07 +08:00
houseme d3660f9ded refactor(config): remove dead Direct I/O constants from zero_copy.rs (#4379)
ENV_OBJECT_DIRECT_IO_ENABLE, DEFAULT_OBJECT_DIRECT_IO_ENABLE,
ENV_OBJECT_DIRECT_IO_THRESHOLD and DEFAULT_OBJECT_DIRECT_IO_THRESHOLD
were never read anywhere in the workspace. The real O_DIRECT read path
landed in PR #4365 uses RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE /
RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD (crates/ecstore/src/disk/local.rs).
Keeping the near-identically named dead knobs invites operators to set
the wrong variable and believe O_DIRECT is enabled.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 23:54:56 +08:00
Zhengchao An 742a59884d test(ecstore): add validation suite coverage gates (#4378) 2026-07-07 23:50:12 +08:00
Zhengchao An 3ed414cdb4 fix(storage): complete pending metadata and quorum fixes (#4375) 2026-07-07 23:22:54 +08:00
houseme 5533e080b0 feat(ecstore): LocalIoBackend trait and O_DIRECT read with fallback (#4365)
* refactor(ecstore): extract LocalIoBackend trait behind LocalDisk

Model the LocalDisk per-file I/O hot path as a LocalIoBackend trait
(pread_bytes / open_read_stream / open_full_read / open_write) and move
the existing method bodies verbatim into a default StdBackend. LocalDisk
holds Arc<dyn LocalIoBackend> and the DiskAPI methods forward to it.

This is a behavior-preserving refactor: no logic, error-mapping, metrics,
or cfg-branch changes. It creates the seam for an alternative
runtime-probed io_uring backend (rustfs/backlog#894) without touching
DiskAPI callers. Commit-point durability (fdatasync -> rename ->
fsync-dir in rename_data) deliberately stays outside the trait.

Add a differential test asserting all four read shapes return identical
bytes across page-boundary and file-tail ranges.

Tracking: rustfs/backlog#891 (parent rustfs/backlog#897)

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(ecstore): true O_DIRECT read path with per-disk graceful fallback (#4366)

* feat(ecstore): true O_DIRECT read path with per-disk graceful fallback

Implement a real O_DIRECT positioned read inside StdBackend::pread_bytes
(Linux only) and wire up the previously dead RUSTFS_OBJECT_DIRECT_IO_READ_*
knobs, which had zero call sites.

Open with rustix OFlags::DIRECT, probe the DIO alignment once per disk via
statx STATX_DIOALIGN (4096 fallback), read the aligned superset into an
alignment-allocated bounce buffer with a short-read loop, and slice out the
exact logical range so padding never reaches BitrotReader.

Any O_DIRECT failure falls back to the buffered read methods; EINVAL or
EOPNOTSUPP (tmpfs, overlayfs, 9p) latches the path off per disk with one
warning. O_DIRECT errors never surface: EINVAL maps to FileNotFound in
to_file_error and would trigger spurious EC rebuilds.

Default behavior is unchanged (knob off). macOS keeps F_NOCACHE.

Tracking: rustfs/backlog#892 (parent rustfs/backlog#897)

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): add P1.5 benchmark gate harness for O_DIRECT reads (#4369)

* test(ecstore): add P1.5 benchmark gate harness for O_DIRECT reads

Add an ignored, Linux-only release-mode test (direct_read_bench_gate)
that measures DiskAPI::read_file_mmap_copy through a real LocalDisk with
cold-cache enforcement (fadvise DONTNEED between rounds), reporting
p50/p95/p99/mean latency, wall time, process CPU time, and throughput as
one JSON line for A/B diffing between the buffered baseline and the
O_DIRECT candidate selected by the production env knobs.

Content is verified byte-for-byte before any timing starts.

Tracking: rustfs/backlog#893 (parent rustfs/backlog#897)

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): add concurrency dimension to P1.5 bench harness

Model the EC GET shape (FuturesUnordered over concurrent shard reads)
via RUSTFS_BENCH_CONCURRENCY (default 1, sequential as before). Needed
to evaluate blocking-pool pressure for the io_uring gate decision.

Tracking: rustfs/backlog#893

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>

* ci: retrigger after cancelled required check

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): use is_multiple_of in O_DIRECT bench cache-drop check

clippy's manual_is_multiple_of (rust 1.96) fails -D warnings on the benchmark helper's `done % file_count == 0`.

Verification:
- cargo clippy -p rustfs-ecstore --all-targets

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 23:01:54 +08:00
houseme 8f4349793e fix(ecstore): log peer offline/online transitions for console reporting (#4367)
A node could show OFFLINE in the console with zero logs explaining why
(rustfs/backlog#888, reported in rustfs#4304): the consecutive-failure
threshold crossing in handle_server_info_failure marked the peer offline
silently, the per-call warnings only existed on the observing node and
never named the transition, and the PeerRestClient offline flag plus its
background recovery monitor ran without any start/success log.

Logging-only change, no behavior change:

- handle_server_info_failure / handle_storage_info_failure: WARN
  event="peer_marked_offline" exactly once at the threshold crossing
  (later failures while already offline stay DEBUG), and DEBUG
  event="peer_probe_failure" while returning cached state below the
  threshold.
- update_server_info_cache / update_storage_info_cache: INFO
  event="peer_recovered_online" when a probe succeeds after the peer had
  been reported offline.
- PeerRestClient::mark_offline_and_spawn_recovery: WARN
  event="peer_connection_marked_offline" when the offline flag is first
  set (guarded by the recovery_running CAS so repeated failures do not
  spam), and INFO event="peer_connection_recovered" with the attempt
  count when connectivity is restored.

An "offline then back" episode now leaves a complete, correlatable
trace: N probe failures -> peer_marked_offline -> recovery monitor ->
peer_recovered_online.

Verification:
- cargo test -p rustfs-ecstore --lib (notification + peer_rest suites)
- make pre-commit

Ref: rustfs/backlog#888, rustfs#4304

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 22:40:24 +08:00
Zhengchao An 9e6d7de0fa docs(agents): forbid hard-wrapping prose in PR/issue bodies (#4374)
GitHub renders single newlines inside a paragraph as line breaks, so hard-wrapped PR/issue/discussion prose shows up with ugly mid-sentence breaks. Add a rule under Git and PR Baseline to keep each paragraph on one line.
2026-07-07 22:16:07 +08:00
houseme 2dfa3d3c36 test(ecstore): restore 30s lock-timeout guard for concurrent resend test (#4370)
concurrent_resend_same_part_commits_one_generation keeps failing on CI
with Lock(Timeout ... 5s) even after the fast-lock lost-wakeup fix
(NOTIFY_WAIT_CAP re-polling) landed — observed on rustfs#4365's Test and
Lint runs. Two independent causes exist and both are real:

1. the lost/stolen wakeup stall — fixed in fast_lock::shard; and
2. the six legitimately serialized cross-disk commits exceeding the
   small 5s default acquire timeout under full-suite CI disk load.

The lost-wakeup fix reverted the earlier 30s test override on the
assumption that cause 1 was the whole story; CI shows cause 2 stands on
its own. Restore the temp_env override (production-default 30s) around
the concurrent section so the regression guard asserts the correctness
property (exactly one intact generation), not CI disk latency. The
NOTIFY_WAIT_CAP fix stays untouched.

Verification:
- cargo test -p rustfs-ecstore --lib concurrent_resend_same_part_commits_one_generation

Ref: rustfs/backlog#882

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 22:07:05 +08:00
houseme 7001373316 fix(iam): load IAM bootstrap snapshot without namespace locks (#4363)
* fix(iam): load IAM bootstrap snapshot without namespace locks

IAM bootstrap (init_iam_sys -> load_all) read every config object with
default ObjectOptions (no_lock=false), so each read acquired a
distributed namespace read lock. Lock quorum is counted over cluster
nodes and unreachable peers are hard failures, so during a sequential
restart the very first read failed with
"Quorum not reached: required 2, achieved 0" and IAM could not come up
until enough peers' lock RPC surfaces converged - even when the storage
read quorum was already satisfiable (rustfs#4304).

Extend the startup contract from rustfs#4056 ("startup metadata I/O must
not require namespace locks") to the IAM bootstrap path:

- Introduce LoadMode {Locked, BootstrapNoLock} and plumb it through the
  load_all chain (groups, users, policies, mapped policies and their
  concurrent variants) down to the storage read options.
- load_all now performs all reads with no_lock=true; on-line
  single-object loads via the Store trait keep locked semantics.
- Fail-closed behavior is unchanged: any loader error still aborts the
  whole snapshot load.

Safety: config objects are atomic whole-object writes, so a lock-free
read only observes an old or a new value; staleness is bounded by the
existing periodic IAM reload. Listing (walk) never took namespace locks,
and maybe_schedule_lazy_rewrite stays a best-effort background task.

Verification:
- cargo test -p rustfs-iam --lib (153 passed, incl. new LoadMode tests)
- cargo clippy -p rustfs-iam --all-targets
- cargo check -p rustfs
- make pre-commit

Ref: rustfs#4304; tracking rustfs/backlog#884, rustfs/backlog#885

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(iam): sequential-restart regression test for lock-free bootstrap

Add an integration test reproducing the rustfs#4304 failure mode against
a real 4-disk temp-dir ECStore:

- Seed IAM group data in single-node mode, then flip the runtime into
  distributed-erasure mode. new_ns_lock now builds a distributed lock
  over the set's (empty) lock-client list, so every namespace-locked
  read fails exactly like a sequential restart with unreachable peers
  (lock quorum unavailable, storage read quorum healthy).
- Assert the locked load_group path fails in that state, while the
  bulk snapshot load_all (no_lock plumbing from the previous commit)
  succeeds, and the data survives intact once single-node mode is
  restored.

Reverse-verified: temporarily switching load_all back to the locked
mode makes the test fail, so it genuinely guards the contract.

Verification:
- cargo test -p rustfs-iam --test iam_bootstrap_no_lock_test
- cargo test -p rustfs-iam --lib

Ref: rustfs#4304; tracking rustfs/backlog#886

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(iam): route test ECStore imports through ecstore_test_compat boundary

The new integration test imported rustfs_ecstore facade paths directly,
tripping three architecture migration rules. Move every ECStore import
behind crates/iam/tests/ecstore_test_compat/mod.rs (the sanctioned
test-compat pattern), and register that module as a reviewed test-only
global-facade boundary in check_architecture_migration_rules.sh: the
sequential-restart regression test needs api::global::update_erasure_type
to flip into distributed-erasure mode for lock-quorum fault injection.

Verification:
- ./scripts/check_architecture_migration_rules.sh
- cargo test -p rustfs-iam --test iam_bootstrap_no_lock_test
- make pre-commit

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(server): expose readiness blocking reason + rolling-restart runbook

Operators hitting the rustfs#4304 sequential cold start could not tell
from the outside why a node stayed unavailable. Three additions:

- The readiness gate's 503 now names the blocking dependency in both the
  body ("Service not ready: waiting for storage_quorum") and a new
  x-rustfs-readiness-pending header (storage_quorum | iam |
  startup_finalization), derived from the current startup stage.
  /health/ready already returned details + degradedReasons; this covers
  the plain S3 requests that hit the gate.
- IAM bootstrap retry logs now carry an actionable `hint` field that
  classifies the failure (storage read quorum vs lock quorum vs
  uninitialized metadata) instead of only echoing the storage error.
- New docs/operations/rolling-restart.md runbook: correct rolling
  restart procedure, sequential cold-start expectations (degraded ->
  auto-recovery), readiness signal reference, and
  RUSTFS_STARTUP_READINESS_MAX_WAIT_SECS guidance.

Verification:
- cargo test -p rustfs --lib -- hint_tests service_not_ready readiness_pending
- make pre-commit

Ref: rustfs#4304; tracking rustfs/backlog#887

Co-Authored-By: heihutu <heihutu@gmail.com>

* upgrade deps version and improve import

* feat(iam): notification-path cache refreshes read without namespace locks (#4368)

P3 step 1 of rustfs/backlog#884 (scoped down from full MinIO readConfig
alignment after review): cross-node notification handlers
(group/policy/policy-mapping/user) refresh the local IAM cache with
single-object reads that previously took distributed namespace read
locks. These refreshes are asynchronous, best-effort, and already
stale-tolerant (the periodic reload converges them), so a node-counted
lock quorum failure or lock RPC hiccup on a peer must not fail them —
the same rationale as the lock-free bootstrap load_all (rustfs#4304).

- Store trait: add load_user_no_lock / load_group_no_lock /
  load_policy_doc_no_lock / load_mapped_policy_no_lock with defaults
  forwarding to the locked variants, so existing implementations and
  test mocks keep their behavior.
- ObjectStore overrides them via the existing LoadMode::BootstrapNoLock
  plumbing. Deletions triggered by the handlers keep locked writes.
- manager.rs: the four *_notification_handler paths (8 call sites)
  switch to the lock-free variants.
- Integration test: while the lock quorum is unavailable (DistErasure
  with empty lockers), load_group_no_lock must succeed exactly where
  the locked load_group fails.

Request-path loads (check_key, verify_temp_user_persistence) and admin
write-then-reload paths intentionally stay locked: load_user_identity
embeds expiry deletions, so those need the side-effect extraction
tracked in rustfs/backlog#884 before going lock-free.

Verification:
- cargo test -p rustfs-iam --lib (156 passed)
- cargo test -p rustfs-iam --test iam_bootstrap_no_lock_test
- make pre-commit

Ref: rustfs/backlog#884, rustfs#4304

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(server): drop unused iam_bootstrap_failure_hint import in tests

The hint tests live in their own hint_tests module with a local import;
the stale re-import in mod tests failed clippy's -D warnings on the
Test and Lint CI variants.

Verification:
- cargo clippy -p rustfs --all-targets

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 20:07:09 +08:00
houseme 2247823200 fix(runtime): remove tokio io-uring feature and add regression guard (#4364)
Drop the [target.'cfg(target_os = "linux")'.dependencies] tokio
"io-uring" feature from 6 crates and the rustfs binary. Because
.cargo/config.toml enables --cfg tokio_unstable globally, this feature
switched every Linux build's file I/O onto tokio's io_uring runtime
backend. Restricted Linux environments (Docker default seccomp, gVisor,
proot, old kernels) reject io_uring_setup with EACCES/ENOSYS, which
tokio surfaced as PermissionDenied and RustFS reported as
DiskAccessDenied at startup.

Add scripts/check_no_tokio_io_uring.sh so the feature cannot silently
return: it fails on any tokio dependency line enabling "io-uring", while
still allowing a future application-level io-uring crate dependency.
Wire it into make pre-commit/pre-pr/dev-check and CI.

Tracking: rustfs/backlog#890 (parent rustfs/backlog#897)

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 17:47:59 +08:00
Henry Guo 0e61ba7c63 test(table-catalog): add scale fault rehearsal (#4359)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-07-07 16:37:58 +08:00
houseme 717cdd2abd fix(migration): decrypt MinIO IAM & server config on drop-in migration (#4358)
* fix(migration): decrypt MinIO IAM & server config on drop-in migration

MinIO encrypts IAM identity/service-account files and the server config at
rest with a key derived from the root credentials. The drop-in migration
paths read those blobs from the legacy `.minio.sys` bucket and parsed them
as plaintext JSON, so any encrypted blob failed to parse and was silently
skipped with "incompatible format". This is why users migrating from MinIO
kept their buckets/objects/policies but lost users and access keys (#2212).

The IAM load path already knows how to decrypt these blobs (RustFS master
keys plus MinIO-compatible legacy keys derived from the root credentials),
but that logic lived behind a private method and was never used by the
migration paths. Expose it as `rustfs_iam::try_decrypt_iam_blob` and inject
it into both migration paths via a `LegacyBlobDecryptFn` callback (ecstore
cannot depend on the IAM crate, so the closure is wired in the binary crate).
When a blob cannot be decrypted the raw bytes are used as-is, preserving the
previous plaintext-only behavior with no regression.

Also improve object-layer migration observability without changing control
flow: `try_migrate_format` now distinguishes "no legacy format" (a normal
fresh install) from "legacy format present but incompatible", and the caller
logs a loud error before initializing a fresh format that would leave the
existing MinIO objects unreadable. Topology/version skip reasons are promoted
from debug to warn.

Fixes a pre-existing test isolation race by marking
`test_recovery_falls_back_to_default_config_when_blob_stays_corrupt` serial,
since it reads a process-wide env var toggled by a sibling test.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(migration): box FormatV3 in LegacyFormatOutcome to satisfy clippy

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): stabilize concurrent multipart resend lock timeout

concurrent_resend_same_part_commits_one_generation spawns 6 same-part
resends whose cross-disk commits serialize on the per-uploadId commit
lock. Under the full nextest suite the parallel disk load pushes those
serialized commits past the small default lock-acquire timeout (5s),
producing a spurious `Lock(Timeout ...)` unrelated to the property under
test (observed on CI at 5.775s vs ~0.5s in isolation).

Raise RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT to the production default (30s)
for the concurrent-commit section via temp_env, so the regression guard
reflects correctness (exactly one intact generation) rather than disk
latency under CI load. The meaningful assertions are unchanged, and
#[serial] keeps the process-wide env override isolated.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(lock): bound fast-lock notification wait to prevent lost-wakeup stall

The real cause of the concurrent_resend_same_part_commits_one_generation
failures was a lost wakeup in the fast-lock slow path, not disk latency:
raising the acquire timeout to 30s only delayed the failure (it then timed
out at 30s), proving a genuine stall rather than overload.

In acquire_lock_slow_path a waiter that reaches the notification phase did a
single `timeout(remaining, wait_for_write())` spanning the whole acquire
budget, and treated that wait's elapse as a hard `Timeout`. But the release
path only notifies when `writer_waiters > 0`, so if the holder releases in
the gap after the waiter's `try_acquire` fails and before it registers as a
waiter, no notification (and no stored permit, since the pooled `Notify` is
gated) is produced. The waiter then blocks until the deadline even though the
lock is free and stays free — a spurious lock-acquire timeout. The shared
process-wide notify pool makes it worse: a wakeup can be consumed by a waiter
of a different lock hashing to the same slot.

Bound each notification wait (NOTIFY_WAIT_CAP = 50ms) and, on elapse, loop
back and re-`try_acquire` instead of returning `Timeout`; the deadline check
at the top of the loop is the single source of truth for timing out. A
lost/stolen wakeup now degrades to bounded re-polling (acquire within ~50ms
of the lock becoming free) instead of stalling for the whole timeout.
Correctness (mutual exclusion) is unchanged — acquisition still only happens
via `try_acquire_*`.

Add a regression test that reproduces the stall (holder + late waiter across
many keys): it times out without the fix and passes in ~1s with it. Revert
the earlier acquire-timeout workaround in the multipart test now that the
underlying stall is fixed, so it runs under the default timeout again.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 16:36:05 +08:00
majinghe 796fbb47da ci: replace docker image build runner with aks dind container (#4361) 2026-07-07 16:01:19 +08:00
houseme 88645c9169 fix(server): stop closing idle proxy keep-alive connections + reverse-proxy hardening (#3076) (#4360)
* fix(server): raise default HTTP/1.1 idle keep-alive timeout for proxies

In hyper's HTTP/1.1 stack `header_read_timeout` is armed as soon as the
connection is ready to read the next request head, so on a keep-alive
connection it also bounds how long an idle upstream connection may sit
before RustFS closes it. The previous 5s default meant RustFS FIN'd pooled
upstream connections while reverse proxies (Caddy ~2min, Nginx 60s) still
believed they were alive. The proxy then reused a dead socket and clients
saw `socket hang up` / connection reset, most visibly on non-idempotent
`PUT` uploads that proxies will not transparently retry (issue #3076).

Raise DEFAULT_HTTP1_HEADER_READ_TIMEOUT to 75s (above common proxy
idle-keepalive windows) and document the coupling. Still overridable via
RUSTFS_HTTP1_HEADER_READ_TIMEOUT for deployments that expose RustFS
directly to untrusted slow clients and want tighter slowloris protection.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(operations): add reverse-proxy deployment guide

Document the request/body semantics RustFS expects from a proxy layer, the
idle keep-alive mismatch that causes `socket hang up` on large PutObject
writes, and known-good Caddy/Nginx configs plus Cloudflare caveats. Closes
the documentation gap called out in issue #3076 and consolidates the
scattered findings from #609/#934/#1492/#1766.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(object): bound stalled PutObject request-body reads with diagnostics

When a reverse proxy or CDN forwards a partial request body and then goes
silent without closing the connection, the inner body stream neither yields
more bytes nor reports EOF, so RustFS waited forever for bytes that never
arrive and the client saw a silent hang/abort (issue #3076). A short body
that ends with a proper EOF was already rejected promptly; the gap was the
no-EOF stall case.

Add a single-point request-body guard on the PutObject path that wraps the
incoming StreamingBlob with an inter-chunk read timeout. The timer resets on
every chunk, so slow-but-progressing uploads are unaffected; it only fires
after RUSTFS_HTTP_REQUEST_BODY_READ_TIMEOUT (default 300s, 0 disables) of
complete silence. On timeout it logs a structured `put_object_body_read_stalled`
event with received/expected byte counts and fails the read with
ErrorKind::TimedOut instead of hanging. remaining_length/size_hint are
forwarded so wrapping is transparent to downstream length handling.

Covered by unit tests for the stall path, length-preserving pass-through, and
the disabled (timeout=0) pass-through.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 15:25:31 +08:00
houseme f73880f354 fix(startup): survive slow multi-node cold starts (#4357)
* fix(server): make startup readiness wait configurable and raise default (#4264)

The startup runtime-readiness wait was a hardcoded 30s constant with no env
override. On slow multi-node cold starts (Docker/K8s/Synology NAS) this window
is shorter than the internal startup budgets it depends on — the endpoint
DNS-retry window (~90s) and the format-load retry loop (~100s worst case) — so
readiness times out and the node exits with
`startup readiness timed out after 30s: storage_ready=false, lock_quorum_ready=false`
before storage/lock quorum can converge, feeding the restart storm reported in
the issue.

- Add `RUSTFS_STARTUP_READINESS_MAX_WAIT_SECS` (default 120s), documented in
  rustfs-config health constants.
- Resolve the wait at runtime via `startup_runtime_readiness_max_wait()`; a
  value of `0` falls back to the default instead of timing out instantly.
- Repoint `STARTUP_RUNTIME_READINESS_MAX_WAIT` at the shared config default so
  there is a single source of truth, and cover the getter with unit tests.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): stop peer/disk background monitors on graceful shutdown (#4264)

Long-lived peer health/recovery and remote-disk monitors are detached
`tokio::spawn` tasks that each hold a `tracing::Span` via `.instrument(..)` for
their whole lifetime. Nothing cancelled them at shutdown, so on the normal
return path the Tokio runtime was dropped while they were still alive and their
`Span`s were dropped during worker-thread thread-local-storage (TLS)
destruction. At that point `tracing-subscriber`'s fmt `on_close` can touch an
already-destroyed TLS slot and panic with
`cannot access a Thread Local Storage value during or after destruction`, which
escalates to a panic-during-panic abort (SIGILL / exit 132) — the crash
reported on Synology in issue #4264, amplified by the restart storm.

- Add `cluster::rpc::background_monitor` with a process-global shutdown token,
  `spawn_background_monitor()` (races the monitor future against that token so
  its span drops while the runtime is alive), and public
  `shutdown_background_monitors()`.
- Route every span-holding peer_s3 / peer_rest / remote_disk monitor spawn
  through `spawn_background_monitor` instead of `tokio::spawn(..).instrument()`.
- Expose `rustfs_ecstore::shutdown_background_monitors()` and call it from the
  graceful shutdown sequence (right after `ctx.cancel()`, before runtime
  teardown) via the `storage_api` compatibility boundary.

Existing recovery-probe span-context tests still pass, confirming log
correlation is preserved.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 14:02:00 +08:00
houseme 9ae233d552 test(e2e): add SHA256 write and HEAD checksum regressions (#4354)
test(e2e): add SHA256 verify-on-write & HEAD-checksum regression for #4341

Issue #4341 reported (on beta.8) that RustFS accepted mismatched S3
SHA256 checksums on PutObject (HTTP 200 instead of 400 BadDigest) and
did not return ChecksumSHA256 on HeadObject with ChecksumMode=ENABLED.

Both behaviors are already correct on current main, but the existing
e2e coverage only asserted the happy path (upload succeeds + GetObject
content matches). It did not encode the issue's two acceptance criteria,
so a future regression would go undetected.

Add two focused e2e tests that assert exactly the issue's contract:
- test_put_object_rejects_mismatched_sha256: a body that does not match
  the declared x-amz-checksum-sha256 must be rejected with BadDigest and
  must not be stored.
- test_head_object_returns_stored_sha256: after a correct upload,
  HeadObject with ChecksumMode=ENABLED must return the stored base64
  SHA256 digest.

Both tests pass against a real rustfs server (mismatch -> HTTP 400
BadDigest verified).

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 14:01:32 +08:00
houseme 58114f49f2 perf(ecstore): k-way heap merge for ListObjects, drop clone-to-parse (#4347)
* perf(ecstore): replace linear merge scan with k-way heap and drop clone-to-parse (backlog#874 backlog#875)

merge_entry_channels advanced the k-way merge with a linear scan over all
channel heads (O(entries x channels)) and allocated two fresh Strings per
pairwise comparison via path::clean. Every step also cloned MetaCacheEntry
values, including entry.clone().xl_meta() clone-to-parse calls.

- Introduce MergeHead with a cached cleaned name (allocated only when the
  raw name is not already clean) and drive the merge with a BinaryHeap of
  boxed heads: O(log channels) per entry, allocation-free comparisons.
- Move entries through the merge instead of cloning; the winner is sent
  without an intermediate copy.
- Remove the dead merge_file_meta_versions block: it only ran for
  prefix-dir groups whose entries have empty metadata, so xl_meta() always
  failed; cross-drive version merging happens in the resolve path.
- Keep legacy same-name semantics (dir groups collapse, objects shadow
  prefix dirs, later object candidate wins) and add regression tests for
  interleaved ordering, dir/object precedence, uncleaned-name grouping,
  and prefix-dir collapse.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): honor ascending versions_sort in ListObjects walk (#4348)

* fix(ecstore): honor ascending versions_sort in walk and document ordering invariant (backlog#876)

The walk loop carried a bare `//TODO: SORT` inside the
`WalkVersionsSortOrder::Ascending` branch, so the requested ascending
order was silently ignored and versions streamed newest-first (the raw
FileMeta order). WalkOptions defaults to Ascending, so every default
walker -- notably replication resync, which replays versions and needs
oldest-first to preserve the version-stack order -- received the exact
opposite of the contract.

FileMeta maintains versions newest-first (sort_by_mod_time is
descending) and into_file_info_versions preserves that order, so
ascending emission is the exact reverse of file_info_versions output.
Reverse in place when ascending is requested and add a regression test
locking the newest-first invariant plus the reversal contract.

Key-ordering audit result (no gap found): per-disk walkers emit sorted
streams, merge_entry_channels performs an ordered k-way merge, and
gather_results only filters by marker/limit, so ListObjects key order is
guaranteed upstream and needs no post-sort.

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): enable GET metadata early-stop by default (#4349)

* perf(ecstore): enable GET metadata early-stop by default with env opt-out (backlog#872)

The metadata early-stop fanout (read_all_fileinfo_early_stop) has been
implemented and instrumented for a while but stayed behind an opt-in
flag, so default GETs always waited for every disk to answer the
metadata read even after quorum agreement was reached.

Flip RUSTFS_GET_METADATA_EARLY_STOP_ENABLE to default-on. The gate stays
conservative: should_allow_metadata_early_stop only admits metadata-only
reads (read_data=false) without version_id, healing, or free-version
requirements, everything else falls back to the full-wait fanout, and
setting the env var to false restores the old behavior entirely. The
version-aware gate (RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE)
remains opt-in because versioned reads carry a higher stale-selection
risk profile.

Also replace the stale "optimize concurrency" TODO in
get_object_fileinfo with a pointer to the early-stop implementation and
add regression tests for the new default plus the explicit opt-out path.

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): lazily construct codec streaming multipart readers (#4350)

* perf(ecstore): lazily construct codec streaming multipart part readers (backlog#871)

get_object_decode_reader_with_fileinfo opened shard readers for every
part of a multipart object before returning the streaming reader, so
TTFB paid for parts x disks file opens up front and an early client
disconnect wasted the setup work for every unread part.

Replace the eager loop with LazyMultipartCodecStreamingReader: the first
part is still built eagerly so the dominant fallback conditions (missing
shards / read quorum) are detected before any byte is streamed and the
whole request can fall back to the legacy duplex path exactly as before.
Each subsequent part is built on demand -- when the previous part hits
EOF -- via a spawned task handle owned by the reader; dropping the
reader aborts an in-flight build so disconnects stop all further IO.

If a later part hits a fallback condition mid-stream (a shard vanished
after the request started), the reader surfaces an explicit read error
with a pipeline-failure metric instead of silently degrading; the
client's retry then detects the condition on the eager first-part setup
and takes the legacy path cleanly.

Adds unit tests for in-order streaming across lazy boundaries, deferred
construction (no build when the client stops within part 1), and the
mid-stream fallback error path.

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): prefetch next multipart part reader setup during decode (#4351)

* perf(ecstore): prefetch next multipart part reader setup during decode (backlog#870)

get_object_with_fileinfo processed multipart parts strictly serially:
the next part's bitrot reader setup (file opens + read-quorum wait
across all disks) only started after the current part finished
decoding, so large multipart reads paid full setup latency between
every part.

Overlap the two stages with a depth-one pipeline: right after the
current part's readers are obtained, the next part's setup is spawned
(shared inputs behind Arc) and joined when the loop reaches that part.
The shared setup_multipart_part_readers helper keeps stage-duration
metrics semantics identical for both paths; a failed or stale prefetch
falls back to the synchronous setup, and the PrefetchedReaderSetup
guard aborts the in-flight task on error returns, early breaks, or
caller drop so disconnects stop background disk IO.

Gate: RUSTFS_GET_MULTIPART_READER_SETUP_PREFETCH (default on, env
opt-out). Adds a three-part end-to-end read test covering the prefetch
hit path and cross-part content ordering.

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): move FileInfo through GET shuffle instead of cloning (#4352)

perf(ecstore): move FileInfo entries through the GET shuffle instead of cloning (backlog#873)

shuffle_disks_and_parts_metadata_by_index deep-cloned every valid
FileInfo (parts, erasure info, metadata map) once per disk on each GET.
Add an ownership-taking variant that runs the same by-index consistency
check as a read-only first pass and then moves entries into their
shuffled slots with mem::take, and switch get_object_with_fileinfo to
it -- that call site already owned the parts metadata vector. Disk
handles are Arc clones and stay cheap.

Scope notes from the backlog#873 audit:
- get_object_fileinfo's disks.clone() stays: DiskStore is Arc<Disk>, so
  the clone is per-slot refcounting and correctly avoids holding the
  RwLock read guard across the metadata fanout awaits.
- get_object_decode_reader_with_fileinfo keeps the borrowing shuffle:
  its caller must retain files/disks for the legacy fallback path, so an
  owned variant would just shift the same clone upstream.
- The metadata-cache hit path still clones parts_metadata; sharing the
  cached entry via Arc changes the read-path return types and is left
  as a follow-up.

Equivalence tests cover both the by-index placement and the mod-time
fallback against the borrowing variant.

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>

* fix(ecstore): gate merge emission on cleaned key and clear clippy redundant_clone

Address review + CI findings on the ListObjects/GET optimization PR:

- merge_entry_channels gated emission on the raw entry name while the heap
  orders by the cleaned key, so entries whose cleaned order and raw byte order
  disagree (e.g. redundant slashes) could be dropped. Gate on the same cleaned
  sort key the heap uses; add a regression test (`a//c` after `a/b`).
- Drop three redundant `.clone()` calls in test code flagged by
  clippy::redundant_clone (owned-shuffle equivalence tests and the walk
  ascending-versions contract test) that failed the CI clippy gate.
- Document the known mid-stream fallback limitation of the opt-in multipart
  codec streaming reader (default off) and mark the in-place per-part legacy
  degradation as a follow-up.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(ecstore): force full metadata fanout for object tagging writes (backlog#872)

put_object_tags reads the object fileinfo with read_data=false and then
writes the updated tags to the online-disk set that read returned. With
metadata early-stop enabled by default, that read now returns as soon as
read quorum is reached, so the online-disk set is only a read-quorum
subset. Writing tags to that subset fails write quorum -> ErasureWriteQuorum
-> S3 SlowDown, which is exactly the s3-tests tagging failures
(PutObjectTagging/DeleteObjectTagging, reached max retries).

Thread a caller-controlled `allow_early_stop` gate through
read_all_fileinfo_observed/_inner and add get_object_fileinfo_gated;
put_object_tags calls it with allow_early_stop=false so the metadata read
does the full quorum fanout and returns the complete online-disk set as
the write target. Pure-read callers (GET/HEAD/tag read) keep the
early-stop fast path unchanged.

Extract metadata_early_stop_permitted() as the single gate and add a unit
test locking the invariant: caller opt-out (and observe=false, and data
reads) never early-stop even with the env flags on.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 14:01:09 +08:00
houseme c9b976ad46 feat(ecstore): reclaim orphaned data dirs during heal (#4356)
* feat(ecstore): reclaim orphaned data dirs during heal (backlog #3231/#3191)

Pre-#3510, an unversioned overwrite leaked one UUID-named data dir per PUT.
The write path now cleans up going forward, but pre-existing strays stay on
disk forever: heal's dangling logic only removes whole objects whose data is
missing, never surplus data dirs of an otherwise-healthy object. That leaked
space is what eventually made ListObjects scan tens of GB and time out.

Add SetDisks::reclaim_orphan_data_dirs, a fail-closed, quorum-safe sweep that
mirrors purge_orphan_dir_object:

- referenced set is the UNION of get_data_dirs() across every online replica's
  xl.meta, so a dir named by any replica is kept;
- if a disk holds the object dir but its xl.meta is missing or unparseable the
  object is treated as degraded and nothing is removed;
- only UUID-named subdirectories are ever considered.

Wire it into heal_object's healthy tail (under the object write lock) as a
best-effort step so the background heal scanner and admin heal recover the
leaked space automatically; a reclaim failure never fails the heal.

Covered by four unit tests: removal of an unreferenced dir, no-op when all dirs
are referenced, fail-closed abort on missing metadata, and cross-replica union
preservation.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(ecstore): fix typo unparseable -> unparsable

Satisfies the repository typos CI check.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 13:06:22 +08:00
Zhengchao An 0ce0388fc0 fix(ecstore): fsync inline rename_data rollback backup (#4346)
* fix(ecstore): fsync inline rename_data rollback backup

The inline branch of LocalDisk::rename_data writes the previous version's
xl.meta rollback backup (<old_data_dir>/xl.meta.bkp) with a bare
std::fs::write, fsyncing neither the file nor its new directory entry, even
when drive_sync_enabled() is true. The same closure already syncs the new
xl.meta and the commit rename directory, and the non-inline branch persists
its backup durably.

That backup is the sole restore source for delete_version(undo_write=true)
on a set-level write-quorum failure. With sync enabled, an inline overwrite
plus power loss plus a quorum failure that triggers undo_write could restore
a lost or truncated backup and fail to roll back to the prior committed
object.

Write the inline backup via OpenOptions + write_all and, when sync is
enabled, sync_data() it and fsync_dir_std its parent, mirroring the
non-inline branch. Extend the inline backup test to assert the .bkp contents
equal the previous metadata bytes.

Refs backlog#868 (disk-durability-01).

* fix(ecstore): preserve to_file_error mapping on inline backup write

Address review: the rewritten inline rollback-backup write must keep the
same DiskError classification as the previous std::fs::write(...).map_err(
to_file_error)? call. Without it, io errors (PermissionDenied/NotFound/
StorageFull/...) would surface as DiskError::Io(_) instead of the specific
DiskError variants (FileAccessDenied/FileNotFound/DiskFull/...), since
From<io::Error> for DiskError only recovers variants that were wrapped via
to_file_error. Map open/write_all/sync_data/fsync_dir_std through
to_file_error, matching write_all_internal.

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-07 11:47:43 +08:00
yihong e4ecb1bce5 fix: ci failed (#4353) 2026-07-07 10:38:41 +08:00
RustFS 85ba51ce72 fix(ci): use direct hash comparison for sha256 verification on Windows (#4345) 2026-07-07 10:17:40 +08:00
Zhengchao An 073bc96756 fix(filemeta): compute header signature instead of hardcoding zero (#4343)
The xl.meta version header `signature` was hardcoded to `[0,0,0,0]` on the
write path (`From<FileMetaVersion> for FileMetaVersionHeader`), and the
existing `get_signature` implementations only hashed `version_id` + `mod_time`
(+ `size`), so two versions that share a version_id and mod_time but differ in
body (e.g. a PutObjectTagging that reached only some disks) produced identical
headers. Such divergence was undetectable and unhealable in the strict merge
path (backlog#861, B12).

Compute a real signature over the full version body, mirroring MinIO's
`xlMetaV2Version.getSignature` semantics:

- Fill the signature on the write path via the `From` impl (covers
  add_version / set_idx / update_object_version).
- `MetaObject::get_signature` / `MetaDeleteMarker::get_signature` now clone the
  body, zero the per-disk `erasure_index`, fold `meta_user` / `meta_sys` in
  with order-independent hashes (msgpack map order is not stable across disks),
  marshal the rest and xxh64 it, then fold 64->32 bits. Empty vs all-empty
  PartETags are normalized alike.
- Invalid/bodyless versions return an `err` sentinel rather than all-zero, so
  they never collide with a real all-zero legacy signature.

Backward compatibility: the decode path preserves stored signature bytes, and
the normal (non-strict) merge path already zeroes signatures before grouping,
so existing all-zero xl.meta stays consistent across disks. Only strict
merge/heal compares signatures, where legacy data is uniformly zero (no split)
and genuine partial-write divergence is now correctly detected.

Adds unit tests covering: non-zero signature on write, erasure_index
independence, tag/metadata/size divergence detection, map-order independence,
PartETags normalization, delete-marker divergence, and the err sentinel.
2026-07-07 09:40:28 +08:00
Zhengchao An 5594b18912 fix(ecstore): make delete_volume non-recursive by default to prevent bucket-heal wipe (backlog#799 B1) (#4339)
* fix(ecstore): make delete_volume non-recursive by default to prevent bucket-heal wipe (backlog#799 B1)

`delete_volume` unconditionally `remove_dir_all`'d the whole bucket tree, and the
bucket-heal "remove" branch called it fire-and-forget on every local disk. A
mis-classified "dangling" bucket (or a non-force S3 DeleteBucket on a populated
bucket) was therefore recursively wiped — a potential whole-bucket data loss.
The `VolumeNotEmpty` -> recreate/`BucketNotEmpty` handling already present in
both delete_bucket paths was dead code because the primitive never refused.

Add an explicit `force_delete` flag to `DiskAPI::delete_volume` and default the
non-force path to a non-recursive `remove_dir` (rmdir), which fails atomically
with `VolumeNotEmpty` if the bucket still holds any object data. Only an explicit
force delete (S3 force bucket delete) removes recursively. Mirrors MinIO's
`xlStorage.DeleteVol` (`Remove` vs `RemoveAll`).

- Trait + all impls (local behavior, dispatch, disk_store, remote RPC) take the
  flag; the gRPC `DeleteVolumeRequest` gains a `force` field (proto3 default
  false → old peers get the safe non-recursive behavior on rolling upgrade).
- Heal remove branch passes `false` and no longer discards the result: a
  `VolumeNotEmpty` refusal is logged (the bucket is not dangling) instead of
  wiping data.
- Both `delete_bucket` paths pass `opts.force`, activating the previously-dead
  `VolumeNotEmpty` -> `BucketNotEmpty`/recreate handling (correct S3 semantics).

Adds a regression test: non-force delete of a non-empty bucket returns
VolumeNotEmpty and preserves the data; force delete removes it.

Design converged by two independent expert reviews (MinIO-fidelity +
defense-in-depth) referencing MinIO xl-storage.go. Refs backlog#799 (B1),
issue rustfs/backlog#850. The safety expert's deeper hardening (typed
capability instead of a bool, trash-instead-of-in-place for force, quorum
re-verification of dangling) is noted on #850 as follow-up.

* fix(ecstore): reword 'mis-classified' -> 'misclassified' to satisfy typos (backlog#799 B1)

* fix(rustfs): thread force_delete through StorageDiskRpcExt::delete_volume + test literal (backlog#799 B1)
2026-07-07 08:25:17 +08:00
Zhengchao An 28c0543f3c fix(filemeta): resolve core-storage audit P2 items B15/B16/B18 (backlog#863) (#4344)
fix(filemeta): resolve P2 core-storage audit items B15/B16/B18 (backlog#863)

Fixes the three remaining actionable items from the core-storage reliability
audit (rustfs/backlog#863). All three are metadata-layer correctness defects in
the filemeta crate.

B15 — version sort tie-break consistency
  `FileMeta::sort_by_mod_time` and the delete path used a hand-rolled comparator
  whose version_type tie-break for equal mod_time was the OPPOSITE of the
  canonical `FileMetaVersionHeader::sorts_before` (used by the metacache merge
  and latest-version selection). At equal mod_time it sorted a delete marker
  before its object, so the per-disk read path could treat an object as deleted
  while the quorum-merge path treated it as present. Both sort sites now derive
  their ordering from `sorts_before`, matching MinIO (object-first).

B16 — replication reset state persistence
  The three sites that flush `reset_statuses_map` into `meta_sys` inserted each
  key verbatim. A bare-ARN key (produced by `ObjectInfo::replication_state`) has
  no internal prefix, so read-back — which only recognizes prefixed keys —
  silently dropped the reset state; a rustfs-only key was invisible to
  MinIO-compatible readers. New `persist_reset_statuses` normalizes every entry
  to the canonical `replication-reset-<arn>` suffix and writes both the
  `x-rustfs-internal-*` and `x-minio-internal-*` prefixes.

B18 — Legacy (V1Obj) body round trip
  `FileMetaVersion::encode_to` never emitted the `V1Obj` field, so re-encoding a
  Legacy version silently dropped its entire body. Adds symmetric `encode_to`
  for `MetaObjectV1`/Stat/Erasure/ChecksumInfo/Part and a `write_msgp_time`
  helper (ext8/type-5/12-byte, matching the decoder). `Mode` uses the strict
  `write_u32` marker the decoder requires, and `Stat.ModTime` is written only
  when present so a `None` never round-trips to `Some(UNIX_EPOCH)`.

Adds regression tests for each fix (138 filemeta tests pass). Verified with an
adversarial multi-expert review that could not break any of the three.

Refs rustfs/backlog#863 (B15, B16, B18).
2026-07-07 08:25:12 +08:00
Zhengchao An b813fc7739 fix(ecstore): reject zero erasure block_size in codec streaming read (#4340)
The codec streaming GET reader divides by erasure.block_size in
build_codec_streaming_part_reader without validating the erasure
dimensions, unlike the legacy multipart path which already rejects
block_size==0 / data_shards==0. FileInfo::is_valid() does not check
block_size, so corrupted on-disk metadata (block_size==0, data_blocks>0)
passes validation and panics the read task with a divide-by-zero.

Add Erasure::has_valid_dimensions() and reject invalid dimensions at the
codec streaming entry before any disk access, mirroring the legacy guard
(which now reuses the same predicate).

Refs backlog#868 (868-1).
2026-07-07 07:17:58 +08:00
Zhengchao An 8b2c67a100 ci: cancel superseded main release builds (#4342) 2026-07-07 05:47:06 +08:00
houseme 6f613317f6 feat(internode): optimize gRPC transport (#4337)
* feat(internode): P0 gRPC transport tuning, message limits, payload metrics

Land the P0 subtask from docs/grpc-optimization: close the client-vs-server
transport gaps and add instrumentation to size which unary RPCs need channel
isolation in P1.

Transport tuning (G3): the client `Endpoint` now disables Nagle and raises the
HTTP/2 stream/connection flow-control windows to mirror the server socket, so
small lock/health RPCs are not batched and larger metadata responses are not
throttled by the 64KiB default window. All env-overridable, 0 opts out.

Message-size limits (G1): both `NodeServiceClient` and `NodeServiceServer` set
max decode/encode size (default 100MiB) instead of tonic's silent 4MiB cap, so a
large multi-version xl.meta or aggregated ReadMultiple no longer fails
out_of_range. The server limit is set on `NodeServiceServer` before wrapping in
the auth `InterceptedService` (the interceptor type does not expose it).

Payload instrumentation (P1 prep): ReadAll/ReadMultiple record a payload-size
histogram plus a large-payload counter when a response crosses the configured
threshold (default 8MiB), feeding alerting on paths that contend with
latency-sensitive control-plane traffic on the shared channel. Threshold-only
counter, no per-call hot-path log.

Verification: cargo check/test on config, io-metrics, ecstore, rustfs; clippy
clean on touched files; make pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(internode): P1 control/bulk gRPC channel isolation (opt-in)

Land the P1 subtask from docs/grpc-optimization: physically separate large
bytes-carrying unary RPCs from latency-sensitive control-plane RPCs so a big
transfer can no longer head-of-line block a lock/health RPC on the shared
HTTP/2 connection (G2/G5).

Introduce ChannelClass { Control, Bulk } and get_channel_for_class in protos.
Control RPCs keep the per-peer connection keyed by the bare address; Bulk RPCs
(ReadAll/WriteAll/ReadMultiple/BatchReadVersion, via a new get_bulk_client) are
round-robined across a small per-peer bulk pool.

Rather than restructuring the global GLOBAL_CONN_MAP (and every consumer), bulk
channels are cached under a composite key (addr\0bulk\0idx). The NUL separator
cannot appear in a URL, so bulk keys never collide with the control key. This
keeps the blast radius small on a consistency-sensitive path. create_new_channel
is refactored into build_channel(dial_addr, cache_key) so several physically
distinct channels to one peer cache independently while dialing/TLS still use
the real address.

Gated by RUSTFS_INTERNODE_CHANNEL_ISOLATION (default OFF) so the default build
is byte-for-byte the pre-P1 behavior: bulk resolves to the control channel and
the switch is a single-env rollback. RUSTFS_INTERNODE_BULK_CHANNELS (default 2,
clamped >=1) sizes the pool. On failure, evict_failed_connection drops the whole
bulk pool for the peer (round-robin hides which index was used), avoiding
half-dead cached channels.

Lock RPCs (remote_locker) already use the default Control path, so lock
semantics and retry behavior are unchanged.

Verification: cargo check/test on config, protos, ecstore, rustfs; new protos
tests for bulk key routing and isolation-off passthrough; clippy clean on
touched files; make pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(internode): P2 msgpack/JSON codec observability + encode buffer presizing

Land the safe, wire-compatible slice of P2 from docs/grpc-optimization: the
observability prerequisite for retiring the redundant JSON fields, plus a codec
micro-optimization. No proto/wire-format change; JSON is still dual-written.

Internode RPCs today dual-encode each metadata value as both msgpack (`*_bin`)
and a JSON compatibility string, and decoders prefer `_bin` with a JSON
fallback. Before the JSON fields can ever be dropped (a cross-version change),
that fallback must be proven unused in production.

Add rustfs_system_network_internode_msgpack_json_fallback_total{direction,
message}: incremented whenever a decode falls back to the JSON field because the
msgpack payload was absent. Wired into both directions — the client decoding
peer responses (remote_disk.rs, incl. the list-level read_multiple/batch
fallbacks) and the server decoding peer requests (node_service/disk.rs). This
counter must read zero across a release window before send paths stop writing
JSON and the proto text fields are reserved/removed (the deferred P2-1 steps).

Also pre-size the msgpack encode buffers (Vec::with_capacity(512)) on both
sides, eliminating the repeated growth reallocations for typical FileInfo
payloads with zero added copy. Full thread_local buffer pooling is deferred: it
needs either an extra copy (unclear net win) or a send-path buffer-return
lifecycle, to be justified by a codec microbenchmark first.

Verification: cargo check/test on io-metrics, ecstore, rustfs; new fallback
counter smoke test; existing codec decode tests green; clippy clean on touched
files; make pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(internode): add msgpack/JSON convergence observation runbook

Runbook driving the observation-gated retirement of the redundant JSON
compatibility fields on internode gRPC metadata RPCs (grpc-optimization P2-1).

Documents the shipped fallback counter
(rustfs_system_network_internode_msgpack_json_fallback_total{direction,message}),
the PromQL to confirm it reads zero across a release window, a standing alert,
and the staged flip/rollback procedure (env-gated msgpack-only send, then proto
field removal in N+1).

Includes the verified field -> peer-decoder audit: only fields whose peer
decodes _bin first may be converged. Notes DeleteVersion.opts (DeleteOptions) is
NOT convergence-ready — its server handler is not _bin-first and must gain a
decode_msgpack_or_json path first. This gates the send-side change so it cannot
empty a JSON field an old peer still needs.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(internode): env-gated msgpack-only send + DeleteVersion _bin support (P2-1)

Implements the send-side lever for retiring the redundant JSON compatibility
fields on internode gRPC metadata RPCs, plus the missing `_bin` support on the
delete path that it depends on (grpc-optimization P2-1). Default-off: the base
build is byte-for-byte the prior dual-write behavior.

Gated msgpack-only send (RUSTFS_INTERNODE_RPC_MSGPACK_ONLY, default false):
- New rustfs_protos::internode_rpc_msgpack_only() reads the flag.
- Client (remote_disk.rs) compat_json() and server (node_service/disk.rs)
  compat_response_json() emit an empty JSON string when the flag is on, so only
  the msgpack _bin payload is sent. The _bin field is always sent; decoders keep
  the JSON read fallback. Applied only to fields with a confirmed _bin-first peer
  decoder (WriteMetadata/UpdateMetadata/RenameData file_info, UpdateMetadata opts,
  ReadOptions, ReadMultipleReq, BatchReadVersionReq; ReadVersion/ReadXL/RenameData
  responses and the ReadMultiple/BatchReadVersion response lists).
- Only enable after the P2 fallback counter has read zero across a release window
  (see docs/operations/internode-msgpack-json-convergence-runbook.md). Single-env
  rollback; no wire-format break.

DeleteVersion(s) _bin support (prerequisite):
- The DeleteVersion/DeleteVersions protos had NO _bin fields. Add additive
  (backward-compatible) bytes file_info_bin/opts_bin (DeleteVersion) and repeated
  bytes versions_bin + bytes opts_bin (DeleteVersions); regenerate the checked-in
  prost struct.
- Client dual-writes them; server decodes them _bin-first with JSON fallback.
- These delete fields are kept OUT of the msgpack-only set (always dual-write)
  until their own fallback counter reads zero across a window with the new
  decoders fully deployed. DeleteVersion.raw_file_info stays JSON-only (no _bin
  field yet).

Verification: cargo check/test on protos, config, ecstore, rustfs (incl. the six
delete request handler tests and a compat_json default-path test); clippy clean
on touched files; make pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(internode): P3 cluster peer online/offline health metric

Land the safe observability core of P3 (grpc-optimization G6/G8): track each
internode peer's reachability and expose the offline count, for parity with
MinIO's minio_cluster_servers_offline_total. Pure instrumentation — peer
selection and quorum are unchanged.

- io-metrics: per-peer PeerHealthState { online, consecutive_failures } registry
  plus record_peer_reachable/record_peer_unreachable. A peer flips offline after
  N consecutive failures (dial failures or RPC-triggered evictions) and back
  online on the next successful dial; the count of offline peers is published to
  the rustfs_cluster_servers_offline_total gauge.
- config: RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD (default 3, clamped >= 1).
- protos: build_channel marks the peer reachable on a successful dial and
  unreachable on a dial failure; evict_failed_connection feeds the failure signal
  too. Keyed by the real peer address, so control and bulk channels to one peer
  share health state.

Deferred (documented in docs/grpc-optimization P3): startup prewarm (no clean
topology-ready hook yet), the offline fast-bypass in peer routing (consistency-
sensitive; must not change quorum), and idempotent-read-only retry. This commit
is observability only.

Verification: cargo check/test on io-metrics, config, protos (new peer-health
state-machine and threshold-clamp tests); clippy clean on touched files; make
pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(internode): P3 control-channel prewarm + self-healing offline bypass

Add the remaining P3 connection-lifecycle levers (grpc-optimization G6/G8), both
env-gated and default-off so the base build is unchanged.

Prewarm (RUSTFS_INTERNODE_PREWARM, default off): RemoteDisk::new spawns a
best-effort background dial of the peer's control channel, deduped per peer
address, moving the connect cost off the first RPC. Failures fall through to the
existing lazy connect + recovery monitor.

Offline bypass (RUSTFS_INTERNODE_OFFLINE_BYPASS, default off): remote_disk
get_client/get_bulk_client fast-fail a peer already marked offline instead of
paying the connect timeout, so the erasure layer proceeds on quorum sooner. This
does NOT change quorum. It is self-healing: cluster_peer_should_bypass lets one
request per RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS (default 5s) through to recover
the peer even with no background monitor, and the recovery monitor's own probe
path calls the client directly so it is never bypassed.

io-metrics gains cluster_peer_is_offline / cluster_peer_should_bypass (with a
per-peer re-probe timestamp). Scope: data path only — remote_locker (lock RPCs,
most consistency-sensitive) is left dual-writing/unbypassed as a follow-up.

Verification: cargo check/test on io-metrics, config, ecstore (new self-healing
bypass tests; all 105 rpc tests green); clippy clean on touched files; make
pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(internode): add A/B benchmark runbook for gRPC optimization stages

Reproducible before/after collection procedure for grpc-optimization P0–P3.
Since every stage is env-gated, before/after is the same binary with different
env — no rebuild. Documents, per stage: the exact env toggles (baseline vs
enabled column), which existing bench script to run
(run_internode_transport_baseline.sh / run_four_node_cluster_failover_bench.sh),
the Prometheus metrics to capture, and the acceptance gates from the design docs
(e.g. lock p99 down >= 20% for P1, msgpack fallback counter = 0 before enabling
P2, correct rustfs_cluster_servers_offline_total for P3).

Live runs require a multi-node cluster + load tool + Prometheus scrape and cannot
be produced in a single-process sandbox; artifacts land under target/bench
(gitignored) and attach to the PR.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(internode): P3-2 lock-path offline bypass + P3-3 idempotent read retry

Extend the offline bypass to the lock path and add opt-in retries for idempotent
reads (grpc-optimization P3-2/P3-3). Both env-gated and default-off/zero.

Offline bypass (lock path): factor the bypass decision into a shared pub(crate)
internode_offline_bypass_reason(addr) and call it from remote_locker::get_client
too, so lock RPCs to an offline peer fast-fail (letting dsync reach quorum
sooner) instead of paying the connect timeout. Does not change quorum; the
self-healing re-probe keeps peers recoverable. Gated by
RUSTFS_INTERNODE_OFFLINE_BYPASS (default off).

Idempotent read retry (P3-3): add execute_read_with_retry — a bounded,
exponential-backoff retry for read-only/reentrant RPCs on transient network
errors — and route disk_info through it. RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES
defaults to 0 (disabled). Write/lock RPCs are never retried (quorum/idempotency
safety, per CLAUDE.md); the wrapper requires an Fn closure so only reads that
rebuild their request from borrowed inputs qualify.

Deferred: grpc.health.v1 (optional ecosystem-compat only; needs a new
tonic-health dep and 3-way hybrid-service wiring — internal needs are met by the
existing Ping RPC).

Verification: cargo check/test on config, ecstore (105 rpc tests green incl.
disk_info now via the retry wrapper); clippy clean on touched files; make
pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(scripts): one-click internode gRPC A/B benchmark driver

Wrap the per-stage env matrix from the benchmark runbook into
scripts/run_internode_grpc_ab_bench.sh: given --stage <p0|p1|p2|p3> and --phase
<before|after>, it emits the stage/phase RUSTFS_INTERNODE_* server env to
<out-dir>/server-env.sh and runs the right underlying bench
(run_internode_transport_baseline.sh for p0/p1/p2, run_four_node_cluster_failover_bench.sh
for p3) into a labeled target/bench/internode-transport/<stage>-<phase>/.

Passthrough args after `--` reach the underlying bench; --dry-run previews the
env + command. The script is explicit that RUSTFS_INTERNODE_* are server env, so
for the load-driven stages the operator must restart rustfs with the emitted env
before the run; the docker four-node (p3) path exports them for a forwarding
compose. shellcheck-clean.

Runbook updated with a "One-click driver" section.

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore(compose): forward RUSTFS_INTERNODE_* into the four-node cluster

The four-node local-build compose only forwarded a fixed whitelist of env, so
the internode gRPC knobs (grpc-optimization P0-P3) never reached the containers
and the A/B bench driver's "after" phase was a no-op. Forward the full
RUSTFS_INTERNODE_* set with defaults matching the binary defaults, so leaving
them unset is a no-op and the A/B driver can toggle a stage per phase.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(internode): address Copilot review — retry health action + poison-safe peer health

Two review nits on #4337:

- P3-3 idempotent read retry (remote_disk.rs): execute_read_with_retry ran every
  attempt through execute_with_timeout_for_op, which hardcodes
  FailureHealthAction::MarkFailure. So the first transient error could flip the
  disk faulty and short-circuit the remaining retries, and each attempt
  over-counted the failure. Route all but the final attempt through
  execute_with_timeout_for_op_and_health_action with IgnoreFailure; only the last
  attempt marks faulty/evicts. No default impact (retries default 0).

- Peer-health helpers (io-metrics): record_peer_reachable/record_peer_unreachable,
  cluster_peer_is_offline and cluster_peer_should_bypass early-returned on a
  poisoned mutex, permanently stalling the offline gauge and bypass state after a
  single panic. Recover via PoisonError::into_inner().

Verification: cargo check/test on io-metrics + ecstore (105 rpc tests green);
clippy clean on touched files; make pre-commit green.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 05:18:31 +08:00
Zhengchao An 8f11222a63 feat(admin): add MinIO-compatible IAM and IDP endpoints (#4334)
feat(admin): add MinIO-compatible IAM/IDP admin endpoints

Register and implement MinIO admin API compatibility for IAM/IDP:
- PUT /v3/import-iam-v2 and POST /v3/revoke-tokens/{userProvider}
- generic /v3/idp-config/{type}[/{name}] CRUD mapped onto existing config
- LDAP/OpenID service-account, policy-entities, and list-access-keys flows

Adds IamSys::delete_temp_account primitive to back STS token revocation.
revoke-tokens requires the broader ListUsers admin capability for
cross-user revocation (self-revocation only needs RemoveServiceAccount),
mirroring the cross-user guard used by the service-account handlers.
Registers admin route-policy inventory entries for every new route.
Unsupported LDAP/OpenID backends return honest compatibility errors.

Refs rustfs/backlog#609 #610 #616
2026-07-07 05:12:30 +08:00
Zhengchao An 3420f28c17 feat(admin): add MinIO-compatible diagnostics endpoints (#4333)
feat(admin): add MinIO-compatible diagnostics and service-control endpoints

Register and implement MinIO admin API compatibility routes:
- POST /v3/service: restart/stop trigger the existing graceful-shutdown
  path; freeze/unfreeze record advisory state (request admission is not
  yet gated, surfaced honestly via effective=false)
- profiling/trace and healthinfo/obdinfo/log diagnostics endpoints
- GET /v3/top/locks and POST /v3/force-unlock, backed by new
  FastObjectLockManager::list_locks()/force_unlock() introspection
- speedtest routes where feasible

Refs rustfs/backlog#604 #606 #607 #615
2026-07-07 04:46:42 +08:00
Zhengchao An 68f048b8fe fix(replication): persist delete marker mtime in MRF entries (#4331)
fix(replication): persist original mtime in MRF entries (backlog#867)

MRF delete entries did not persist the original delete-marker mtime, so
after a restart the recovery replay path reconstructed the delete without
a source timestamp. Downstream the replica delete-marker was stamped with
the replay time (now()) instead of the source mtime, causing delete-marker
timestamp divergence across clusters.

Extend the MrfReplicateEntry disk format with an optional deleteMarkerMtime
field (persisted as Unix nanoseconds) in both duplicate struct definitions
(rustfs-replication and rustfs-filemeta). DeletedObjectReplicationInfo now
persists delete_marker_mtime, and start_mrf_processor restores it onto the
reconstructed delete so the replica keeps the source timestamp.

Backward compatibility: the new key uses skip_serializing_if + serde
default, so historical MRF files without it decode to None and replay
falls back to the current time (pre-#867 behaviour). No panic or entry
loss on old files.

Closes rustfs/backlog#867
2026-07-07 04:42:10 +08:00
Zhengchao An b60686eb6f feat(admin): add replication diff/mrf and batch job admin endpoints (#4332)
Add MinIO-compatible admin endpoints under the /v3 prefix, wired to real
RustFS subsystems where they exist and documented as compatibility-only
where they do not.

Replication (backlog#611):
- POST /v3/replication/diff: bounded on-demand scan of object versions,
  returning versions whose replication status is PENDING or FAILED. Wired
  to list_object_versions and the per-version replication status. Requires
  the bucket to exist and have a replication configuration. The scan is
  capped (REPLICATION_DIFF_MAX_SCAN) and reports IsTruncated when partial.
- GET /v3/replication/mrf: reports the failed-replication backlog (MinIO's
  MRF concept) from the replication stats handle: aggregate failed count and
  size per target plus in-queue counters. RustFS records failed replications
  as aggregate counters and persists MRF entries without a runtime query API,
  so per-object MRF rows are not enumerable; PerObjectEntriesAvailable is
  always false to make that explicit.

Batch jobs (backlog#613):
- POST /v3/start-job, GET /v3/list-jobs, GET /v3/status-job,
  GET /v3/describe-job, DELETE /v3/cancel-job.
- RustFS has no batch-job execution engine (no scheduler, queue, or worker),
  so these implement MinIO request/response/error semantics only and never
  fake execution or success: start-job parses and validates the job
  definition then returns NotImplemented for known job types and
  InvalidRequest for unknown ones; list-jobs returns an empty list;
  status/describe/cancel validate jobId and return a no-such-job error.

All routes are registered with admin auth using existing AdminAction
variants (ReplicationDiff, GetReplicationMetricsAction, StartBatchJobAction,
ListBatchJobsAction, DescribeBatchJobAction, CancelBatchJobAction) and are
covered by the admin route-registration matrix and new handler unit tests.

Refs rustfs/backlog#611 #613
2026-07-07 04:36:55 +08:00
Zhengchao An 511ad31ba0 fix(s3): normalize root double-slash ListBuckets requests (#4336)
test(s3): MinIO parity for double-slash ListBuckets, invalid copy-source date, region-aware CreateBucket

- Add DoubleSlashListBucketsCompatLayer: rewrite `GET //` to `GET /` so
  it routes to ListBuckets, matching MinIO browser-client behavior (#601)
- Add e2e regression coverage for invalid copy-source conditional date
  headers (#618) and region-aware MakeBucket(us-east-1) (#629)

Refs rustfs/backlog#601 #618 #629
2026-07-07 04:36:16 +08:00
Zhengchao An eb02486574 chore(swift): remove placeholder SSE module (#4330)
security(swift): remove placeholder SSE module (backlog#646)

The Swift `encryption` module was a non-functional stub: `encrypt_data`
returned the plaintext unchanged while labeling it AES-256-GCM in the
object metadata, and `generate_iv` derived the IV from a timestamp
rather than a CSPRNG. It had no production caller (only a `pub mod`
declaration and one integration test), so wiring it in as-is would have
silently shipped plaintext advertised as ciphertext.

We are not supporting Swift server-side encryption for now, so remove
the module outright rather than keep a dangerous stub around:
- delete crates/protocols/src/swift/encryption.rs
- drop `pub mod encryption;` from swift/mod.rs
- remove the encryption case (and unused import) from the swift
  integration test

The module reached main dubiously: it was introduced together with the
whole Swift API in commit 86e93624 ("fix(heal): canonicalize scanner
object-dir repairs (#3864)"), a 1665-file squash whose PR description
only covered the heal change and never mentioned Swift or SSE.

Verified: cargo fmt; cargo test -p rustfs-protocols --features swift
--test swift_simple_integration (10 passed); arch guardrail scripts pass.
2026-07-07 04:29:24 +08:00
Zhengchao An 3bc8d79fe5 fix(multipart): serialize put_object_part per uploadId (#4329)
fix(multipart): serialize the part commit per uploadId to prevent mixed-generation shards (backlog#853)

Concurrently re-transmitting the same part could land two shard
generations across different disks: each shard is individually
bitrot-valid, so the corruption surfaces only as a silent mix at read
time.

The hazard is confined to rename_part, where two temp parts are moved
cross-disk onto the SAME final part path — interleaving there can leave
shards from two generations. Each concurrent stream already writes to its
own unique temp dir, so the encode/stream phase never conflicts and must
stay lock-free: holding a lock across it would serialize slow re-transmits
of the same part, break the S3 'last finisher wins' semantics, and cause
UploadPart lock-acquire timeouts (ServiceUnavailable).

Scope a write lock to the uploadId namespace around only the rename_part
commit, so each commit is atomic across disks and the last committer wins
consistently. Mirrors MinIO's per-uploadID NS lock; disjoint from the
object lock held by complete_multipart_upload (no lock-ordering cycle).
Honors opts.no_lock.

The pre-delete-before-rename half of backlog#853 was already fixed in
#4316; this completes the issue.

Adds an end-to-end regression test: six concurrent resends of the same
part must all succeed (no lock-acquire timeout), exactly one generation
stays visible, and the reassembled object equals one intact generation.

Closes rustfs/backlog#853
2026-07-07 04:28:39 +08:00
houseme 6d0583872b test(ecstore): serialize list_objects mutation-sequence tests (#4338)
Three tests share the global list-objects mutation-sequence state (the
LIST_OBJECTS_MUTATION_SEQUENCE atomic and the per-bucket sequence map)
and all call reset_list_objects_mutation_sequences_for_test(), but only
observed_mutation_persists_namespace_journal_high_water was marked
#[serial_test::serial]. The serial lock only excludes tests that also
carry the attribute, so the unmarked tests still ran concurrently with
it: the journal test's persist of "bucket" -> 11 could land after the
recovery test's reset, making it observe 11 instead of the expected 9
(flaky under the default multi-threaded test runner, green with
--test-threads=1).

Add #[serial_test::serial] to the two unmarked tests:
- persistent_key_only_index_load_recovers_mutation_sequence_from_journal
- list_objects_mutation_sequence_advances_per_bucket

Unique bucket names alone would not fix this: the per-bucket assertions
in list_objects_mutation_sequence_advances_per_bucket check exact values
produced by the single global atomic counter, which any concurrent
observe call perturbs, and every reset zeroes that global state for
whichever peer is mid-flight.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-07 04:18:56 +08:00
Zhengchao An 75fab0a842 fix(filemeta): satisfy clippy::field_reassign_with_default in replication_info_equals test (#4335)
fix(filemeta): use struct-init to satisfy clippy::field_reassign_with_default

The replication_info_equals test constructed FileInfo via
`let mut x = FileInfo::default()` followed by field reassignment, which
clippy::field_reassign_with_default (deny-by-default under -D warnings)
rejects on the lib-test target, breaking `Test and Lint` CI on every PR.

Rewrite the three constructions using struct-init + ..Default::default().
Test-only change; behavior unchanged.
2026-07-07 03:01:43 +08:00
houseme 25193ee6cf feat(listobjects): add guarded metadata-fast listing (#4311)
* feat(listobjects): add phase 0 observability metrics

* perf(listobjects): reduce gather metadata decoding

* test(listobjects): add adversarial listing coverage

* feat(listobjects): add source-aware cursor fields

* feat(listobjects): define index source modes

* feat(listobjects): model index rebuild health

* feat(listobjects): add opt-in index fallback gate

* feat(listobjects): verify index candidate pages

* docs(listobjects): add rollout runbook

* docs(listobjects): untrack baseline fixtures

* feat(listobjects): add opt-in key-only provider

* feat(listobjects): add index verification metrics

* perf(listobjects): gate list metrics on get stage switch

* feat(listobjects): bind provider health generation

* feat(listobjects): add persistent key-only provider

* feat(listobjects): bind persistent provider lifecycle

* feat(listobjects): track persistent index mutations

* feat(listobjects): persist index mutation checkpoints

* fix(listobjects): aggregate benchmark metric snapshots

* feat(listobjects): store journal in system namespace

* chore(listobjects): report fallback reasons in bench

* test(listobjects): verify metadata-fast stale fallbacks

* feat(listobjects): serve metadata-fast snapshots behind guardrails

* test(listobjects): add metadata-fast chaos bench

* fix(listobjects): attribute metadata-fast fallback metrics

* test(listobjects): add metadata-fast fallback chaos probes

* test(listobjects): add quorum journal chaos guardrails

* fix(listobjects): satisfy pre-pr clippy gate

* docs(listobjects): untrack rollout runbook

* fix(ecstore): remove redundant heal error conversion

* test(filemeta): satisfy replication info clippy

----

Co-Authored-By: heihutu <heihutu@gmail.com>
2026-07-07 01:38:52 +08:00
Zhengchao An 0271abc14d docs: add MinIO compatibility router + file-format docs (#4328)
Add two durable architecture references grounded in current code:

- minio-rustfs-router-compatibility.md: MinIO cmd/api-router.go (S3
  object/bucket) and cmd/admin-router.go (admin /v3, /v4) vs RustFS
  implementation status, with per-row landing points and a gaps-only
  checklist of still-missing admin endpoints (profiling, healthinfo,
  LDAP IDP config, replication diff, MRF, batch, locks, speedtest,
  log stream, top, trace).
- minio-file-format-compat.md: xl.meta (meta_ver 3 write, <=3 read,
  XL2 magic, rs-vandermonde, HighwayHash256 bitrot, inline data) and
  .metadata.bin bucket-metadata interop matrix for the backlog#580
  items, plus old-RustFS -> new migration and a phased plan.

Cross-links s3-compatibility-matrix.md and admin-route-action-snapshot.md
and indexes both docs in docs/architecture/README.md.

Refs rustfs/backlog#596 rustfs/backlog#603 rustfs/backlog#580
2026-07-06 23:34:31 +08:00
Zhengchao An a2d679cdb6 fix(bucket): propagate bucket deletion to peer metadata caches (backlog#646) (#4326)
The peer `DeleteBucketMetadata` RPC handler was a stub that returned
success without doing anything, and the delete-bucket flow never sent
the notification in the first place. As a result, after a bucket was
deleted other nodes kept serving its stale cached metadata.

Wire the whole path end to end:
- ecstore: add an in-memory `remove_bucket_metadata` (free fn) and
  `BucketMetadataSys::remove`, the counterpart to `set_bucket_metadata`,
  and export it through the `api::bucket::metadata_sys` facade.
- node_service: `handle_delete_bucket_metadata` now validates the bucket
  name and actually drops the cached metadata for it.
- bucket_usecase: after a successful delete_bucket, notify peers via
  `notification_sys.delete_bucket_metadata` in the background, symmetric
  to the existing `notify_bucket_metadata_reload` path.

Also update the delete-bucket-metadata unit test to assert the
empty-bucket rejection instead of the old always-success stub, and drop
an unused `tracing::debug` test import left over from #4322.

Verified: cargo fmt; cargo check -p rustfs-ecstore; cargo test -p rustfs
--lib --features rio-v2 test_delete_bucket_metadata_empty_bucket; arch
guardrail scripts pass.
2026-07-06 23:27:47 +08:00
Zhengchao An 655c5cb403 fix(ecstore): reject short-read shards and advance ParallelReader per stripe (backlog#799 B2) (#4327)
Two compounding defects let a truncated shard corrupt a GET (ranged GET could
return HTTP 200 with wrong bytes):

- `BitrotReader::read` returned `Ok(short_len)` when the shard stream hit EOF
  before filling the caller's buffer. With `skip_verify` /
  `HashAlgorithm::None` / parity=0 there is no hash to catch it, so the short
  shard was accepted and every downstream byte shifted.
- `ParallelReader.offset` was set once and never advanced per stripe, so every
  stripe after the first reused the first stripe's geometry and the last-stripe
  length clamp was wrong.

Fix both (they are mutually required):

- `BitrotReader::read` now errors (`UnexpectedEof`) on a short read, before and
  independent of the bitrot hash check, so it fires under skip-verify/no-hash
  too. The caller sizes the buffer to the expected per-stripe shard length, so
  "buffer not filled" == "shard truncated". A short read routes through the
  existing `errs[i]` path, dropping that reader from the stripe so parity
  reconstruction engages; with parity=0 the stripe fails read quorum and the GET
  errors loudly instead of streaming shifted bytes. Mirrors MinIO's
  `parallelReader.Read` (`n != shardSize` -> reader failed).
- `ParallelReader::read` / `read_lockstep` advance `self.offset += shard_size`
  per stripe so the per-stripe expected length (incl. the shorter final stripe)
  is exact, matching the correct pattern already used by heal.

Updates three bitrot tests that used an obsolete oversized-buffer pattern to
size per-stripe (as the real decode/heal paths do), and adds a regression test
that a truncated shard errors under None/HighwayHash + skip_verify.

Refs backlog#799 (B2), issue rustfs/backlog#851. Design converged by two
independent expert reviews referencing MinIO cmd/erasure-decode.go.
2026-07-06 23:25:54 +08:00
Zhengchao An 6297d112c3 fix: auto-repair breaking changes from 1b3727e2 (#4324)
fix(heal): remove useless .into() conversion in heal error path

Clippy flagged a useless_conversion lint at heal.rs:491 where
 is redundant because  is already of type .
2026-07-06 23:14:14 +08:00
Zhengchao An 92596da7fa fix(filemeta): key round-tripped replication reset state by canonical header (backlog#799 B16) (#4325)
fix(filemeta): key round-tripped replication reset state by canonical header (backlog#799 B19->B16)

`get_internal_replication_state` stored the reset status under the bare ARN after
stripping the internal prefix, while the write and lookup sides
(`get_replication_state` / `ReplicationState::target_state`) use the full
`target_reset_header(arn)` key. A `target_state` fallback masked the lookup miss,
but the map stayed keyed inconsistently (bare on read, full on write), which can
drop reset state across merge/reflatten cycles.

Store the canonical `target_reset_header(arn)` key on read so the map is
consistent everywhere; the lookup fallback becomes belt-and-suspenders rather
than load-bearing. Adds a round-trip regression test.

Refs backlog#799 (B16), tracked in rustfs/backlog#863.
2026-07-06 22:58:52 +08:00
Zhengchao An 31c6859965 chore: converge stale TODOs and apply safe fills (backlog#646) (#4322)
Second TODO-convergence round over the current tree (backlog#646). All
line numbers in the old inventory had gone stale after the set_disk /
diagnostics / cluster refactors, so this re-scans and reduces the marker
count from 144 to 99.

STALE removals (comment describes already-implemented behavior, or dead
commented-out blocks) across ecstore (set_disk ops/core, store,
cluster/rpc, bucket/metadata_sys, services), iam, filemeta, s3select and
rustfs auth/object_usecase. No behavior change.

Safe fills, each verified:
- filemeta: replication_info_equals now also compares
  replication_state_internal (function currently has no callers; adds a
  regression test).
- bitrot: drop the confirmed-unused `_want` parameter from bitrot_verify
  and the now-unused `sum` on LocalDisk::bitrot_verify, removing a
  Bytes::copy_from_slice allocation. Streaming verify uses the file's
  embedded per-shard hash, never the passed sum.
- signer: rename v4_ignored_headers -> V4_IGNORED_HEADERS and drop the
  non_upper_case_globals allow.
- admin/heal: test_decode was #[ignore]d and used serde_urlencoded on a
  JSON body (would panic); rewire to serde_json::from_slice to match the
  production decode path, add assertions, un-ignore.

Verified: cargo fmt; cargo check on touched crates; tests pass
(filemeta, signer, bitrot, heal::test_decode); arch guardrail scripts
pass.
2026-07-06 22:43:32 +08:00
Zhengchao An 5f1759eb3c ci: cancel PR workflows on close (#4323) 2026-07-06 22:38:00 +08:00
Zhengchao An 83edafb11f fix(ecstore): reject renewing a mis-mounted drive into the wrong set (backlog#799 B19) (#4321)
fix(ecstore): reject renewing a misplaced drive into the wrong set (backlog#799 B19)

`renew_disk` located the drive's (set, disk) position from its own format via
`find_disk_index`, but never checked that the resolved `set_idx` matched this
`SetDisks`' own `set_index` before inserting the drive into `self.disks`. A
drive whose format places it in another set would be claimed by this set too,
so two sets could manage the same drive and degrade together.

Skip (with a warning) when `set_idx != self.set_index`.

Refs backlog#799 (B19), tracked in rustfs/backlog#863.
2026-07-06 22:29:05 +08:00
Zhengchao An 5c5f8063d8 fix(replication): log (not swallow) resync status persistence failure (backlog#799 B23) (#4320)
fix(replication): don't silently swallow resync status persistence failure (backlog#799 B23)

After a resync computes a new replication status, it persists it via
`put_object_metadata` but discarded the `Err` case (`if let Ok(u) = ...`). A
failure left the object's on-disk replication status disagreeing with the resync
result with no signal at all. Log the failure at warn level instead.

Refs backlog#799 (B23), tracked in rustfs/backlog#863.
2026-07-06 22:25:00 +08:00
Zhengchao An 1b3727e2c4 fix(heal): clean up .rustfs/tmp healed shards on all failure paths (backlog#799 B20) (#4319)
heal_object wrote healed shards to .rustfs/tmp/<uuid>/ and only removed that tmp
dir on the success path (the final delete_all). Three early exits after the tmp
shards were written leaked the dir:

- `erasure.heal(...)?` failing midway,
- the `disks_to_heal_count == 0` early return, and
- the `?` on the post-rename remote data-dir delete.

Add the tmp cleanup before the first two, and downgrade the remote data-dir
cleanup failure to a warning (the healed shard is already renamed into place, so
that cleanup failing must not abort the heal or leak the tmp shards).

Refs backlog#799 (B20), tracked in rustfs/backlog#863.
2026-07-06 22:16:58 +08:00
唐小鸭 849ea9a122 fix(site-replication): align IAM and bucket metadata replication (#4318) 2026-07-06 22:15:02 +08:00
Zhengchao An 5c67c508cb fix(filemeta): skip unknown fields when decoding MetaDeleteMarker (backlog#799 B17) (#4317)
`MetaDeleteMarker::decode_from` returned an error on any field key it didn't
recognize, unlike `MetaObject::decode_from` / `MetaObjectV1::decode_from`, which
skip unknown fields. That breaks forward compatibility: a delete marker written
by a newer version with an extra key fails to decode on an older binary.

Skip the unknown field's value (`skip_msgp_value`) and continue, matching the
object decoders. Adds a regression test.

Refs backlog#799 (B17), tracked in rustfs/backlog#863.
2026-07-06 22:09:05 +08:00
Zhengchao An 8ffe230a89 fix(multipart): don't pre-delete the committed part before rename_part (backlog#853) (#4316)
`rename_part` unconditionally ran `cleanup_multipart_path([part.N, part.N.meta])`
on every disk *before* the per-disk rename fan-out. The per-disk `rename_part`
already replaces `part.N` atomically (std::fs::rename) and rewrites `part.N.meta`,
so the pre-delete is redundant — and destructive: it removed an already-committed
(ACKed) part on all disks before the new rename landed, so a re-upload of the same
part that then failed write quorum destroyed the committed part outright.

Remove the pre-rename cleanup and rely on the atomic rename to overwrite in place;
the quorum-failure rollback below is unchanged. No behaviour change on the success
paths (first upload / retry both end with part.N replaced), verified by the
existing multipart suite.

Refs backlog#799 (B4). The remaining half of B4 — serializing concurrent
same-part uploads with an uploadId-scoped lock so two generations can't be mixed
across disks — is a larger concurrency change tracked as a follow-up on the issue.
2026-07-06 22:08:50 +08:00
Zhengchao An 5e73d59eb6 fix(replication): re-derive replication decision when replaying MRF delete entries (backlog#858) (#4315)
MRF delete replay reconstructed the delete with `..Default::default()`, leaving
`replication_state = None`. `replicate_delete` derives its target set purely
from `replication_state.replicate_decision_str`, so an empty state produced an
empty decision -> zero targets: the replayed delete contacted no remote at all,
a silent no-op that left replicas permanently diverged after a restart.

The MRF entry doesn't persist the decision and the source object is already
gone, so re-derive it from the live bucket config at replay time via
`check_replicate_delete` — mirroring the object heal path
(`get_heal_replicate_object_info`) — and set it on the reconstructed delete's
`replication_state`. This is a contained fix with no change to the on-disk MRF
format.

Refs backlog#799 (B9).

Note: the source delete-marker mtime is still not persisted in the MRF entry,
so a replayed delete marker is stamped with the replay time on the target. That
is a separate, minor consistency nuance (the delete now propagates correctly)
and can be addressed by extending the MRF entry format in a follow-up.
2026-07-06 22:08:17 +08:00
Zhengchao An f33ae8e9af fix(replication): keep MRF persister backlog cumulative so flushes don't drop entries (backlog#859) (#4314)
The MRF persister accumulated overflow entries in `pending`, flushed them with
`flush_mrf_to_disk`, and cleared `pending` on success. But `flush_mrf_to_disk`
*overwrites* the whole MRF file with exactly the entries passed. After flushing
batch A (file = A) and clearing, the next flush wrote batch B and thereby
overwrote the file to contain only B — and the MRF file is only replayed (and
cleared) at startup, never during the run, so batch A's entries were silently
lost. A crash after the B flush lost all of batch A's pending replications.

Keep `pending` cumulative (the file must hold the full set of overflow entries
for the run) and rewrite the whole set on each flush instead of clearing after
success:

- flush eagerly once 1 000 *new* entries accumulate since the last write
  (measured against the flushed length, so a large backlog isn't rewritten on
  every add), and on the 10s tick when dirty;
- bound the in-memory/on-disk backlog with `MRF_PENDING_CAP` (200 000) and log
  once when the cap is hit rather than growing without limit.

Refs backlog#799 (B10).
2026-07-06 21:34:45 +08:00
Zhengchao An 650a3e5734 fix(replication): classify resync verification HEAD errors instead of miscounting (backlog#862) (#4312)
The resync result verification HEADed the target after replicating and counted
the outcome with inverted error handling:

- for a delete marker, ANY HEAD error (timeout, 5xx, auth, malformed) was
  counted as replicated (success);
- for a versioned object against an AWS-style target, HEAD was sent with the
  RustFS UUID versionId, which AWS rejects with 400, so a well-replicated
  object was counted as failed.

Classify the error before counting:

- delete marker: only a definitive 404/NoSuchKey or 405/MethodNotAllowed
  confirms the marker propagated (`is_retryable_delete_replication_head_error`
  == false); any retryable/ambiguous error now counts as failed;
- versioned object with a version-id-format rejection: re-verify via
  `head_object_fallback` (versionId-less HEAD) before deciding — present ->
  replicated, absent/error -> failed;
- all other errors: failed, as before.

Reuses the existing, unit-tested classifier helpers. Verified against the
existing resyncer suite (24 tests).

Refs backlog#799 (B13).
2026-07-06 21:20:41 +08:00
Zhengchao An 32845e03b0 fix(replication): mark version-id fallback ETag match as Completed, not Failed (backlog#860) (#4310)
During resync against an AWS-style target that rejects RustFS UUID versionIds,
the code retries HEAD without a versionId and compares ETags. On a match it set
`replication_action = None` ("already in sync, nothing to copy") but did not
return, so control fell through to `if replication_action != ReplicationAction::All`
— a branch meant only for the unsupported metadata-only case — and stamped the
object FAILED with "metadata-only replication is not implemented". The target
already held an identical object, yet the source recorded FAILED forever, so
AWS-style targets never converged and the MRF kept re-queuing.

Handle `ReplicationAction::None` explicitly before that branch: record it as
Completed (with the resync timestamp/`replication_resynced` bookkeeping for
ExistingObject + reset_id, mirroring the HEAD-success None path) and return.
Only `ReplicationAction::Metadata` now takes the metadata-unsupported failure
branch; `All` still proceeds to the copy. This path is the only way `None`
reaches that point (the HEAD-success None case already returns earlier).

Refs backlog#799 (B11).
2026-07-06 21:10:50 +08:00
cxymds 6c9efc8064 fix(heal): treat no-heal-required format results as noop (#4301) 2026-07-06 21:09:18 +08:00
Zhengchao An 1cc1fc0f83 fix(ecstore): exclude failed-shard disks from commit so write quorum isn't inflated (backlog#852) (#4309)
`MultiWriter` recorded per-writer failures only in a private `errs` vector and
nulled the failed writer locally, but `put_object` never used that to prune the
disk set: `rename_data` ran over the full `shuffle_disks`, so a disk that took a
short/failed write still had its truncated shard renamed into place and counted
as an online disk. The object then claimed N good shards while one was
short/corrupt, so a single later disk failure could drop it below reconstructable
quorum — silent data loss.

- `write_shard`: null the writer on a generic write error too (not only on
  `ShortWrite`), so a failed writer is uniformly represented as `None` — matching
  `shutdown_writer`, which already does this.
- `put_object`: after encode, drop every disk whose writer failed
  (`drop_failed_writer_disks`) from `shuffle_disks` before `rename_data`, and
  re-check write quorum over the survivors. `rename_data` already re-checks
  quorum and rolls back if too few disks remain, so excluded disks are neither
  committed nor counted (MinIO sets failed writers to nil before `renameData`).

Adds unit tests for the exclusion/quorum accounting.

Refs backlog#799 (B3).
2026-07-06 21:05:05 +08:00
Zhengchao An c3f54f95ed fix(heal): preserve resume state and retry when heal objects fail (backlog#855) (#4308)
fix(heal): don't mark set healed / discard resume state when objects failed (backlog#855)

`execute_heal_with_resume` unconditionally called `mark_completed()` and
returned `Ok(())` after the bucket loop, even when objects failed. The caller
then treats `Ok` as success and cleans up the resume + checkpoint state, so a
heal run with per-object failures was reported as a clean completion and its
state (including the failed set) was destroyed with no retry.

Finalize based on the failure count instead:

- failed_objects == 0 -> mark completed (unchanged);
- failed_objects > 0 with retry budget left -> `schedule_retry()` (bump the
  bounded retry counter + reset per-pass progress for a full re-scan) and
  return Err, so `heal_erasure_set` preserves the resume/checkpoint state and
  the caller keeps the healing markers for the next run;
- failed_objects > 0 with retries exhausted -> drop the resume state (no
  zombie task) but still return Err so the markers survive for a later heal
  cycle / the background scanner. Never silently claim a clean completion.

The failure identities are not persisted across pages (the per-page sets are
pruned in `complete_page`), so a retry is a bounded full re-scan; healed
objects are skipped quickly on the normal-scan pass.

Adds `ResumeState::reset_for_retry`, `ResumeManager::schedule_retry`,
`ResumeCheckpoint::reset_for_retry`, `CheckpointManager::reset_for_retry`, and
regression tests. This activates the failure path the caller already documents
at task.rs ("Keep the markers on failure ... the next run re-marks and
eventually clears them"), which was previously dead because heal never
returned Err on object failures.

Refs backlog#799 (B6).
2026-07-06 20:47:29 +08:00
Zhengchao An 7975f26b90 fix(ecstore): reach orphan-dir purge on nil-version delete miss (#4189) (#4307)
PR #4220 added a purge for orphan empty-directory trees (folder keys with
no xl.meta) on the delete path, but the guard only accepted
object-not-found. Over the real HTTP DELETE path the guard is never
reached: `del_opts` pins `version_id = Uuid::nil()` for directory keys, so
the missing dir object fails the specific-version lookup with
version-not-found (FileVersionNotFound), not object-not-found. The guard
short-circuits, the store returns the error, and the API layer turns it
into a fake 204 — the ghost folder survives, exactly the #4189 symptom.

The existing unit tests passed because they call
`purge_orphan_dir_object` directly, bypassing the nil-version lookup.

Accept both misses in the guard (extracted as
`should_purge_orphan_dir_on_missing`) so the folder delete actually takes
effect. Non-directory keys and non-miss errors (e.g. quorum failures) are
unaffected; the cross-disk data-safety refusal in
`purge_orphan_dir_object` is unchanged.

Verified end-to-end against a running 4-disk erasure server: DELETE of a
planted orphan `ghost/` tree now purges it on all drives and clears the
listing, a real object under the same prefix is untouched, and a prefix
holding real data on any drive is refused (all drives preserved).

Adds predicate regression tests covering version-not-found /
object-not-found on directory keys, and the negative cases.
2026-07-06 20:47:04 +08:00
Henry Guo be52e35a1f test(table-catalog): add vendor compatibility audit (#4299)
* test(table-catalog): add vendor compatibility audit

* fix(table-catalog): include OSS data-plane endpoint

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-06 14:12:19 +08:00
Ramakrishna Chilaka b09526c0f5 feat(helm): allow overriding Kubernetes cluster domain (#4259) 2026-07-06 14:06:44 +08:00
Zhengchao An a9115f729e fix: block force delete on object lock buckets (#4298) 2026-07-06 14:05:39 +08:00
Zhengchao An 3e4c15da5d fix(object-lock): prevent locked version deletes (#4297) 2026-07-06 14:05:19 +08:00
cxymds 2585855d23 fix(ecstore): hide deleted versioned folder prefixes (#4296) 2026-07-06 14:04:53 +08:00
Zhengchao An 005197140e fix(heal): classify heal_object errors by type instead of "not found" substring (backlog#856) (#4295)
fix(heal): classify heal_object errors by type, not "not found" substring (backlog#856)

The heal page loop classified heal_object errors with a substring test
(`message.contains("not found")`). `StorageError::DiskNotFound` renders as
"disk not found", so an offline drive during a deep-scan heal matched the
"object absent" branch: the object was returned as `Ok(false)`, recorded into
the checkpoint's processed set, counted as a success, and permanently skipped
on resume — the object silently never got healed.

Replace the substring test with a typed classifier over the wrapped
`StorageError`:

- genuine object/version absence (FileNotFound / FileVersionNotFound /
  ObjectNotFound / VersionNotFound) -> Absent (treated as handled);
- transient infrastructure conditions (quorum errors, DiskNotFound,
  VolumeNotFound, SlowDown, OperationCanceled) -> TransientSkip, so the object
  is retried on a later pass instead of recorded as processed;
- everything else -> Failed (recorded as failed).

Deliberately does NOT use `StorageError::is_not_found()`, which lumps
DiskNotFound/VolumeNotFound with object absence — the exact conflation that
caused the bug. Applied to both the deep-scan and normal-scan heal_object call
sites. Adds regression tests for the classifier.

Refs backlog#799 (B7).
2026-07-06 10:45:29 +08:00
Zhengchao An bc8e546636 fix(ci): verify release downloads before use (#4294) 2026-07-06 10:29:49 +08:00
Zhengchao An e68a52ff59 fix: replace while-let loop with for-loop to fix clippy lint on main (#4292) 2026-07-05 23:35:06 +08:00
Zhengchao An a8d8e56478 refactor(ecstore): relocate NamespaceLocking + finalize God-Object split (backlog#822) (#4291) 2026-07-05 23:34:46 +08:00
Zhengchao An f737b39cfc refactor(ecstore): move ObjectIO/ObjectOperations into set_disk::ops::object (backlog#821) (#4290)
P6 of the SetDisks God-Object split (tracking backlog#815, issue backlog#821;
depends on P5 #4288). Relocate the core object read/write hot-path contract
impls out of the set_disk/mod.rs God-Object into their own module home:

- impl ObjectIO for SetDisks (~1,010 lines) -> set_disk/ops/object.rs
- impl ObjectOperations for SetDisks (~1,137 lines) -> set_disk/ops/object.rs
- Registered pub(crate) mod object; under set_disk/ops.

Pure move — zero logic change. Both contracts stay implemented for SetDisks, so
their EcstoreObjectIO / EcstoreObjectOperations associated-type bounds are
unchanged and the contract-compat tests still guard them. Method bodies are
moved verbatim: a whitespace-insensitive token-stream diff of the two impl
blocks (with their #[async_trait::async_trait] attributes) against the pre-move
mod.rs source is byte-identical (5,754 tokens each) — NO visibility widening was
required, because the impls reach SetDisks helpers and the P5 io_primitives
through inherent self./Self:: calls that resolve across modules unchanged. The
two inherent impl SetDisks blocks that sat between the trait impls (lock batch
helpers) remain in mod.rs, verified present exactly once and uncorrupted.

The issue's borrow/Arc-clone-avoidance optimization is intentionally deferred:
it is a perf-sensitive change requiring the #738 benchmark and would risk the
'byte-level behavior unchanged' acceptance; this PR delivers the relocation.

Verification:
- cargo check / clippy -D warnings -p rustfs-ecstore --all-targets: clean
- cargo test -p rustfs-ecstore --lib: 1841 passed, 0 failed
- all five arch guard scripts: pass
- token-stream diff of moved impls vs original: identical (mod.rs diff is a pure
  relocation; ObjectIO/ObjectOperations each defined exactly once post-move)
2026-07-05 23:13:29 +08:00
Zhengchao An 3e4a7c1f6a fix(ecstore): lockstep EC stripe read to fix large-object GET EOF (#4289)
fix(ecstore): lockstep stripe read to stop EC GET desync truncation

On the reconstruction-verifying GET path, ParallelReader::read used a
data-first schedule: it read only `data_shards` readers per stripe and
pulled in a parity reader as a substitute on demand. Because the shard
readers are streaming (advanced only by being read, no seek), a parity
reader first used mid-object was still positioned at its stream start
(block 0) and returned an earlier stripe than the surviving data shards.
Every shard passed its own bitrot hash, yet the set was mutually
misaligned, so decode_data_with_reconstruction_verification correctly
rejected it with "inconsistent read source shards" and the large-object
GET truncated mid-stream (client "unexpected EOF").

Add read_lockstep(): on the verify_reconstruction path, read every live
shard reader once per stripe and wait for all of them, so all readers
advance one block per stripe and stay mutually aligned; any reader that
errors is retired for the rest of the object (a stream that failed
mid-block can no longer be trusted to be aligned). The adaptive
data-first path is unchanged for non-verifying callers (e.g. heal).

Adds a regression test reproducing a data shard that dies partway through
a multi-stripe object; it must still reconstruct byte-exact output.

Refs backlog#832.
2026-07-05 22:58:18 +08:00
Zhengchao An 166679a723 refactor(ecstore): sink write + shared-read primitives into set_disk::core::io_primitives (backlog#820) (#4288)
* refactor(ecstore): sink write/rename/delete primitives into set_disk::core::io_primitives (backlog#820)

P5 step 2 of the SetDisks God-Object split (tracking backlog#815, issue
backlog#820; follows step 1 #4285). Relocate the entire set_disk/write.rs
primitive family into the core/io_primitives.rs module home established by
step 1:

- Module items: dangling_delete_grace() + its env consts, and the
  OrphanDirScan scan-result enum.
- impl SetDisks write/rename/delete primitives shared across mod.rs and the
  ops/ operation families: rename_data, commit_rename_data_dir,
  cleanup_multipart_path, rename_part, eval_disks, write_unique_file_info,
  update_object_meta(_with_opts), delete_if_dangling, delete_prefix,
  scan_orphan_dir, purge_orphan_dir_object, check_write_precondition,
  default_read_quorum, default_write_quorum.
- The dangling_delete_grace unit tests move with their subject.

set_disk/write.rs is removed and 'mod write;' dropped from mod.rs.

Pure move + visibility adjustment, zero logic change. Method bodies are moved
verbatim; pub(super) items are widened to pub(in crate::set_disk) so mod.rs /
ops still reach them (write.rs's super was set_disk; the deeper module needs the
explicit path to preserve identical reach). Callers use inherent self./Self::
calls, so no call sites change.

Verification:
- cargo check / clippy -D warnings -p rustfs-ecstore --all-targets: clean
- cargo test -p rustfs-ecstore --lib: 1841 passed, 0 failed (moved
  dangling_delete_grace tests run as set_disk::core::io_primitives::tests::*)
- all five arch guard scripts: pass
- token-stream diff of moved block vs original write.rs: identical modulo
  visibility tokens (2713 tokens each)

* refactor(ecstore): sink shared read primitives into set_disk::core::io_primitives (backlog#820)

P5 step 3 (final) of the SetDisks God-Object split (tracking backlog#815, issue
backlog#820; follows steps 1 #4285 and 2). Relocate the shared, low-level
metadata/erasure READ PRIMITIVE methods out of set_disk/read.rs into the
core/io_primitives.rs module home, leaving the object-read operation itself in
read.rs.

Moved into a new impl SetDisks block in io_primitives.rs (verbatim bodies):
- read_parts, read_all_fileinfo (+ _observed / _inner / _full_wait /
  _early_stop variants), read_all_xl, load_file_info_versions_exact,
  read_all_raw_file_info, pick_latest_quorum_files_info, read_multiple_files.
- The should_allow_metadata_early_stop free helper (called by the moved
  read_all_fileinfo_observed).

Kept in read.rs: the object-read operation and its private helpers
(read_version_optimized, get_object_fileinfo, get_object_info_and_quorum,
try_get_object_direct_data_shards_with_fileinfo, get_object_with_fileinfo,
get_object_decode_reader_with_fileinfo, build_codec_streaming_part_reader) and
the metadata-cache helpers/tests. read.rs reaches the moved primitives through
the SetDisks core (inherent self./Self:: calls; the sole cross-boundary edge,
get_object_fileinfo -> read_all_fileinfo_observed, is handled by widening that
one method to pub(in crate::set_disk)).

Pure move + visibility adjustment, zero logic change. pub(super) primitives are
widened to pub(in crate::set_disk); the three internal-only fanout variants
(_inner/_full_wait/_early_stop) stay private; load_file_info_versions_exact
stays pub(crate). Method bodies are moved verbatim.

Verification:
- cargo check / clippy -D warnings -p rustfs-ecstore --all-targets: clean
- cargo test -p rustfs-ecstore --lib: 1841 passed, 0 failed
- all five arch guard scripts: pass
- token-stream diff of the three moved regions vs original read.rs: identical
  modulo visibility tokens, the new impl-block scaffolding, and rustfmt
  signature re-wrapping (read.rs diff is pure deletion, zero added lines)
2026-07-05 22:35:05 +08:00
Zhengchao An 23075518c2 docs: update security advisory lessons (#4287) 2026-07-05 22:04:59 +08:00
Zhengchao An e3b51e0a94 test(s3): promote non-multipart get-part coverage (#4286) 2026-07-05 20:40:41 +08:00
houseme 76124423a4 fix: stabilize s3-tests delete key-limit coverage (#4283)
* fix: tighten list handling and s3 test support

* chore: tidy imports and metric updates

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Zhengchao An <anzhengchao@gmail.com>

* fix: address s3tests review follow-ups

---------

Signed-off-by: Zhengchao An <anzhengchao@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-05 20:35:32 +08:00
Zhengchao An 46bb7e52b6 refactor(ecstore): extract read IO primitives to set_disk::core::io_primitives (backlog#820) (#4285)
* refactor(ecstore): extract read IO primitives to set_disk::core::io_primitives (backlog#820)

P5 step 1 of the SetDisks God-Object split (tracking backlog#815, issue
backlog#820). Relocate the module-level low-level read/erasure primitives out of
the 5,898-line set_disk/read.rs into a new core/io_primitives.rs module home:

- Metadata-fanout quorum machinery (MetadataQuorumAccumulator,
  MetadataFanoutDiagnostics/Observation, MetadataEarlyStopDecision,
  MetadataCacheLookup).
- Bitrot reader scheduling/creation (BitrotReaderSetup and the
  create_bitrot_readers_* / schedule / fill helpers).
- Shard-cost classification, read-repair heal dedup, and codec-streaming
  reader helpers.

Pure move + visibility adjustment, zero logic change. The moved block is
byte-identical to the pre-move read.rs source modulo the module header swap
(use super::* -> use super::super::*) and item visibility widening
(module-private / pub(super) -> pub(in crate::set_disk) so read.rs still
reaches them). read.rs's impl SetDisks body and imports are byte-identical to
before; mod.rs only gains 'mod core;' and repoints two
GetCodecStreamingReaderBuildOutcome references to the new path.

Verification:
- cargo check / clippy -D warnings -p rustfs-ecstore --all-targets: clean
- cargo test -p rustfs-ecstore --lib: 1840 passed, 0 failed
- normalized + token-stream diff of moved block vs original: identical modulo
  visibility tokens and rustfmt signature re-wrapping

* fix(arch): allow io_primitives as READ_REPAIR_HEAL_CACHE owner (backlog#820)

The READ_REPAIR_HEAL_CACHE owner-path guard in
check_architecture_migration_rules.sh pinned the cache to
set_disk/read.rs. P5 step 1 relocated the cache (and its accessor
helpers) to set_disk/core/io_primitives.rs, so allow that path too.
Guard intent is unchanged: the cache stays behind the ECStore set-disk
read-owner helpers.

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-05 20:23:49 +08:00
Zhengchao An 9959c828a1 fix(internode): relax keepalive/RPC timeouts to fix large-object GET EOF (#4284)
fix(internode): relax aggressive HTTP/2 keepalive & RPC timeouts to stop large-object GET truncation

The internode gRPC channel used a 3s HTTP/2 keepalive timeout and a 10s
overall RPC timeout. Under high-concurrency large-object reads a saturated
peer's PING ACK is legitimately delayed past 3s, so the whole channel (and
every RPC/stream on it) is torn down as a 'dead peer'. In-flight peer shard
reads then fail mid-object and large GETs truncate after headers+Content-Length
are already sent, surfacing to clients as 'download error: unexpected EOF'.

Reproduced on a 4-node erasure cluster with pure GET-only warp (no concurrent
writes) at 64 concurrency: 10MiB GET ~31 unexpected-EOF / 3min. Raising the
internode keepalive timeout (3s->30s via env) alone cut that to ~7; also raising
the RPC timeout cut it to ~3.

- Raise DEFAULT_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS 3 -> 20
- Raise DEFAULT_INTERNODE_RPC_TIMEOUT_SECS 10 -> 30
- Wire the data-plane rio HttpReader keepalive timeout (was hardcoded 3s) to the
  same env/default so control- and data-plane stay consistent.

Refs backlog#832.
2026-07-05 19:27:23 +08:00
GatewayJ 9cf211930d fix(iam): expand OIDC auth diagnostics (#4281)
* fix(iam): expand OIDC auth diagnostics

* fix(iam): accept RFC3339 OIDC timestamps

* chore(iam): log OIDC policy mapping diagnostics

* chore(iam): log OIDC claim and policy details

* chore(iam): lower OIDC diagnostic log verbosity

* fix(iam): gate OIDC diagnostics behind debug

* chore: update yanked num-bigint lockfile
2026-07-05 18:05:23 +08:00
Zhengchao An 1f51d21d33 refactor(ecstore): move List/Bucket Operations into set_disk::ops (backlog#819) (#4282) 2026-07-05 15:53:16 +08:00
Henry Guo 5f2359de45 test(table-catalog): expand live conformance guide (#4277)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-05 15:41:08 +08:00
唐小鸭 188ab2131d fix(kms): persist Vault Transit key metadata (#4262) 2026-07-05 13:37:59 +08:00
Zhengchao An a164645ff6 refactor(ecstore): move MultipartOperations into set_disk::ops::multipart (backlog#818) (#4279) 2026-07-05 12:22:09 +08:00
houseme db277b17a4 fix: harden GET object performance paths (#4271)
* fix: harden GET object performance paths

* fix: satisfy GET multipart layer guard

* fix: keep v1 list markers S3 compatible

* perf: tighten GET direct-memory decision

* ci: isolate s3tests from scanner workload

* refactor: simplify get object body lifecycle

* fix: satisfy get object clippy
2026-07-05 12:03:22 +08:00
houseme a134717249 chore(deps): update flake.lock (#4273) 2026-07-05 10:32:41 +08:00
Zhengchao An 6b0fcb1180 fix(docker): require explicit root credentials (#4278) 2026-07-05 10:31:28 +08:00
Zhengchao An 26d6c06e03 fix: auto-repair breaking changes from 0271d7aa (#4276) 2026-07-05 10:28:13 +08:00
Zhengchao An ecc7827780 test(ecstore): cover disabled config recovery fallback (#4272) 2026-07-05 10:28:00 +08:00
Zhengchao An 137008b09a fix(s3): hide internal list marker tags (#4275) 2026-07-05 10:27:34 +08:00
Zhengchao An 456b1ce52f fix(s3): hide internal list markers in v1 responses (#4274) 2026-07-05 09:21:49 +08:00
Zhengchao An 0271d7aa2b refactor(ecstore): narrow api facade exports (#4270)
* refactor(ecstore): narrow bucket api facade

* refactor(ecstore): narrow more api facades

* test(ecstore): stabilize multipart cleanup test
2026-07-05 06:06:39 +08:00
houseme 9b69c6d14c fix(s3): preserve metadata listing extensions (#4261)
* chore(deps): update s3s to 0.14.1

* fix(s3): preserve metadata listing extensions

* fix(swift): make version names monotonic

* fix(s3): preserve v1 list pagination markers
2026-07-05 05:03:21 +08:00
Zhengchao An d0a965f2ee fix(lock): harden transient quorum and diagnostics (#4268)
* fix(lock): retry transient remote quorum gaps

* perf(storage): reduce deadlock detector blocking

* fix(storage): satisfy deadlock detector clippy
2026-07-05 03:01:20 +08:00
Zhengchao An a6b3e4f5d6 refactor: use canonical Rust module paths (#4269) 2026-07-05 03:01:14 +08:00
Zhengchao An 55ad8df1c2 refactor(ecstore): move HealOperations into set_disk::ops::heal (backlog#817) (#4267)
P1 of the SetDisks split (tracking backlog#815, depends on P0 backlog#816).

Give the Heal operation family its own module home: relocate set_disk/heal.rs
to set_disk/ops/heal.rs and move the HealOperations storage-api contract impl
beside its inherent helpers. The contract stays implemented for SetDisks so its
associated-type bounds are unchanged (ecstore_contract_compat_test still
covers it).

Method bodies are moved unchanged. The four inherent helpers widen from
pub(super) to pub(in crate::set_disk) to preserve their exact prior visibility
from the deeper module. get_pool_and_set now reads topology through
SetDisksCtx to keep the Heal family aligned with the P0 borrow pattern; the
read is provably identical (ctx.format()/pool_index() alias the core fields).

Runtime behavior is unchanged.
2026-07-05 00:53:09 +08:00
houseme 6dabbaab4d refactor(deps): replace snafu and heal anyhow (#4266) 2026-07-05 00:31:37 +08:00
Zhengchao An 86eafc799b refactor(ecstore): add SetDisks borrow context for God-Object split (backlog#816) (#4265)
P0 of the SetDisks split (tracking backlog#815). Introduce SetDisksCtx<'a>,
a Copy borrow handle over the shared SetDisks core state (topology/config,
disks, locker trio), and validate the pattern by routing the List family's
delete_all through a ListOperations<'a> service unit.

No trait impl is moved and runtime behavior is byte-for-byte unchanged; the
public SetDisks::delete_all entry point is preserved and delegates to the
borrow-based unit.
2026-07-05 00:13:34 +08:00
GatewayJ f730a5b3c5 test(s3tables): clarify durable draft failure test (#4263) 2026-07-04 23:57:56 +08:00
Henry Guo be45b35472 feat(table-catalog): add distributed maintenance scheduling (#4257)
* feat(table-catalog): add distributed maintenance scheduling

* fix(table-catalog): address scheduler review feedback

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-07-04 23:44:08 +08:00
Ramakrishna Chilaka f3147c5f8c perf(metadata): drop per-object heap allocs in internal-key lookups (#4260) 2026-07-04 22:14:07 +08:00
Zhengchao An d0792b87be refactor(lifecycle): extract core rule contracts (#4258) 2026-07-04 20:05:56 +08:00
Zhengchao An e3a8234bc9 fix: 12 P1 reliability/security defects from the full-repo audit (backlog#806) (#4256)
* fix(rio): reject corrupted short compressed/encrypted blocks instead of panicking

DecompressReader::poll_read and DecryptReader::poll_read sliced the block
body with a fixed `[0..16]` index to read the length varint. The body length
comes from an untrusted 24-bit header field, so a corrupted/truncated block
shorter than 16 bytes made the slice panic and crash the request task — a
read-path DoS on GET of tiered/corrupted data.

Pass the whole (arbitrary-length-safe) slice to uvarint and reject a
non-positive or out-of-range length prefix with InvalidData. Adds a repro
test for each reader; all existing round-trip tests still pass.

Refs rustfs/backlog#812

* fix(utils): close SSRF bypass via IPv4-mapped IPv6 addresses

validate_outbound_ip branched on the IpAddr variant, and the V6 branch's
is_loopback/is_unicast_link_local/is_unique_local checks never inspect the
embedded IPv4 of an IPv4-mapped address (::ffff:a.b.c.d). The metadata guard
also only matched the plain V4 169.254.169.254. So ::ffff:127.0.0.1,
::ffff:10.0.0.5 and ::ffff:169.254.169.254 all passed the outbound guard,
letting an attacker reach loopback/private/metadata endpoints.

Normalize IPv4-mapped IPv6 to its embedded IPv4 (via to_ipv4_mapped, which
matches only the true mapped form) before classification. Adds reject tests
for mapped loopback/private/metadata and an allow test for public IPv6.

Refs rustfs/backlog#813

* fix(ecstore): streaming last-part loss, GCS tier Range/remove, stat_all_dirs alignment

Four confirmed data-reliability defects:

- put_object_multipart_stream: the CompleteMultipartUpload part-collection loop
  used exclusive `1..total_parts_count`, dropping the final part (and collecting
  zero parts for a single-part object) — silently truncating the completed object.
  Extracted collect_complete_parts (1..=total_parts_count) with unit tests.
- GCS warm backend get() ignored the requested byte range, returning the whole
  object for a Range GET; now applies ReadRange::segment like the other backends.
- GCS warm backend remove() was an empty stub, so deleting a tiered object left
  it on GCS forever; now deletes via StorageControl (added a control-plane client),
  and in_use() actually lists (prefix-scoped) instead of always returning false.
- stat_all_dirs skipped None disk slots and dropped JoinErrors, returning a
  compressed, misaligned error vector; heal_object_dir then zipped it against the
  full disks array and could make_volume on the WRONG disk. Now returns one
  index-aligned entry per slot (None -> DiskNotFound), and heal no longer
  pre-fills the drive report (which would double it). Added an alignment test.

Refs rustfs/backlog#807

* fix(kms): stop Vault backend from destroying/reviving keys on failure

Two confirmed key-safety defects in the Vault KV2 backend:

- get_key_material() 'self-healed' a decrypt or wrong-length failure by minting a
  fresh random master key and overwriting the stored value. That destroys the
  original key material, making every DEK ever wrapped by it permanently
  undecryptable. Decryption must never mutate the stored key: both branches now
  return a cryptographic_error instead. (The empty-material bootstrap path, which
  only fills a never-initialized key, is intentionally left intact.)
- cancel_key_deletion() reset key_state to Enabled only in the returned response
  and never persisted it, so the key stayed PendingDeletion in storage and would
  still be reaped. It now writes the state back via update_key_metadata_in_storage
  and fails the request if the write fails.

Adds ignored (Vault-requiring) integration tests documenting both behaviours.

The third item (VaultTransit key state only in memory -> revived as Enabled after
restart) is deferred: a fail-closed guard would break restart availability for all
transit keys; the correct fix needs a persistent metadata store + Vault integration
testing. Tracked in rustfs/backlog#808.

Refs rustfs/backlog#808

* fix(admin): clamp STS AssumeRole duration; persist ImportBucketMetadata to disk

Two confirmed admin-API defects:

- Standard AssumeRole used the raw client-supplied DurationSeconds with no upper
  bound, so a caller could mint near-permanent temporary credentials. Clamp it to
  the AWS/MinIO STS window [900, 43200] (with 0 -> default 3600) via a shared
  clamp_assume_role_duration helper, and build the exp claim with saturating_add.
  This matches the existing AssumeRoleWithWebIdentity path.
- ImportBucketMetadata only mutated an in-memory map and returned 200, silently
  dropping every imported config. It now persists each non-empty config via
  metadata_sys::update (which merges onto existing on-disk metadata) and returns
  InternalError if a write fails. Mapping extracted to imported_configs_to_persist
  with unit tests.

Refs rustfs/backlog#809

* fix(heal): enqueue displacing request in release builds

push_displacing_lower_priority folded the real enqueue call into
debug_assert_eq!(self.push(request), Accepted). In release builds
(debug_assertions off) the whole macro — including its argument — is compiled
out, so after evicting a lower-priority queued item the new high-priority
request was silently dropped and never healed. Hoist self.push(request) out of
the assertion so the side effect runs in all builds. Adds a --release regression
test.

Refs rustfs/backlog#811

* fix(iam): propagate real delete_policy backend errors instead of swallowing them

delete_policy's is_from_notify path had its error handling inverted: a real
backend failure (disk IO / insufficient quorum) evicted the cache and returned
Ok(()), reporting a phantom success while policy.json survived on disk (to be
reloaded on the next full IAM reload); NoSuchPolicy — which should be idempotent
success — returned Err. Propagate real errors and let NoSuchPolicy fall through
to the idempotent cache-evict + Ok, matching delete_user / the notification
handler in the same file. Adds a backend-error-injection regression test.

Refs rustfs/backlog#810

* fix(utils): also normalize IPv4-compatible IPv6 in the SSRF guard

The initial fix only unwrapped IPv4-mapped (::ffff:a.b.c.d) addresses; the
deprecated IPv4-compatible form (::a.b.c.d, e.g. ::127.0.0.1 / ::169.254.169.254)
still bypassed the guard. Reject pure-IPv6 specials (::, ::1, fe80::, fc00::)
first, then normalize BOTH embedded-IPv4 forms before the IPv4 rules. Adds tests
for compatible-form loopback/metadata and confirms ::1 / :: stay rejected.

Found by adversarial review of the initial fix. Refs rustfs/backlog#813

* fix(ecstore): fix the same last-part loss in the parallel streaming path

put_object_multipart_stream_parallel had the identical off-by-one
(1..total_parts_count) that truncated the last part / produced zero parts for a
single-part upload — reachable when concurrent stream parts are enabled. Reuse
collect_complete_parts, which now returns an error instead of panicking on a gap
in the parts map. Adds a missing-part error test.

Found by adversarial review of the initial fix. Refs rustfs/backlog#807

* fix(kms): local backend must preserve key material on status change

LocalKmsClient (the default KMS backend) regenerated the master key material on
enable_key/disable_key/schedule_key_deletion/cancel_key_deletion — a pure status
change. A single disable+enable cycle therefore destroyed the original key,
making every DEK ever wrapped by it permanently undecryptable (silent data loss,
no network needed). Preserve the existing material via get_key_material and
re-save with only the status changed. Adds a hermetic regression test that wraps
a DEK, cycles all four status methods, and asserts the DEK still decrypts.

Found by adversarial review of the Vault fix. Refs rustfs/backlog#808

* test(rio): cover the length-prefix guard; correct its comment

Add a DecompressReader test that feeds an unterminated length varint so uvarint
returns 0 and the new guard (not the downstream codec) produces the InvalidData
error, and reword the guard comment which overclaimed that the > len bound
prevents a reachable panic (it is belt-and-suspenders). No behavior change.

Found by adversarial review. Refs rustfs/backlog#812

* test(rio): build test block headers via vec! to satisfy clippy

The new corrupted-block tests built the header with Vec::new() + repeated push,
tripping clippy::vec_init_then_push (-D warnings in CI). Construct the fixed
header bytes with vec![] instead. No behavior change.

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-04 14:24:02 +08:00
Ramakrishna Chilaka eb85607e37 fix(s3): allow CopyObject to restore a historical object version (#4250) 2026-07-04 10:36:42 +08:00
Zhengchao An 123552d32b refactor(replication): own utility wire contracts (#4255) 2026-07-04 08:00:37 +08:00
Zhengchao An 3d4fb532e5 refactor(replication): own filemeta wire contracts (#4254) 2026-07-04 06:39:54 +08:00
Zhengchao An 09fcacbe2b fix(e2e): align KMS test helpers with server API field names and env requirements (#4252) 2026-07-04 04:18:52 +08:00
Zhengchao An 125c228832 refactor(replication): own delete DTO contracts (#4253) 2026-07-04 04:00:27 +08:00
Zhengchao An 8a0617865b refactor(replication): route app storage through ecstore (#4251) 2026-07-04 02:00:28 +08:00
Henry Guo 2214f67fba feat(table-catalog): deepen row-level maintenance planning (#4249)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-03 23:49:40 +08:00
Zhengchao An 4883d3eb50 refactor(replication): route runtime facades through ecstore (#4248) 2026-07-03 23:49:24 +08:00
Zhengchao An 7cc470cb08 refactor(replication): isolate ecstore boundary imports (#4247) 2026-07-03 23:18:27 +08:00
Zhengchao An 918fd29711 fix(object-capacity,rio): capacity refresh safety + internode HTTP hardening (#4246)
* fix(object-capacity): stop cancelled/remote-disk refreshes from corrupting capacity

Two independent capacity-refresh bugs (backlog rustfs/backlog#805):

- refresh_or_join set the singleflight `running` flag then awaited the
  refresh future with no drop guard. When the admin request that became
  leader was cancelled (client disconnect) mid-await, `running` stayed true
  forever: joiners blocked indefinitely and the 120s scheduled refresh could
  never start again. catch_unwind covered panics but not cancellation. A
  RefreshLeaderGuard now resets the state and publishes an error on drop.

- capacity_disk_refs mapped the cluster-wide storage_info disk list without
  filtering non-local disks, so admin-triggered refreshes ran a local WalkDir
  over remote disks' drive_path. On multi-node clusters this double-counted
  local bytes (shared mount layout) or hit NotFound (per-node layouts), and
  poisoned the per-disk cache the scheduled local-only refresh depends on,
  making the cached total oscillate. Filter to local disks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(rio): harden internode HTTP client build, cache, and PUT body integrity

Follow-ups from the internode HTTP review (backlog rustfs/backlog#805):

- handle_put_file accepted a truncated body as success: a HttpWriter dropped
  mid-stream closes the chunked body cleanly, indistinguishable from EOF, and
  the server never compared bytes copied against the declared size. Reject
  size mismatches on the create path (append/unknown-size writes send size<=0
  and are exempt).

- build_http_client used `.expect()` on ClientBuilder::build(), which runs
  lazily on the first request and on every TLS generation bump (cert
  rotation), so a build failure panicked a serving task. It now returns an
  io::Error; get_http_client falls back to the previous TLS generation when a
  rebuild fails instead of failing the request.

- CLIENT_CACHE was a tokio::Mutex taken on every stream open (data_shards
  times per GET). Replaced with arc_swap::ArcSwapOption for lock-free reads on
  the hot path; the generation-monotonic replacement guard is preserved via rcu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 23:12:49 +08:00
majinghe 4542d4060f ci: replace ubicloud runner with self host runner on k8s (#4245) 2026-07-03 23:12:18 +08:00
houseme 63b6954ae3 chore(deps): update bytesize and s3s (#4242)
* chore(deps): update bytesize and s3s

* fix: keep s3s on rustfs fork for minio extensions

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-03 23:11:52 +08:00
Zhengchao An d19ee6c51c refactor(replication): isolate storage api contracts (#4244) 2026-07-03 22:48:21 +08:00
Zhengchao An 24e88a2cf4 refactor(replication): isolate filemeta facade contracts (#4243) 2026-07-03 22:20:46 +08:00
Zhengchao An c9bf097595 fix(storage): enrich versioning fallback log (#4241) 2026-07-03 22:11:05 +08:00
Zhengchao An 6839e57c96 refactor(replication): isolate storage api contracts (#4240)
* refactor(replication): isolate storage api contracts

* docs(replication): record storage api contract boundary
2026-07-03 22:10:16 +08:00
Zhengchao An 6a81445a96 refactor(replication): isolate ecstore owner contracts (#4239) 2026-07-03 21:42:45 +08:00
Zhengchao An 92402a3bde perf(get,heal): fix GET hot-path overhead and heal checkpoint scaling (#4237)
perf(get,heal): land verified fixes for backlog #800-#804

Five fixes from the GET-path performance audit and scanner/heal
completeness audit (rustfs/backlog#800..#804), each verified locally:

- backlog#800 (heal checkpoint O(N^2)): ResumeCheckpoint object sets are
  now HashSet (Vec::contains was O(n) per healed object; 1.5ms at n=1M
  vs 11ns measured), and per-object checkpoint/resume-state persistence
  is batched (1000 mutations / 5s) instead of rewriting the whole file
  per object. complete_page() prunes the sets at page boundaries so
  memory stays bounded; positions still persist unconditionally and
  legacy Vec-format checkpoints still deserialize.

- backlog#801 (DiskInfo.healing never set): erasure-set heal now writes
  a healing marker (.rustfs.sys/healing.bin) on the disks it rebuilds
  (endpoints plumbed via HealRequest/HealTask.heal_endpoints from the
  auto disk scanner) and clears it on success. LocalDisk::disk_info
  surfaces the marker, so scanner heal coordination, lock selection and
  admin/metrics healing counts see the rebuild.

- backlog#802 (cache probe after data read): new GetObjectBodyCacheHook
  in ecstore lets the app-layer object data cache serve the body inside
  get_object_reader, after metadata quorum resolution (etag known) but
  before the erasure shard read/decode. Previously a hit still paid the
  full disk read. Hook is None/no-op when the cache is disabled.

- backlog#803 (GET hot-path redundant work): ObjectInfo is cloned for
  event notification only when an event will actually be built (GET and
  HEAD paths; events are currently suppressed so the clone was pure
  waste); get_opts/put_opts/del_opts resolve bucket versioning with one
  metadata-sys lookup instead of two; skip_verify_bitrot and
  get_lock_acquire_timeout env reads are cached via OnceLock; the
  io-priority metric is no longer double-counted; GetObject input fields
  are cloned selectively instead of cloning the whole input.

- backlog#804 (disk permit starvation): the disk-read permit wait is now
  bounded (RUSTFS_OBJECT_DISK_PERMIT_WAIT_TIMEOUT, default 5s, 0 =
  previous unbounded behavior); on timeout the GET proceeds without a
  permit and the bypass is counted. DiskReadPermitReader also releases
  the permit at body EOF instead of holding it until the client drops
  the stream.

Verification: make pre-commit; cargo clippy -D warnings on the four
changed crates; full rustfs lib suite (2096 tests) green; rustfs-heal
lib suite green with new unit tests for checkpoint pruning/legacy
format/throttle, permit EOF release, and the cache hook (hit + SSE
skip). The heal_integration_test and one set_disk listing test fail
identically on unmodified main (pre-existing global-state ordering
flakes, verified via git stash A/B).

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-03 21:42:27 +08:00
Zhengchao An 769549dd2c refactor(replication): isolate status contract boundaries (#4236) 2026-07-03 19:40:49 +08:00
Zhengchao An 7001e53546 refactor(replication): isolate stats and app contract boundaries (#4235)
* refactor(replication): isolate stats boundary adapters

* refactor(replication): route app contracts through storage boundary
2026-07-03 18:48:26 +08:00
houseme eebd16d8a4 feat(cache): add object data cache engine and app flow (#4187)
* feat(cache): add object data cache engine

* feat(cache): wire app-layer object cache flow

* refactor(cache): streamline app-layer cache flow

* refactor(cache): tighten cache flow internals

* refactor: address final clippy cleanup

* chore(deps): update quick-xml to 0.41.0

* feat(cache): wire object data cache env config

* fix(cache): gate materialize fill by cache plan

* chore(cache): add object data cache benchmark gate

* fix(cache): guard object cache fill size mismatches

* refactor(cache): streamline object cache body planning

* fix(cache): align object cache rollout config

* test(cache): cover buffered object cache benchmark

* test(cache): isolate object cache benchmark metrics

* test(cache): mark materialize rollout experimental

* test(cache): tighten object cache benchmark gate

* fix(cache): address review findings for object data cache

- singleflight: clean up leader entry on cancellation (Drop impl) so a dropped GET future can no longer wedge all subsequent fills for the same key; switch the fill map to a std Mutex and add a regression test

- adapter: honor RUSTFS_OBJECT_DATA_CACHE_ENABLE=true by defaulting to hit_only when no explicit mode is set (explicit mode still wins)

- planner: treat nil version UUIDs as "no value" per repo convention so unversioned objects key under the canonical "null" instead of fragmenting the key space

- multipart: invalidate the object cache on the quota-exceeded rollback delete after complete-multipart, closing a stale-cache window

- layering: move the disabled-cache fallback into app::context and drop the new infra->app layer-dependency baseline entry

* fix(cache): close invalidation races and drop full-cache scan on writes

- index: make identity-index insert/remove/prune atomic via starshard compute_if_present/compute_if_absent so concurrent fills can no longer drop each other's keys (lost keys made entries unreachable to invalidation until TTL); add a concurrency regression test

- fill: register the key in the identity index before the entry becomes visible in the cache and re-check the index afterwards, undoing the fill when an invalidation raced in between (new skipped_invalidation_race fill result)

- invalidate: with the index now authoritative, remove the full-cache iter() fallback that made every PUT/DELETE of a never-cached object O(total cache entries) (two scans per PUT, 2N per batch delete)

- materialize-fill: fail the GET instead of falling back to the partially consumed stream after a mid-read error (the fallback would send a body missing its prefix under a full-length Content-Length), and log the same size-mismatch warning as the sibling buffering paths

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(storage): fix media-dependent buffer clamp expectation

test_concurrency_manager_multi_factor_strategy_buffer_clamp asserted media_cap.min(MI_B), but the implementation's final safety clamp is [32KiB, media_cap.max(MI_B)] — deliberately so a media cap above 1MiB (NVMe's 2MiB default) stays effective. The test only passed on machines detected as SSD/Unknown (cap == 1MiB) and failed on NVMe-backed CI runners with 2MiB != 1MiB. Assert the media cap itself, which is what the strategy actually guarantees on every environment.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(storage): format buffer clamp assertion

* chore(logging): update tier guardrail path

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-03 18:11:14 +08:00
houseme 25d80d7c60 feat(storage): harden internode data-path controls (#4224)
* fix(rio): propagate http writer shutdown errors

* fix(ecstore): unify remote lock rpc deadlines

* fix(storage): reject corrupt read multiple payloads

* feat(rio): add internode http tuning profiles

* feat(metrics): add internode baseline signals

* feat(ecstore): observe shard locality topology

* feat(ecstore): gate shard locality scheduling

* feat(ecstore): gate batch read version rpc

* feat(ecstore): observe batch processor adaptation

* feat(ecstore): gate batch processor observation

* docs: add get benchmark regression analysis

* docs: add issue 797 execution plan status

* fix(ecstore): require explicit batch rpc support

* fix(ecstore): honor documented batch read gate

* fix(ecstore): keep batch read gate stable per call

* chore: update workspace dependencies

* feat(ecstore): log batch read gate decisions

* feat(ecstore): count batch read gate decisions

* test(issue-797): add local internode A/B runner

* test(rio): fix tuning profile spelling fixture

* fix(protocols): adapt sftp channel open callbacks

* fix(metrics): wrap batch processor observation args

* chore(docs): keep issue notes local only

* fix(storage): address internode review feedback

* fix(storage): address internode data-path review findings

- Run the BatchReadVersion auto-mode unary fallback outside the batch
  RPC deadline so each read_version keeps its own per-op timeout and
  health accounting instead of racing the whole batch against one
  drive timeout.
- Cap adaptive batch-processor concurrency growth at a hard multiple
  of the configured baseline so sustained fast batches cannot ratchet
  past the configured limit.
- Parse RUSTFS_INTERNODE_HTTP_* tuning, RUSTFS_BATCH_PROCESSOR_ADAPTIVE,
  and RUSTFS_METADATA_BATCH_READ once per process instead of re-reading
  the environment on hot paths.
- Skip shard read-cost collection in observe mode when stage metrics
  are disabled, and cache the local endpoint host list instead of
  rebuilding it on every read.
- Allow --warp-extra-args values starting with -- and drop the unused
  warp_hosts_csv helper in the issue-797 A/B runner.

* fix(storage): address internode data-path review findings

- Run the BatchReadVersion auto-mode unary fallback outside the batch
  RPC deadline so each read_version keeps its own per-op timeout and
  health accounting instead of racing the whole batch against one
  drive timeout.
- Cap adaptive batch-processor concurrency growth at a hard multiple
  of the configured baseline so sustained fast batches cannot ratchet
  past the configured limit.
- Parse RUSTFS_INTERNODE_HTTP_* tuning, RUSTFS_BATCH_PROCESSOR_ADAPTIVE,
  and RUSTFS_METADATA_BATCH_READ once per process instead of re-reading
  the environment on hot paths.
- Skip shard read-cost collection in observe mode when stage metrics
  are disabled, and cache the local endpoint host list instead of
  rebuilding it on every read.
- Allow --warp-extra-args values starting with -- and drop the unused
  warp_hosts_csv helper in the issue-797 A/B runner.

Co-Authored-By: heihutu<heihutu@gmail.com>

* fix(storage): align buffer clamp test with media cap

* fix(ecstore): release optimized read locks before streaming

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-07-03 17:08:15 +08:00
GatewayJ a9ac5f578d fix(s3tables): harden durable-strong refresh (#4230) 2026-07-03 14:56:30 +08:00
GatewayJ b1582b3391 fix(iam): improve OIDC token exchange diagnostics (#4232) 2026-07-03 14:15:58 +08:00
majinghe 6fd642253a ci: change runner from ubicloud to k8s (#4231) 2026-07-03 13:27:53 +08:00
Zhengchao An 1c36c6b208 fix(config): recover from corrupt server config instead of crashing (#4229) 2026-07-03 13:08:04 +08:00
Zhengchao An ba1f162f0f fix(server): normalize leading and duplicate slashes in object keys (#4228) 2026-07-03 12:50:57 +08:00
Zhengchao An cec3285d23 fix(bucket): notify peers on bucket versioning and metadata config updates (#4227) 2026-07-03 12:40:53 +08:00
Zhengchao An d2c100fd3e fix(filemeta): guard metacache decoding against corrupt length prefixes (#4226) 2026-07-03 12:27:35 +08:00
Zhengchao An c59d4e6f89 fix(ecstore): reject Windows-unreadable object name segments (#4225) 2026-07-03 12:21:04 +08:00
Zhengchao An 19c21b3522 fix(storage): make acknowledged writes durable across power loss (#4221) 2026-07-03 11:57:05 +08:00
Zhengchao An 7329816ed7 test(e2e): add disk fault injection harness and reliability tests (#4223)
Add crates/e2e_test/src/chaos.rs with in-process fault-injection
primitives for a single-node multi-disk RustFS server: take a disk
offline (rename to <dir>.offline), bring it back online, replace a
disk with a fresh empty directory, corrupt an object's erasure shard
(part.* byte flips, xl.meta untouched), and SIGKILL/restart the server
with the same volumes and port.

Add reliability tests on a 4-disk (EC 2+2) topology, verified via
sha256 manifests recorded at write time:

- degraded read/write with one disk offline (incl. multipart object)
- bitrot read-through with two corrupted shards per object
- fresh-disk replacement healed via admin deep heal after a SIGKILL
  restart

Also reuse the shared signed_admin_post helper from chaos.rs in the
heal regression suite instead of a duplicated local copy.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:37:32 +08:00
Zhengchao An cf056b39e3 fix(core-storage): fix critical correctness defects from core-storage reliability audit (#4222)
* fix(core-storage): fix critical correctness defects from core-storage audit

Fixes verified defects found in a deep audit of the core storage path
(erasure coding, disk persistence, quorum, heal, replication resync):

- ecstore/disk: rewrite live xl.meta atomically (temp+rename) in
  delete_versions_internal and write_metadata instead of in-place
  truncate, which exposed torn metadata to concurrent readers and
  crashes on the DeleteObjects hot path
- ecstore/erasure: allow heal to reconstruct from exactly data_shards
  bitrot-verified sources; requiring data_shards+1 made objects
  permanently unhealable after losing parity_shards disks
- ecstore/set_disk: direct-memory inline GET applied the erasure
  distribution permutation twice (shuffled inputs re-indexed through
  distribution), concatenating wrong shards into the response body in
  degraded reads; collect from canonical disk-ordered inputs
- ecstore/set_disk: heal now preserves the committed inline layout
  instead of recomputing it with a hardcoded unversioned threshold,
  which split quorum identity of healed replicas and caused endless
  re-heal churn
- ecstore/replication: resync results channel switched from
  broadcast(1) to mpsc; a lagged broadcast receiver ended the stats
  collector and every subsequent failure went uncounted, letting
  failed resyncs be marked completed
- ecstore/replication: ignore an empty persisted resync checkpoint;
  resuming with one skipped every object and marked the resync
  completed without replicating anything
- ecstore/replication: fix inverted not-found error classification in
  replicate_object/replicate_delete logging paths
- ecstore/erasure: guard decode paths against zero block_size or
  data_shards from corrupt on-disk metadata (divide-by-zero panic)
- ecstore/disk: os::read_dir no longer consumes the entry limit on
  entries it does not return (is_empty_dir misjudgment); create_file
  opens with O_TRUNC to avoid stale trailing bytes
- filemeta: treat Some(nil) version id as a null version in
  matches_not_strict; disk-loaded headers never store None, so the
  mod_time quorum guard for unversioned overwrites never fired and an
  interrupted overwrite could displace the committed version in merge
- filemeta: fix msgpack skip lengths for fixext (missed the ext type
  byte) and ext16/32 (over-skipped) unknown fields
- filemeta: return FileCorrupt instead of usize underflow when
  xl.meta is truncated inside the CRC trailer
- filemeta: surface delete-marker insertion failure in delete_version
  instead of reporting success when the data dir is shared

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(replication): drop duplicate cfg(test) etag import from boundary module

The test module already imports content_matches_by_etag locally, so the
top-level cfg(test) import is unused under -D warnings and fails clippy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:37:02 +08:00
Zhengchao An aecac5c0ae refactor(replication): isolate object decision contracts (#4219) 2026-07-03 11:36:14 +08:00
Zhengchao An b234f9cf98 fix: auto-repair breaking changes from 48b70f6e (#4218)
fix(ecstore): remove unused cfg(test) import of content_matches_by_etag

The #[cfg(test)] use of content_matches_by_etag at module level is
redundant — the test module already imports it locally at line 369.
This was causing a clippy unused-imports error under -D warnings.
2026-07-03 11:35:51 +08:00
Zhengchao An e9af89b40d fix(ecstore): purge orphan empty-directory trees on folder delete (#4220)
fix(ecstore): purge orphan empty-directory trees on folder delete (#4189)

Empty "phantom" folders — on-disk directory trees with no xl.meta
anywhere — show up in listings as common prefixes but cannot be removed
by any S3 call: DeleteObject on the folder key encodes it to __XLDIR__,
finds no metadata, and returns NotFound, which the API layer masks as a
fake 204 while the tree survives on disk.

Make handle_delete_object attempt a guarded purge when a dir-object key
resolves to NotFound:

- sweep every erasure set of every pool (orphan fragments can sit on
  any set, left behind by whichever sets stored the deleted children)
- per set, refuse if ANY disk holds a regular file under the prefix,
  so degraded/healable objects are never destroyed
- remove empty directories children-first with non-recursive deletes;
  a racing PutObject makes the rmdir fail with DirectoryNotEmpty and
  the entry is skipped

This state is unrepresentable on AWS S3 (CommonPrefixes derive only
from real keys; DeleteObject is an idempotent 204), so the purge moves
visible behavior closer to S3. It deliberately goes beyond MinIO, whose
DeleteObject leaves these trees stranded and whose heal only removes
dangling (minority-presence) dirs — a long-standing complaint
(minio/minio#15257, #19516, #18444).

Also move the test-only content_matches_by_etag import in
replication_target_boundary.rs into the cfg(test) module; it failed
cargo clippy -D warnings and blocked the pre-PR gate.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:34:43 +08:00
Zhengchao An fb0e2d6d8f refactor(plugins): harden target-plugin system and admin plugin contract (#4217) 2026-07-03 10:18:21 +08:00
Zhengchao An c260a2f20f refactor(replication): isolate queue boundary adapters (#4216) 2026-07-03 09:43:31 +08:00
Zhengchao An 48b70f6e4f refactor(replication): isolate resync boundary adapters (#4215) 2026-07-03 08:59:42 +08:00
Zhengchao An 38cdbf3939 fix(ecstore): scope replication boundary etag import to test module (#4214)
content_matches_by_etag is only used inside the #[cfg(test)] module, so
the lib-scope import from #4211 fails cargo clippy -D warnings on every
non-test build.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:58:56 +08:00
Zhengchao An 65849740f5 fix(test): deflake capacity dirty scope test global state contamination (#4213)
fix(test): use subset check for capacity dirty scope test to handle global state contamination

The data_movement_put_object_marks_dirty_disks_for_capacity_manager test
asserts exact dirty disk count, but the global dirty scope registry is
process-wide. Other concurrent tests can add entries between the drain
and the assertion, causing flaky failures (8 instead of expected 4).

Use subset assertion to verify the expected disks are present without
being sensitive to entries from other tests.
2026-07-03 08:19:32 +08:00
Zhengchao An d1a2dec18e fix(object-capacity): resolve clippy type_complexity lint in test code (#4212)
fix(object-capacity): simplify config getter test type
2026-07-03 08:18:59 +08:00
Zhengchao An 31df11beb7 refactor(replication): isolate multipart and target adapters (#4211)
* refactor(replication): isolate multipart part planning

* refactor(replication): isolate target head adapters
2026-07-03 08:06:17 +08:00
Zhengchao An 8ac6bfcef6 fix: auto-repair clippy type_complexity failure from 83c32a7a (#4210) 2026-07-03 06:35:11 +08:00
Zhengchao An 2d4f5bfb8a fix: suppress clippy type_complexity lint in capacity_manager test helper (#4209) 2026-07-03 06:34:59 +08:00
cxymds 83c32a7a40 fix(lifecycle): avoid blocking expiry enqueue (#4197) 2026-07-03 00:45:34 +08:00
Henry Guo 782e73ca92 feat(table-catalog): add maintenance audit timeline (#4207)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-03 00:36:17 +08:00
Zhengchao An 9dfeffc4c1 test: prune redundant cases and add high-risk coverage across crates (#4208)
Remove or consolidate 57 test cases that cannot catch regressions
(literal-constant asserts, construct-then-assert, derived-serde
round-trips, near-duplicate env/getter matrices) in common, config,
iam, madmin, and object-capacity, keeping all wire-format and
error-path guards. Add 13 tests for previously uncovered high-risk
behavior: filemeta version-sort determinism and merge resilience to
garbage headers, zip extraction path-traversal rejection and exact
limit boundaries, JWT tampered-signature rejection, and the bytes
variant of dual-key (rustfs/minio) metadata fallback and precedence.

Test-only change; no production code touched.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:35:37 +08:00
Zhengchao An dd6b5525e4 fix(ecstore): remove reachable panics in tiering, replication, and heal paths (#4205)
* fix(ecstore): remove reachable panics in tiering, replication, and heal paths

- Parse x-amz-expiration leniently in tier PUT responses; any lifecycle
  rule on the remote tier bucket returns an RFC1123 date that the previous
  ISO8601 unwrap turned into a panic of the ILM transition worker
- Skip invalid user-metadata header values (with a warning) when building
  tier and replication PUT headers instead of panicking on non-ASCII input
- Heal: tolerate absent data_dir for delete markers and remote objects
- transition_object: don't unwrap version_id on unversioned buckets when
  recording partial writes for offline disks
- Admin server info: use port_or_known_default() so default-port (80/443)
  endpoints don't panic is_server_resolvable
- Tier ListObjectsV2 client: decode response body with from_utf8_lossy
- walk_internal: log merge_entry_channels errors instead of dropping them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ecstore): fail heal explicitly when data_dir is missing

Address review feedback: unwrap_or_default() silently substituted a nil
UUID when latest metadata lacked data_dir. Delete markers and remote
objects legitimately have no data_dir and skip the data-heal block, but
for a regular object a missing data_dir means corrupt metadata — return
FileCorrupt with a descriptive log instead of building part paths under
a nil UUID directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:14:11 +08:00
Zhengchao An 3779e674a8 ci(s3-tests): add mint workflow, multi-node sweep, and API coverage tool (#4206)
Follow-ups to the compatibility harness rework:

- Weekly full sweep now runs as a matrix over both topologies: single
  node and the 4-node distributed cluster. Manual dispatch keeps the
  test-mode input.
- New mint workflow (weekly + dispatch) runs MinIO Mint against RustFS:
  functional suites of real client SDKs and tools (awscli, mc,
  aws-sdk-*, minio-*, s3cmd, ...), catching client-specific signing and
  streaming edge cases that boto3-only ceph/s3-tests cannot. Report-only
  for test failures with a per-suite summary table; fails only when mint
  produces no results.
- New scripts/s3-tests/api_coverage.py quantifies API surface coverage
  by diffing the s3s S3 trait (at the Cargo.toml pinned revision)
  against the methods overridden in impl S3 for FS, distinguishing
  NotImplemented defaults from delegating defaults (e.g. post_object).
  Current state: 76/101 operations covered (75 overridden, 1 delegated).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:00:34 +08:00
Zhengchao An 3eeb459ece ci(s3-tests): pin upstream suite, add weekly full sweep and compat report (#4204)
Reworks the S3 compatibility test harness for reproducibility and faster
feedback:

- Pin ceph/s3-tests to a fixed commit (S3TESTS_REV, fetch-by-SHA) so the
  449-test PR gate is reproducible; previously every run cloned upstream
  master, letting test renames or assertion changes break CI silently.
- PR gate (ci.yml s3-implemented-tests): MAXFAIL=0 + XDIST=4 so a single
  CI round reports every failure in parallel instead of stopping at the
  first one serially.
- Add TEST_SCOPE=all to run.sh to run the entire upstream suite, and
  report_compat.py to diff junit results against the classification
  lists (regressions, promotion candidates, unclassified tests).
- Rewrite e2e-s3tests.yml: delegate execution to run.sh (single source
  of truth; also fixes the broken config generation that left S3_PORT
  empty), add a weekly scheduled full sweep that fails only on whitelist
  regressions, and fix the multi-node topology to a real distributed
  cluster (endpoint-style RUSTFS_VOLUMES) instead of four independent
  single-node stores behind a load balancer.
- Docs: rewrite stale .github/s3tests/README.md (marker-era strategy),
  update scripts/s3-tests/README.md, fix dead build_testexpr.sh
  reference in S3_COMPAT_WORKFLOW.md, drop legacy non_standard_tests.txt.

All 747 classified test names verified present at the pinned revision;
13 upstream tests are currently unclassified and will surface in the
first full-sweep report.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:43:19 +08:00
Henry Guo 31c026dd4f feat(table-catalog): add maintenance quarantine operations (#4201)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-02 23:30:44 +08:00
Zhengchao An d1db9a10cd chore(docs): refresh agent docs, guard doc paths, archive plans (#4203)
Agent-instruction and architecture docs had drifted from the code:

- CLAUDE.md: slim to commands + pointers; fix wrong claim that
  `make pre-commit` is the full pre-PR gate (that is `make pre-pr`);
  drop stale pre-#3929 file paths and merged bug narratives
- AGENTS.md: drop dead `rust-refactor-helper` skill rule and the
  hand-maintained (already stale) scoped-AGENTS index; link
  architecture docs from Sources of Truth
- .github/AGENTS.md: replace the outdated copied CI command matrix
  with a pointer to ci.yml
- crates/AGENTS.md: merge duplicated Testing sections
- ARCHITECTURE.md: resolve the utils->config contradiction (edges are
  removed), mark volatile counts as snapshots, fix a bad path
- docs/architecture: add README router; move one-shot plans/trackers
  (rebalance-decommission phases, migration-progress ledger, PR
  template) to docs/superpowers/plans with archive headers; fix stale
  source paths in kept inventories (core/sets.rs, core/pools.rs,
  store/mod.rs, startup_* split from #3671)
- docs/operations/tier-ilm-debugging.md: extracted tier debugging
  playbook with corrected paths
- scripts/check_doc_paths.sh: new guard failing pre-commit/pre-pr when
  instruction/architecture docs reference nonexistent file paths
- .claude/skills: add tier-debug and arch-checks repo skills;
  .gitignore now keeps .claude/skills and docs/operations committable

Verification: ./scripts/check_doc_paths.sh,
./scripts/check_architecture_migration_rules.sh (both pass)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:30:13 +08:00
Zhengchao An 29899f4731 refactor(replication): isolate queue backpressure decisions (#4202) 2026-07-02 23:28:45 +08:00
Zhengchao An a9894843d9 ci: remove artificial build throttles and fix caching gaps (#4200)
Speeds up the CI pipeline without changing what is tested:

- Drop the global CARGO_BUILD_JOBS=2 and per-job --jobs 2 flags, which
  halved compiler parallelism on the 4-core runners.
- Stop embedding hashFiles(Cargo.lock) in rust-cache shared keys. The
  action already fingerprints lockfiles internally; putting the hash in
  the key prefix meant any PR that touched Cargo.lock started from a
  completely cold cache instead of reusing unchanged dependencies.
- Give the e2e-tests job the full setup action with dependency caching.
  Its migration-proof step compiles the e2e_test crate (which pulls in
  most of the workspace) and previously did so cold on every run with
  no cache, on a 2-core runner.
- Set profile.dev debug = "line-tables-only": keeps usable backtraces
  while cutting compile/link time, target-dir size (faster cache
  save/restore), and debug-binary artifact upload/download.
- Split fmt + repo-script checks into a compile-free quick-checks job
  on ubuntu-latest so contributors get feedback in ~1 minute instead
  of after the full test run.
- Collapse the five per-filter migration-proof cargo test reruns into
  one filtered nextest invocation; the same tests already run in the
  full nextest pass.
- Remove the touch rustfs/build.rs + forced rebuild from the debug
  binary jobs (version stamping is irrelevant for e2e binaries) and
  the unreachable Windows cross-compile branch in build.yml.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:27:28 +08:00
cxymds f9cf4ff2bf fix(ecstore): surface multipart cleanup delete errors (#4198) 2026-07-02 22:36:48 +08:00
cxymds 5404ab155b fix(ecstore): precheck decommission capacity (#4196) 2026-07-02 22:36:29 +08:00
cxymds cf33bcc742 fix(ecstore): hold read locks for streaming readers (#4195) 2026-07-02 22:36:12 +08:00
cxymds 91a02914cc fix(ecstore): retry multipart abort cleanup (#4194) 2026-07-02 22:35:56 +08:00
cxymds d1a6033191 fix(ecstore): recheck source cleanup under lock (#4193) 2026-07-02 22:35:31 +08:00
Henry Guo 962c20bf47 feat(table-catalog): deepen maintenance rewrite support (#4192)
* feat(table-catalog): deepen maintenance rewrite support

* fix(ecstore): satisfy replication boundary clippy lint

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-02 22:35:07 +08:00
cxymds f8f373a236 fix: preserve compacted bucket counts (#4183)
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-02 22:34:47 +08:00
cxymds 4dc75b8760 fix: backpressure replication mrf saves (#4180) 2026-07-02 22:34:30 +08:00
cxymds c7a0178994 fix: honor replication status metadata (#4178)
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-02 22:33:41 +08:00
cxymds 6ed5c8fcd6 fix: fail unsupported replication actions (#4182)
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-07-02 22:33:16 +08:00
Zhengchao An 9a142fb123 refactor(replication): move delete schedule decisions (#4199) 2026-07-02 22:11:29 +08:00
houseme 70e1d79dfd feat: replace jemalloc with mimalloc (#4174)
* feat: replace jemalloc with mimalloc

* docs: record allocator rounds5 retest

* fix(replication): satisfy clippy unwrap lints

* docs: keep allocator migration plan local only

* feat(profiling): rely on pyroscope cpu profiling

* refactor(profiling): centralize unsupported pprof responses

* chore(deps): update s3s revision
2026-07-02 19:03:38 +08:00
Rafael Peroco 54496f2796 fix(scanner): advance cycle counter when a cycle ends on budget (#4163) 2026-07-02 17:15:51 +08:00
Zhengchao An 902bfa554b fix: collapse nested if to satisfy clippy::collapsible_if lint (#4190) 2026-07-02 17:15:15 +08:00
Zhengchao An b972e25c51 refactor(replication): move app decisions into crate (#4191) 2026-07-02 17:14:55 +08:00
cxymds 902cf1d3dc fix: handle scanner task failures (#4185) 2026-07-02 16:34:56 +08:00
Henry Guo 14c0fca576 test(table-catalog): extend live engine conformance probes (#4184)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-02 16:34:37 +08:00
cxymds b21b27dddd fix: preserve scanner usage backups (#4179) 2026-07-02 16:33:59 +08:00
cxymds d6a9826949 fix: preserve scanner usage state (#4177) 2026-07-02 16:32:51 +08:00
cxymds 71e9e81257 fix: finalize replication resync workers (#4175) 2026-07-02 16:32:33 +08:00
Zhengchao An f0adabbe90 refactor(replication): move object config bridge (#4188)
* refactor(replication): move object config bridge

* refactor(replication): centralize log constants
2026-07-02 16:32:02 +08:00
majinghe 3533080d4a ci: add self host runner on k8s (#4181) 2026-07-02 15:55:36 +08:00
Zhengchao An 1a9952c3fe refactor(replication): move target option builders (#4186) 2026-07-02 15:54:48 +08:00
Zhengchao An 008b7fcb6f refactor(replication): move heal queue routing contracts (#4176)
* refactor(replication): move heal queue routing contracts

* fix(replication): satisfy feature clippy
2026-07-02 15:15:08 +08:00
Zhengchao An 98a0cca4a2 refactor(replication): move resync decision contracts (#4173)
* refactor(replication): move resync decision contracts

* fix(replication): avoid resync status clones
2026-07-02 14:13:05 +08:00
Zhengchao An 4615df006d fix: auto-repair clippy failures from d666028cdc (#4172)
fix: simplify match arms to satisfy clippy manual-unwrap-or-default and manual-unwrap-or

Two match expressions in crates/replication/src/runtime.rs triggered
clippy lints (manual-unwrap-or-default, manual-unwrap-or) with the
project's -D warnings policy. Replace them with the idiomatic
.unwrap_or_default() and .unwrap_or() calls.
2026-07-02 14:11:44 +08:00
Henry Guo ba0276dd6f feat(table-catalog): harden maintenance scheduler control (#4164)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-07-02 14:11:07 +08:00
Zhengchao An 48f8443626 refactor(replication): move object compare contracts (#4171) 2026-07-02 13:19:47 +08:00
Zhengchao An d666028cdc refactor(replication): move resync classifiers into crate (#4170) 2026-07-02 12:57:59 +08:00
Zhengchao An 953a080b37 refactor(replication): move runtime sizing contracts (#4169) 2026-07-02 12:33:11 +08:00
Zhengchao An 473132f36b refactor(replication): move stats contracts into crate (#4168) 2026-07-02 12:14:36 +08:00
Zhengchao An 8ce0f824cf refactor(replication): move queue contracts into crate (#4167) 2026-07-02 11:40:49 +08:00
Zhengchao An 2cf856a51d Move replication delete worker contract into crate (#4166)
refactor(replication): move delete worker contract into crate
2026-07-02 11:03:24 +08:00
Zhengchao An df6ce41acd Move replication operation contracts into crate (#4165)
refactor(replication): move operation contracts into crate
2026-07-02 10:48:02 +08:00
Zhengchao An b57820a486 refactor(replication): move config contracts into crate (#4162) 2026-07-02 10:17:48 +08:00
Zhengchao An 18b79ccc2a refactor(replication): route filemeta paths through crate (#4161) 2026-07-02 09:08:47 +08:00
houseme 7a075c91da perf: avoid eager parity reader setup (#4133) 2026-07-02 08:49:08 +08:00
Zhengchao An 18d89d1e31 refactor(replication): route metadata contracts through crate (#4160) 2026-07-02 08:48:42 +08:00
Zhengchao An 0f4f4a6367 fix(replication): avoid target status map allocation (#4159) 2026-07-02 05:08:42 +08:00
Zhengchao An e05c281176 refactor(replication): centralize object replication state parsing (#4158)
* refactor(replication): centralize object replication state parsing

* fix(replication): gate test-only msgpack import
2026-07-02 04:41:54 +08:00
Zhengchao An 2de38d3e5c refactor(replication): move mrf wire format to replication crate (#4157)
* refactor(replication): move mrf wire format to replication crate

* refactor(replication): match re-exported error variants
2026-07-02 04:27:53 +08:00
Zhengchao An 8f01012071 fix(replication): address resync review follow-up (#4155)
* fix(replication): address resync crate review

* fix(replication): skip fixext fields correctly

* fix(replication): satisfy resync clippy
2026-07-02 04:00:28 +08:00
Zhengchao An e80391f1aa refactor(replication): extract resync contracts crate (#4154) 2026-07-02 01:30:01 +08:00
Zhengchao An 00e93f8267 fix(storage): tighten replication handle visibility (#4153) 2026-07-02 00:57:08 +08:00
GatewayJ 7728c9f203 fix(ecstore): require commit quorum for latest metadata (#4117) 2026-07-02 00:41:11 +08:00
Zhengchao An 572a001f93 refactor(storage): hide replication runtime handles (#4152) 2026-07-02 00:40:42 +08:00
Zhengchao An 14016dbe8c refactor(scanner): hide replication queue DTOs (#4150) 2026-07-02 00:17:03 +08:00
Zhengchao An 644a472e52 refactor(obs): hide replication stats handle (#4149) 2026-07-01 23:37:23 +08:00
Zhengchao An 2f5e4a8de4 fix: auto-repair breaking changes from a2ec13ab0 (#4148) 2026-07-01 23:16:52 +08:00
Zhengchao An b43655ccdd refactor(app): hide replication object DTOs (#4147) 2026-07-01 23:02:00 +08:00
Henry Guo a305160087 feat(table-catalog): add maintenance scheduler guardrails (#4123) 2026-07-01 22:31:19 +08:00
GatewayJ f7769884ff fix(lifecycle): honor expired delete marker semantics (#4124) 2026-07-01 22:31:08 +08:00
GatewayJ a60b6310a7 fix(lifecycle): block actions on replication state (#4125) 2026-07-01 22:31:00 +08:00
Zhengchao An b94e7d514d refactor(admin): hide replication resync DTOs (#4146) 2026-07-01 22:18:06 +08:00
majinghe d79f6872d7 ci: add self host runner support for build workflow (#4143) 2026-07-01 22:17:14 +08:00
majinghe a2ec13ab04 fix(otel): fix container unhealty issue caused by healthy check (#4145)
* fix: fix container unhealty issue caused by healthy check

* update readme file

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-01 21:56:26 +08:00
Zhengchao An 22cbd41ee9 refactor(ecstore): add replication owner bridges (#4141)
* refactor(ecstore): add replication owner bridges

* fix(ecstore): tighten replication owner guards
2026-07-01 21:51:26 +08:00
loverustfs f7e3fc780f ci: migrate artifact upload step to cloudflare r2 (#4142) 2026-07-01 20:04:31 +08:00
Zhengchao An c0dfc260ca refactor(ecstore): add replication object bridge (#4140) 2026-07-01 19:24:20 +08:00
Zhengchao An e5ab6ee8e4 refactor(ecstore): make replication facade explicit (#4139) 2026-07-01 18:54:05 +08:00
Zhengchao An fc806679fb refactor(ecstore): add replication work bridges (#4137) 2026-07-01 18:37:01 +08:00
Zhengchao An 28e87c1cab refactor(ecstore): name replication boundary contracts (#4134) 2026-07-01 17:55:12 +08:00
Zhengchao An f5adb5abc1 fix: bypass OnceLock in test for metadata early-stop gates (#4135) 2026-07-01 17:51:06 +08:00
Zhengchao An e8326e76a6 fix(ecstore): harden replication runtime guard (#4132) 2026-07-01 17:14:38 +08:00
houseme b16120dbcc feat: optimize small GET read paths (#4022) 2026-07-01 15:40:00 +08:00
Zhengchao An b0f491549a refactor(ecstore): hide replication runtime types (#4131) 2026-07-01 15:39:02 +08:00
Zhengchao An 8fd532ebb7 refactor(ecstore): use local replication paths (#4130) 2026-07-01 15:08:26 +08:00
Zhengchao An 7cb42529fc refactor(ecstore): route replication config access (#4128) 2026-07-01 09:30:25 +08:00
cxymds 6e5c58ca3f fix(replication): harden bucket replication correctness (#4116) 2026-07-01 09:21:30 +08:00
Zhengchao An 722c0805ce refactor(ecstore): route replication errors (#4127) 2026-07-01 08:13:57 +08:00
Zhengchao An 50a3ed8bf3 refactor(ecstore): route replication event host (#4126) 2026-07-01 06:00:50 +08:00
Zhengchao An e917f1f4be refactor(ecstore): route replication filemeta contracts (#4122) 2026-07-01 01:01:01 +08:00
Zhengchao An 942a8b505d refactor(ecstore): route replication metadata paths (#4121) 2026-06-30 22:48:34 +08:00
唐小鸭 4efefd67b5 fix(site-replication): site replication flag issue and resolved the replication storm (#4120)
* fix(site-replication): Add Docker Compose setup for site replication testing

- Fixed the site replication flag issue and resolved the replication storm.
- Introduced a new directory for site replication tests with Docker Compose.
- Created `docker-compose.yml` to define three RustFS sites and a setup container.
- Added `README.md` to document the purpose, usage, and test flow of the replication setup.
- Implemented `run-object-flow-check.sh` script to verify object replication across sites.
- Configured health checks and volume permissions for the RustFS containers.
- Enabled customization of access keys, bucket names, and other parameters via environment variables.

* fix

* fix
2026-06-30 21:52:06 +08:00
Henry Guo 5e68455eed test(table-catalog): add disaster recovery rehearsal probes (#4119)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-30 21:27:06 +08:00
Zhengchao An da7f421f69 refactor(ecstore): route replication storage contracts (#4118) 2026-06-30 20:12:06 +08:00
cxymds f9340da74a fix(lifecycle): harden S3 lifecycle rule handling (#4115)
* fix(lifecycle): honor noncurrent version retention

* fix(lifecycle): validate transition rules

* fix(lifecycle): support immediate multipart abort

* fix(lifecycle): harden select restore metadata

* fix(lifecycle): expose stale multipart cleanup to app

* fix(lifecycle): reduce expiry helper arguments
2026-06-30 19:06:34 +08:00
wood 7484a61fa3 feat(compression): show cluster-level compression stat in grafana (#4112) 2026-06-30 19:05:05 +08:00
Henry Guo cfb24b31ea test(table-catalog): add live client conformance harness (#4110)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-30 16:33:14 +08:00
Zhengchao An 70a1f37508 refactor(ecstore): route replication target types (#4114) 2026-06-30 16:29:05 +08:00
cxymds c67eea7bfa fix(ecstore): preserve rename data rollback metadata (#4107) 2026-06-30 15:45:27 +08:00
Zhengchao An 20919f0b97 refactor(ecstore): route replication bandwidth wrapping (#4113) 2026-06-30 15:42:04 +08:00
Zhengchao An b0dbf35cd4 refactor(ecstore): route lifecycle metadata reads (#4111) 2026-06-30 15:22:12 +08:00
Zhengchao An 4538919efa refactor(ecstore): route runtime getters through facade (#4109) 2026-06-30 14:32:01 +08:00
Zhengchao An 143833ec45 test(architecture): guard rustfs owner statics (#4108) 2026-06-30 14:01:19 +08:00
houseme 22c7cb1923 feat(ecstore): refresh issue-787 phase-3 validation track (#4106)
* feat: isolate list objects quorum config

* perf: add list gather observability for issue-785

* feat: add list-path observability for storage layers

* chore: add issue 785 list observability acceptance runner

* fix: avoid moved fields in list_path logging

* test: add marker replay idempotence checks for list pagination

* feat(ecstore): add list marker v2 cache id

* Revert "feat(ecstore): add list marker v2 cache id"

This reverts commit 980c364e02.

* Reapply "feat(ecstore): add list marker v2 cache id"

This reverts commit 766d2cee65.

* feat(ecstore): propagate list cache cursor in list layers

* test: add versioned continuation fallback tests

* test: add live pagination continuity smoke mode

* chore: run cargo fmt on issue-4003 branch

* build: fix bash3 compatibility in live smoke

* test(ecstore): simplify bool marker test assertion

* refactor(ecstore): unify list pagination state machine

* test: add issue 787 quorum validation runner

* build: support bash32 in issue-787 runner

* docs: add issue-787 benchmark evidence
2026-06-30 11:37:44 +08:00
GatewayJ 4815d608a2 fix(ecstore): preserve recovery quorum identity (#3937)
* fix(ecstore): preserve recovery quorum identity

* fix(get): hold read permits through response streaming

* fix(heal): queue read repair at low priority

* fix(ecstore): harden recovery read validation
2026-06-30 11:19:46 +08:00
Zhengchao An 7041d60cbf refactor(storage): narrow root runtime facade (#4105) 2026-06-30 11:19:21 +08:00
Zhengchao An f6689f5b39 test(app): remove object store fallback (#4104) 2026-06-30 08:08:54 +08:00
Zhengchao An 27e1cb5ffd refactor(storage): narrow object store resolver boundary (#4103) 2026-06-30 07:56:36 +08:00
Zhengchao An 9a8a82e90c test(architecture): guard owner cache globals (#4102) 2026-06-30 07:17:51 +08:00
Zhengchao An 733904deb1 test(architecture): guard owner runtime globals (#4101) 2026-06-30 06:54:07 +08:00
Zhengchao An f0d27982e5 refactor(app): collapse transition stats boundary (#4100) 2026-06-30 06:48:09 +08:00
Zhengchao An 46edc118f0 refactor(app): add lifecycle state context boundary (#4099) 2026-06-30 06:26:06 +08:00
Zhengchao An 055d3ad825 refactor(ecstore): scope dead code allowances (#4098) 2026-06-30 06:07:45 +08:00
Zhengchao An de19342744 refactor(runtime): finish legacy global naming (#4097) 2026-06-30 05:50:58 +08:00
Zhengchao An 74aef760f0 refactor(lifecycle): normalize state global names (#4096) 2026-06-30 05:29:10 +08:00
Zhengchao An cbd527cd6e refactor(runtime): normalize legacy global names (#4095) 2026-06-30 05:13:06 +08:00
Zhengchao An 90f9c24503 refactor(runtime): clean scalar globals (#4094) 2026-06-30 04:52:23 +08:00
Zhengchao An 77e45896ba refactor(runtime): remove unused node names global (#4093) 2026-06-30 04:44:17 +08:00
Zhengchao An c279d34af6 refactor(runtime): batch owner boundary guards (#4092) 2026-06-30 04:34:54 +08:00
Zhengchao An 19dc019038 test(runtime): guard kms service manager global (#4086) 2026-06-30 04:17:24 +08:00
Zhengchao An 90ea66423e test(runtime): guard internode transport global (#4085) 2026-06-30 04:11:30 +08:00
Zhengchao An 47b12972ad refactor(lifecycle): add object lock boundary (#4084) 2026-06-30 04:11:03 +08:00
houseme 3d868cb6e8 perf: add list objects quorum tuning knob (#4072) 2026-06-30 03:41:29 +08:00
Zhengchao An 3b754f649d test(runtime): guard batch processor global (#4083) 2026-06-30 03:40:03 +08:00
Zhengchao An f99429f2f2 refactor(lifecycle): add tagging boundary (#4082) 2026-06-30 03:39:32 +08:00
Zhengchao An fb7bcfb541 refactor(replication): add tagging boundary (#4081) 2026-06-30 03:39:17 +08:00
Zhengchao An 4f335889c1 refactor(lifecycle): add config persistence boundary (#4080) 2026-06-30 02:10:12 +08:00
Zhengchao An fab78f8402 refactor(scanner): use expiry runtime boundary (#4079) 2026-06-30 02:09:52 +08:00
Zhengchao An 35d7d6bf23 refactor(ecstore): wrap background cancel token access (#4078) 2026-06-30 02:09:39 +08:00
Zhengchao An d3a84395dc refactor(replication): add msgp boundary (#4076) 2026-06-30 02:09:14 +08:00
Zhengchao An 74c95920ca fix: dual-write renamed IO metrics (#4077) 2026-06-30 02:08:58 +08:00
Zhengchao An f401a4388e fix(ecstore): tolerate OnceLock re-initialization in tests (#4074) 2026-06-30 00:18:28 +08:00
Zhengchao An c17df6e3d3 refactor(replication): add lock boundary (#4075) 2026-06-30 00:18:08 +08:00
Zhengchao An 50ca1c2cbe refactor(replication): add config store boundary (#4073) 2026-06-30 00:03:45 +08:00
Zhengchao An f8c70e017d refactor(replication): add versioning boundary (#4071) 2026-06-29 23:52:36 +08:00
Zhengchao An 04e8c1bb6b refactor(replication): add metadata boundary (#4070) 2026-06-29 23:24:39 +08:00
Zhengchao An f1ec0bd765 refactor(replication): add target boundary (#4069) 2026-06-29 22:59:10 +08:00
Zhengchao An c94f2981d7 refactor(replication): add event sink boundary (#4068) 2026-06-29 22:46:06 +08:00
Zhengchao An 13474ec1de refactor(replication): add runtime boundary (#4067) 2026-06-29 22:34:10 +08:00
Zhengchao An e9a89e13ae refactor(lifecycle): route events through audit sink (#4066) 2026-06-29 22:21:23 +08:00
Zhengchao An 806dfb233c test(ecstore): guard set disks contract coverage (#4065) 2026-06-29 22:06:21 +08:00
Zhengchao An 7c2789e75a refactor(runtime): guard endpoint port source (#4064) 2026-06-29 21:56:57 +08:00
Zhengchao An a5e7dda6d9 refactor(io): make aligned pread error primary (#4063) 2026-06-29 21:46:20 +08:00
Zhengchao An 158f4a32a1 refactor(io): deprecate legacy method names (#4062) 2026-06-29 21:31:37 +08:00
Zhengchao An a7dd9603e9 refactor(ecstore): add mmap copy disk read name (#4061) 2026-06-29 21:18:41 +08:00
Zhengchao An 9567e7b3be refactor(io): make accurate reader names primary (#4060) 2026-06-29 20:20:22 +08:00
GatewayJ 14e9fa2c8d fix(ecstore): avoid startup pool meta locks (#4056) 2026-06-29 20:19:38 +08:00
Zhengchao An 50c9078142 refactor(runtime): guard scalar lock globals (#4059) 2026-06-29 20:04:44 +08:00
Zhengchao An 27a491a730 refactor(runtime): guard replication stats globals (#4058) 2026-06-29 19:55:38 +08:00
Zhengchao An 9b6c6df81b refactor(runtime): guard local node globals (#4057) 2026-06-29 19:47:21 +08:00
Zhengchao An 35077c033c refactor(runtime): guard endpoint erasure globals (#4055) 2026-06-29 19:26:05 +08:00
Zhengchao An 636476c3e5 refactor(runtime): guard lifecycle event globals (#4053) 2026-06-29 17:56:02 +08:00
Zhengchao An 0c4deb0e2f refactor(runtime): guard setup type globals (#4052) 2026-06-29 17:38:57 +08:00
Zhengchao An 0cd08eae1c refactor(runtime): remove stale lifecycle state comments (#4051) 2026-06-29 17:29:57 +08:00
Zhengchao An 726bf142e0 refactor(runtime): remove stale tier transport comments (#4050) 2026-06-29 17:29:46 +08:00
Zhengchao An aadefcc577 refactor(runtime): guard local disk set global (#4049) 2026-06-29 17:29:24 +08:00
Zhengchao An 2b04bf3a19 refactor(runtime): remove stale lifecycle global comment (#4048) 2026-06-29 16:33:49 +08:00
Zhengchao An ecbc5385d3 refactor(runtime): hide local disk id global (#4047) 2026-06-29 16:33:36 +08:00
Zhengchao An ad6be6a167 refactor(runtime): hide local disk map global (#4046) 2026-06-29 14:57:14 +08:00
Zhengchao An 4111ce1547 refactor(runtime): wrap tier config global (#4045) 2026-06-29 14:38:36 +08:00
Zhengchao An e012d1e799 refactor(runtime): wrap boot time global (#4044) 2026-06-29 14:21:31 +08:00
Henry Guo 809e77d5db fix(runtime): keep lock clients available during startup (#4042) 2026-06-29 14:14:26 +08:00
Zhengchao An 4567f6f521 refactor(runtime): guard rpc secret global (#4043) 2026-06-29 14:02:03 +08:00
Zhengchao An 6093eb0c9c refactor(runtime): wrap local node name global (#4041) 2026-06-29 13:13:54 +08:00
Zhengchao An 9c62a24efc refactor(runtime): wrap outbound tls globals (#4039) 2026-06-29 12:56:34 +08:00
Henry Guo de41303f3e feat(table-catalog): add durable backing migration guardrails (#4038)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-29 12:39:04 +08:00
Zhengchao An 9414d645f4 refactor(runtime): wrap rustfs address globals (#4037) 2026-06-29 11:18:30 +08:00
Henry Guo c200dc10d2 fix(scanner): preserve partial scan progress (#3996) 2026-06-29 10:43:25 +08:00
Zhengchao An 9165de3241 refactor(runtime): wrap internode connection cache (#4036) 2026-06-29 10:43:13 +08:00
dependabot[bot] df82e3e645 build(deps): bump the dependencies group with 3 updates (#4030)
Bumps the dependencies group with 3 updates: [aes-gcm](https://github.com/RustCrypto/AEADs), [chacha20poly1305](https://github.com/RustCrypto/AEADs) and [arc-swap](https://github.com/vorner/arc-swap).


Updates `aes-gcm` from 0.11.0-rc.4 to 0.11.0
- [Commits](https://github.com/RustCrypto/AEADs/compare/aes-gcm-v0.11.0-rc.4...aes-gcm-v0.11.0)

Updates `chacha20poly1305` from 0.11.0-rc.3 to 0.11.0
- [Commits](https://github.com/RustCrypto/AEADs/compare/chacha20poly1305-v0.11.0-rc.3...chacha20poly1305-v0.11.0)

Updates `arc-swap` from 1.9.1 to 1.9.2
- [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/arc-swap/commits)

---
updated-dependencies:
- dependency-name: aes-gcm
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: chacha20poly1305
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: arc-swap
  dependency-version: 1.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-29 10:32:52 +08:00
Zhengchao An 801838fdbd test(ecstore): guard replication facade boundary (#4035) 2026-06-29 10:32:09 +08:00
Zhengchao An 319497446b refactor(ecstore): add lifecycle runtime boundary (#4034)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-29 10:31:49 +08:00
Zhengchao An 5fc25eccb3 feat(io-core): add truthful buffered io aliases (#4032) 2026-06-29 10:01:59 +08:00
Zhengchao An 1e6f20912a test(ecstore): cover SetDisks storage contracts (#4033) 2026-06-29 10:01:42 +08:00
Zhengchao An ea88f5b67b refactor(obs): use lifecycle runtime facades (#4031) 2026-06-29 09:29:50 +08:00
Zhengchao An ed56ffb54a docs(io): clarify zero copy metric transition (#4029) 2026-06-29 08:21:07 +08:00
Zhengchao An 2d547de84a docs(architecture): inventory global state migration (#4028) 2026-06-29 08:20:32 +08:00
cxymds 0ff261864a fix(scanner): harden data scanner integrity handling (#4012)
* fix(scanner): heal metadata scan failures

* fix(scanner): preserve dirty buckets after scan failures

* feat(scanner): record leader lock liveness

* fix(scanner): preserve failed deep scan state

* fix(scanner): reject untimestamped stale usage

* fix(scanner): report topology-derived admission limit

* fix(scanner): accumulate tier usage stats

* fix(scanner): guard data usage cache recursion

* feat(scanner): expose startup enabled status

* fix(scanner): continue after heal admission rejection

* fix(scanner): avoid cyclic root usage double count

* fix(scanner): preserve dirty markers on cache save failure

* feat(scanner): expose leader liveness status

* fix(scanner): avoid heal escalation for transient metadata reads

* feat(scanner): persist rejected heal retry candidates

* test(scanner): satisfy freshness status clippy

* test(app): avoid global store reinit in context test

* test(app): gate global store helper to tests
2026-06-29 08:19:58 +08:00
Henry Guo cf7af17b43 feat(table-catalog): wire durable strong backing config (#3971)
* feat(table-catalog): wire durable strong backing config

* fix(table-catalog): guard strong backing warehouse prefixes

* fix(checksums): remove unused base64 decode helper

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-29 07:58:14 +08:00
Zhengchao An 51d44f20c7 docs(ecstore): inventory replication split blockers (#4027) 2026-06-29 07:57:48 +08:00
Zhengchao An 763cbdd5b7 docs(ecstore): inventory lifecycle split blockers (#4026) 2026-06-29 07:57:22 +08:00
Zhengchao An aaef31d4dd docs(ecstore): inventory api facade boundaries (#4025) 2026-06-29 07:57:01 +08:00
Zhengchao An e1c1d8edcb test: expand snapshot coverage (#4024) 2026-06-29 07:56:37 +08:00
Zhengchao An 1104d630d1 docs(obs): inventory ecstore dependency boundary (#4023) 2026-06-29 07:07:47 +08:00
Zhengchao An 8284e6a75c config: add mmap read env alias (#4021) 2026-06-28 23:24:24 +08:00
Zhengchao An b37c8c6435 build: pin rc crypto dependency versions (#4020) 2026-06-28 22:52:20 +08:00
houseme 0485e5adf0 feat(get): Small-file GET performance optimization for 1KiB-1MiB objects (#4016)
* feat(get): SF01 - bucket validation cache

Add 5s TTL cache for bucket validation to avoid repeated stat_volume()
calls on every GET request.

Changes:
- Add BUCKET_VALIDATED_CACHE (OnceLock + RwLock + HashMap)
- Add invalidate_bucket_validation_cache() for cache invalidation
- Add invalidate_all_bucket_validation_cache() for bulk invalidation
- Update get_validated_store() to use cache
- Add cache invalidation in execute_delete_bucket()

Expected impact: 3-5x improvement for small file GET latency.

Closes rustfs/backlog#766

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(get): SF03 - metadata cache TTL increase

Increase metadata cache TTL from 250ms to 2s and capacity from 1024
to 4096 entries.

Changes:
- GET_OBJECT_METADATA_CACHE_TTL: 250ms -> 2s
- GET_OBJECT_METADATA_CACHE_MAX_ENTRIES: 1024 -> 4096

All mutation paths already call invalidate_get_object_metadata_cache,
so the longer TTL is safe.

Expected impact: 10-50x improvement for hot objects.

Closes rustfs/backlog#768

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(get): SF04 - remove unnecessary tokio::spawn in metadata fanout

Replace tokio::spawn with direct async future in read_all_fileinfo_full_wait.
join_all already provides concurrency, so tokio::spawn adds unnecessary
task creation and scheduling overhead.

Changes:
- Remove tokio::spawn from metadata fanout futures
- Update result handling for direct future results

Expected impact: 16-32us reduction per GET request.

Closes rustfs/backlog#769

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(get): SF06 - conditional lifecycle check

Only call resolve_put_object_expiration when the object has an
x-amz-expiration metadata marker. This avoids unnecessary lifecycle
configuration reads on every GET request.

Expected impact: 50-100us reduction per GET request.

Closes rustfs/backlog#771

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(get): SF07 - conditional metrics recording

Gate hot path metrics behind get_stage_metrics_enabled() to reduce
overhead when metrics are not needed.

Changes:
- Conditional record_zero_copy_read
- Conditional manager.record_disk_operation
- Conditional manager.record_access
- Conditional manager.record_transfer

Expected impact: 20-50us reduction per GET request.

Closes rustfs/backlog#772

Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor(get): SF01 - use moka instead of dashmap for bucket cache

Replace OnceLock + RwLock + HashMap with moka::sync::Cache for bucket
validation cache. moka provides built-in TTL support and is already
available in the workspace.

Changes:
- Add moka dependency to rustfs crate
- Replace manual TTL management with moka's time_to_live
- Simplify cache operations

Closes rustfs/backlog#766

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(get): SF02 - inline data fast path

Add fast path for small inline objects that bypasses duplex pipe,
tokio::spawn, and bitrot reader creation when data is already in memory.

Changes:
- Add inline data detection before codec streaming gate
- Direct in-memory erasure decode for inline objects <= 128KB
- Add GET_OBJECT_PATH_INLINE_DIRECT metric path
- Skip duplex pipe and background task for inline data

Conditions for fast path:
- Single part object
- Inline data available
- Size <= 128KB
- Not encrypted/compressed/remote
- No range request

Expected impact: 2-3x improvement for small file GET latency.

Closes rustfs/backlog#767

Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor: translate Chinese comments to English

Translate all Chinese comments to English in modified files:
- rustfs/src/storage/ecfs_extend.rs
- rustfs/src/app/bucket_usecase.rs

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix

* add

* fmt and improve import

* fmt

* feat(get): SF05 skip IO planning + refactor inline detection + adaptive bucket cache

SF05: Skip disk I/O semaphore for inline data fast path
- Reorder prepare_get_object_read_execution: read first, then decide semaphore
- Inline objects skip acquire_disk_read_permit() entirely (saves 100-200us)
- Add is_inline_fast_path field to GetObjectReadSetup

Refactor: Unify inline detection logic
- Add ObjectInfo::is_inline_fast_path_eligible() as single source of truth
- Version-aware thresholds: non-versioned 128KB, versioned 16KB (matches PUT)
- Eliminates divergent conditions between set_disk/mod.rs and object_usecase.rs

Refactor: Restore fault tolerance in metadata fanout
- Restore tokio::spawn + JoinError handling in read_all_fileinfo_full_wait
- Prevents single disk read panic from unwinding the entire operation

Refactor: Restore lifecycle check correctness
- Remove incorrect SF06 conditional that skipped lifecycle for most objects
- Always call resolve_put_object_expiration (original behavior)

Fix: make_bucket cache invalidation
- Invalidate bucket validation cache on create_bucket

Fix: erasure decode written validation
- Check decode() return value; error if 0 bytes written for non-empty object

Adaptive bucket cache
- Default: RwLock<HashMap> for < 100 buckets (low overhead)
- Opt-in: starshard::ShardedHashMap via RUSTFS_BUCKET_CACHE_STARSHARD=1
- 5s TTL with manual timestamp checking

Benchmark results (warp get, concurrency 32, 10s, 3 rounds):
- 10KiB: 25.10 MiB/s (+28.2% vs SF01-07)
- 100KiB: 221.81 MiB/s
- 1MiB: 1972.78 MiB/s
- vs main: -10% to -12% (inline path not triggered by warp)

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(versioning): use read lock for versioning config query + five-expert analysis

P0 fix: BucketVersioningSys::get() was using write lock on
GLOBAL_BucketMetadataSys for a pure read operation. This serialized
all concurrent GET requests (3 write-lock acquisitions per request).

Changed to read lock — get_versioning_config() handles its own
internal locking via metadata_map RwLock.

Five-expert analysis identified top bottlenecks:
1. Versioning write lock (P0, fixed)
2. Inline fast path not triggered (P0, needs verification)
3. Metadata fanout no early-stop (P1, early-stop has bug, reverted)
4. Request-level versioning cache (P1, pending)
5. Duplex pipe for small objects (P2, pending)

Benchmark (read-lock fix, warp concurrency 32):
- 1KiB: 2.29 MiB/s (vs 2.53 before, within variance)
- 10KiB: 25.00 MiB/s (same as before)
- 100KiB: 246.72 MiB/s (+11% vs 221.81)
- 1MiB: 2039.95 MiB/s (+3% vs 1972.78)

Co-Authored-By: heihutu <heihutu@gmail.com>

* chore: remove benchmark results from git, keep locally only

Remove docs/benchmark/*.md from version control.
Files remain on disk but are no longer tracked by git.
Added docs/benchmark/*.md to .gitignore.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(get): decode inline fast path through bitrot readers

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-06-28 22:35:42 +08:00
Zhengchao An f5d7fea7a4 fix(ecstore): remove unused mirror fields (#4019) 2026-06-28 22:31:37 +08:00
Zhengchao An 0f3213a530 docs: update security advisory lessons (#4018) 2026-06-28 21:55:40 +08:00
Zhengchao An 99f981a868 docs: plan ecstore split boundaries (#4017)
docs: plan ecstore module split boundaries
2026-06-28 21:55:10 +08:00
Zhengchao An ed0f191e1a docs: document global singleton migration plan (#730) (#4015)
docs: document global singleton migration plan

Add comments documenting the Tier A/B classification and migration
plan for global singletons in ecstore runtime.

Refs #730
2026-06-28 21:25:42 +08:00
Zhengchao An 163e8089db fix: remove all backlog links from code and comments (#4013) 2026-06-28 20:55:25 +08:00
Zhengchao An aa8b8b5706 docs: document crypto RC version dependencies (#732) (#4011)
* docs: document crypto RC version dependencies

Add comment explaining why aes-gcm and chacha20poly1305 use RC
versions and the migration path when stable versions are released.

Refs #732

* fix: remove all backlog links from code and comments
2026-06-28 20:47:51 +08:00
Zhengchao An cd165ab181 docs: document obs reverse dependency on ecstore (#735) (#4010)
docs: document obs reverse dependency on ecstore

Add comment explaining why obs depends on ecstore and the scope
of work required to break this dependency.

Refs #735

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-28 20:28:04 +08:00
Zhengchao An a37e918936 docs: document SHA-1 HMAC migration proposal (#747) (#4009) 2026-06-28 19:42:18 +08:00
Zhengchao An 8d0ba1cd3c docs: document deadlock detector mutex design rationale (#744) (#4008)
docs: document deadlock detector mutex design rationale

Add comment explaining why std::sync::Mutex is used instead of
tokio::sync::Mutex in the deadlock detector.

Refs #744

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-28 19:29:50 +08:00
Zhengchao An 27b8592879 docs: add documentation to storage-api public types (#741) (#4007)
docs: add documentation to storage-api public types

Add doc comments to public structs, enums, and traits in
storage-api crate to improve documentation coverage.

Refs #741
2026-06-28 19:12:02 +08:00
Zhengchao An 1f56ddf913 docs: document table_catalog mutex design rationale (#739) (#4006)
docs: document table_catalog mutex design rationale

Add comment explaining why the single mutex is intentional and
what alternatives to consider if contention becomes a bottleneck.

Refs #739
2026-06-28 18:56:13 +08:00
Zhengchao An 710ae74cde perf: add S3 operations benchmark framework (#738) (#4005) 2026-06-28 18:02:41 +08:00
Zhengchao An 51acf2a99c feat: add rate limiting middleware framework (#737) (#4002)
* feat: add rate limiting middleware framework

Add rate limiting middleware with token bucket algorithm for
per-client request rate limiting. This provides the foundation
for DoS protection.

Refs #737

* fix: address clippy lints in rate_limit.rs

- Collapse nested if statements into single if-let chains
- Use .is_multiple_of() instead of manual modulo check
2026-06-28 17:48:40 +08:00
Zhengchao An ee82d6c026 test: add insta snapshot test for storage error display format (#740) (#4001)
test: add insta snapshot test for storage error display format

Add snapshot test to detect unexpected changes in StorageError
display format. This catches output format regressions that
traditional assert tests might miss.

Refs #740
2026-06-28 16:10:07 +08:00
Zhengchao An 84cdf12083 test(security): add security boundary tests (#748) (#3998)
test(security): add security boundary tests

Add e2e tests for security-sensitive scenarios:
- Large XML body handling (DoS protection)
- Excessive multipart parts (DoS protection)
- Concurrent object operations (race condition handling)
- Internal URL validation (SSRF prevention)

Refs #748
2026-06-28 15:17:30 +08:00
Zhengchao An c768a9c382 docs(storage-api): document filemeta dependency as known limitation (#731) (#3997)
* docs(storage-api): document filemeta dependency as known limitation

Add comment explaining why storage-api depends on filemeta and
the scope of work required to break this dependency (300+ files).

Refs https://github.com/rustfs/backlog/issues/731

* docs(storage-api): remove backlog link from comment
2026-06-28 14:59:24 +08:00
Zhengchao An c475d03b48 fix: replace unwrap() with expect() in more files (#729 batch 14) (#3994) 2026-06-28 11:45:23 +08:00
Zhengchao An 05d201679c fix: replace unwrap() with expect() in more files (#729 batch 13) (#3993) 2026-06-28 11:45:13 +08:00
Zhengchao An f0ab812213 fix: replace unwrap() with expect() in remaining files (#729 batch 12) (#3992) 2026-06-28 11:45:03 +08:00
Zhengchao An 1f8a5bc095 fix(ecstore): replace unwrap() with proper error handling in api_get_object_attributes (#729 batch 11) (#3991)
fix(ecstore): replace unwrap() with proper error handling in api_get_object_attributes

Replace unsafe unwrap() calls with proper error handling in
api_get_object_attributes.rs:
- HTTP header access now uses ok_or_else with descriptive messages
- String parsing now uses map_err with descriptive messages
- HeaderValue creation now uses expect with descriptive messages

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:23:41 +08:00
Zhengchao An 25170943ce fix(ecstore): improve expect() messages in admin_server_info (#729 batch 9) (#3990)
fix(ecstore): improve expect() messages in admin_server_info

Replace unwrap() with expect() for better error diagnostics in
admin_server_info.rs:
- URL host/port access now has descriptive messages
- HashMap get_mut calls now have descriptive messages

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:23:04 +08:00
Zhengchao An 20c7cb1074 fix(server): improve expect() messages in layer.rs (#729 batch 8) (#3989)
fix(server): improve expect() messages in layer.rs

Replace unwrap() with expect(valid response body) for Response
builder calls in layer.rs.

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:22:46 +08:00
Zhengchao An 5c94fe7dd7 fix(ecstore): improve expect() messages in replication_resyncer (#729 batch 7) (#3988)
fix(ecstore): improve expect() messages in replication_resyncer

Replace unwrap() with expect() for better error diagnostics in
replication_resyncer.rs:
- HashMap get_mut calls now have descriptive expect messages
- format() calls now use unwrap_or_else for error handling

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:22:26 +08:00
Zhengchao An d668ef837a fix(admin): improve expect() messages in remaining admin handlers (#729 batch 6) (#3987)
fix(admin): improve expect() messages in remaining admin handlers

Replace generic parse().unwrap() with parse().expect(valid header value)
for better error diagnostics in service_account.rs, group.rs, and console.rs.

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:22:08 +08:00
Zhengchao An e5a355efe9 fix(admin): improve expect() messages in policies handler (#729 batch 5) (#3986)
fix(admin): improve expect() messages in policies handler

Replace generic parse().unwrap() with parse().expect(valid header value)
for better error diagnostics in policies.rs.

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:21:51 +08:00
Zhengchao An dbca96faf6 fix(admin): improve expect() messages in user handler (#729 batch 4) (#3985)
fix(admin): improve expect() messages in user handler

Replace generic parse().unwrap() with parse().expect(valid header value)
for better error diagnostics in user.rs.

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:21:24 +08:00
Zhengchao An e8470bbc66 fix(admin): replace unwrap() with safe pattern in bucket_meta handler (#729 batch 3) (#3984)
fix(admin): replace unwrap() with safe pattern in bucket_meta handler

Replace 11 instances of HashMap.get_mut().unwrap() with
match pattern that continues to next iteration if key is missing.

Also improve expect() messages for header value parsing.

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:20:51 +08:00
houseme 46d7f9e1f2 feat(get): harden codec streaming rollout (#3981)
* feat(get): consolidate GET performance optimization

Consolidated implementation of all GET performance optimizations into
a single, well-organized commit replacing the previous patch-on-patch
approach.

## Changes

### Configuration (set_disk/mod.rs)
- Consolidated all GET optimization flags into a single organized section
- Enabled by default: codec streaming, metadata early-stop, page cache reclaim
- Added codec streaming multipart flag (default: disabled)
- Added version-aware early-stop flag (default: disabled)
- Added adaptive duplex buffer sizing based on object size
- All flags use OnceLock caching with rollout percentage support

### Metadata Early-Stop (set_disk/read.rs)
- Delete marker early-stop when quorum agrees
- Version-aware early-stop for versioned GET requests
- MetadataQuorumAccumulator enhanced with:
  - delete_marker_votes tracking
  - requested_version_id and matching_version_votes tracking
  - version_early_stop_decision() method
- 6 new tests for version early-stop scenarios

### Codec Streaming (erasure/coding/decode_reader.rs)
- DualInFlight (2-stripe lookahead) enabled by default

### Decode Pipeline (erasure/coding/decode.rs)
- Stripe prefetch count configuration
- Bitrot-decode overlap configuration

### Disk Layer (disk/local.rs)
- O_DIRECT read configuration constants (preparation)

### Metrics (io-metrics/lib.rs)
- BytesPool acquisition/return metrics
- Metadata phase duration with early-stop label
- Total duration with reader_path label

### Diagnostics (diagnostics/)
- Early-stop reason constants
- Pool tier/outcome label constants

### Observability (.docker/observability/)
- 3 Grafana dashboards for GET optimization monitoring
- Prometheus alert rules (6 alerts: 3 critical, 3 warning)
- Updated README.md and README_ZH.md with usage docs

### Config (config/src/constants/runtime.rs)
- Page cache reclaim read enabled by default

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| RUSTFS_GET_CODEC_STREAMING_ENABLE | true | Codec streaming base flag |
| RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT | 100 | Codec streaming rollout % |
| RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE | false | Multipart codec streaming |
| RUSTFS_GET_METADATA_EARLY_STOP_ENABLE | true | Early-stop base flag |
| RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT | 100 | Early-stop rollout % |
| RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE | false | Version-aware early-stop |
| RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE | true | Page cache reclaim |
| RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE | false | O_DIRECT (preparation) |
| RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT | 1 | Stripe prefetch |
| RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE | false | Bitrot-decode overlap |
| RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT | 2 | DualInFlight stripes |

## Rollback

All optimizations can be disabled via environment variables:
RUSTFS_GET_CODEC_STREAMING_ENABLE=false
RUSTFS_GET_METADATA_EARLY_STOP_ENABLE=false
RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE=false

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(get): add stress test scripts for GET optimization validation

- quick-validate-get-optimization.sh: Quick 5-minute validation
- stress-test-get-optimization.sh: Full 30+ minute stress test
- README-stress-test.md: Usage documentation

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): align file cache reclaim defaults

* chore(deps): update redis and erasure codec

* test(ecstore): align decode fill policy default

* fix(get): wire codec streaming rollout gate

* perf(get): skip metrics-off codec timers

* test(get): capture codec streaming diagnostics

* test(get): add multipart fallback probe

* test(get): add encrypted fallback probe

* test(get): add compressed fallback probe

* test(get): add degraded read fallback probe

* test(get): cover remote fallback probe

* test(get): report warp request p99

* test(get): capture OTLP metric deltas

* perf(get): align codec streaming inflight default

* perf(get): reuse codec reader output buffers

* test(get): count codec reader fill starts

* perf(get): reuse codec reader fill worker

* perf(get): lazy init rustfs codec reconstruct

* test(get): cover rustfs codec source faults

* docs(get): record rustfs codec fallback scope

* feat(get): add multipart codec reader opt-in

* test(get): add multipart codec smoke option

* test(get): cover multipart codec degraded fallback

* perf(get): bound multipart codec eager setup

* test(get): satisfy codec hardening PR gate

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-06-28 11:20:21 +08:00
Zhengchao An d99056902e fix(admin): replace unwrap() with proper error handling in tier handler (#729 batch 2) (#3983)
fix(admin): replace unwrap() with proper error handling in tier handler

Replace 9 instances of args.{type}.clone().unwrap() with
ok_or_else() that returns a descriptive S3Error when the
tier configuration is missing.

Also improve expect() messages for header value parsing.

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 11:00:50 +08:00
Zhengchao An 6e72dc3076 fix(ecstore): replace unsafe k.unwrap() in bucket_target_sys (#729) (#3982)
fix(ecstore): replace k.unwrap() with safe pattern in bucket_target_sys

Replace unsafe k.unwrap().as_str() with if let Some(key_str) pattern
in 5 locations where HeaderMap iterator yields (Option<HeaderName>, Value).

This prevents potential panics if header names are invalid.

Refs https://github.com/rustfs/backlog/issues/729
2026-06-28 10:42:56 +08:00
Zhengchao An 259a99a501 fix(ci): pin protocol matrix checkout action (#3977) 2026-06-28 10:04:36 +08:00
houseme c928fd1c7a chore(deps): update flake.lock (#3978) 2026-06-28 09:54:22 +08:00
Zhengchao An 7238a937a9 fix: correct misleading zero-copy/direct-io docs and internal naming (#733 Phase 1) (#3976) 2026-06-28 09:12:49 +08:00
Zhengchao An 3c6dc2a633 ci: remove --no-default-features from protocol test matrix (#3980) 2026-06-28 09:12:23 +08:00
Zhengchao An e1272f2aba revert: restore #![allow(dead_code)] - CI clippy -D warnings conflict (#3979)
revert: restore #![allow(dead_code)] - clippy -D warnings treats warn as error

The #742 PR changed #![allow(dead_code)] to #![warn(dead_code)], but
CI runs clippy with -D warnings which turns warnings into errors.
This caused CI failures across multiple PRs.

Reverting to #![allow(dead_code)] until the dead code is actually
cleaned up. The 189 warnings in ecstore should be fixed incrementally
by deleting dead code and adding item-level allows, not by changing
the crate-level policy.
2026-06-28 08:32:34 +08:00
Henry Guo 1e303e5be0 fix(data-usage): refresh versioned usage state (#3969)
fix(data-usage): refresh versioned usage from authoritative state

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-28 07:58:07 +08:00
Zhengchao An 129ad2296b ci: add protocol feature test matrix (swift, sftp, no-default) (#736) (#3975) 2026-06-28 07:51:15 +08:00
Zhengchao An 113058af54 chore: replace blanket #![allow(dead_code)] with #![warn(dead_code)] (#742) (#3974) 2026-06-28 07:50:51 +08:00
houseme 27468ebfa9 feat(get): consolidate GET performance optimization (#3972)
* feat(get): consolidate GET performance optimization

Consolidated implementation of all GET performance optimizations into
a single, well-organized commit replacing the previous patch-on-patch
approach.

## Changes

### Configuration (set_disk/mod.rs)
- Consolidated all GET optimization flags into a single organized section
- Enabled by default: codec streaming, metadata early-stop, page cache reclaim
- Added codec streaming multipart flag (default: disabled)
- Added version-aware early-stop flag (default: disabled)
- Added adaptive duplex buffer sizing based on object size
- All flags use OnceLock caching with rollout percentage support

### Metadata Early-Stop (set_disk/read.rs)
- Delete marker early-stop when quorum agrees
- Version-aware early-stop for versioned GET requests
- MetadataQuorumAccumulator enhanced with:
  - delete_marker_votes tracking
  - requested_version_id and matching_version_votes tracking
  - version_early_stop_decision() method
- 6 new tests for version early-stop scenarios

### Codec Streaming (erasure/coding/decode_reader.rs)
- DualInFlight (2-stripe lookahead) enabled by default

### Decode Pipeline (erasure/coding/decode.rs)
- Stripe prefetch count configuration
- Bitrot-decode overlap configuration

### Disk Layer (disk/local.rs)
- O_DIRECT read configuration constants (preparation)

### Metrics (io-metrics/lib.rs)
- BytesPool acquisition/return metrics
- Metadata phase duration with early-stop label
- Total duration with reader_path label

### Diagnostics (diagnostics/)
- Early-stop reason constants
- Pool tier/outcome label constants

### Observability (.docker/observability/)
- 3 Grafana dashboards for GET optimization monitoring
- Prometheus alert rules (6 alerts: 3 critical, 3 warning)
- Updated README.md and README_ZH.md with usage docs

### Config (config/src/constants/runtime.rs)
- Page cache reclaim read enabled by default

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| RUSTFS_GET_CODEC_STREAMING_ENABLE | true | Codec streaming base flag |
| RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT | 100 | Codec streaming rollout % |
| RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE | false | Multipart codec streaming |
| RUSTFS_GET_METADATA_EARLY_STOP_ENABLE | true | Early-stop base flag |
| RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT | 100 | Early-stop rollout % |
| RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE | false | Version-aware early-stop |
| RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE | true | Page cache reclaim |
| RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE | false | O_DIRECT (preparation) |
| RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT | 1 | Stripe prefetch |
| RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE | false | Bitrot-decode overlap |
| RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT | 2 | DualInFlight stripes |

## Rollback

All optimizations can be disabled via environment variables:
RUSTFS_GET_CODEC_STREAMING_ENABLE=false
RUSTFS_GET_METADATA_EARLY_STOP_ENABLE=false
RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE=false

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(get): add stress test scripts for GET optimization validation

- quick-validate-get-optimization.sh: Quick 5-minute validation
- stress-test-get-optimization.sh: Full 30+ minute stress test
- README-stress-test.md: Usage documentation

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): align file cache reclaim defaults

* chore(deps): update redis and erasure codec

* test(ecstore): align decode fill policy default

* test(ecstore): align metadata early-stop default

* fix(ecstore): keep metadata early stop opt-in

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-06-28 07:14:07 +08:00
Zhengchao An 512418cda9 fix(ecstore): replace unbounded metadata cache with moka (#743) (#3970)
fix(ecstore): replace unbounded metadata cache with moka

Replace the manual Arc<RwLock<HashMap>> metadata cache with
moka::future::Cache, which provides:
- Built-in LRU eviction when max_capacity is reached
- Automatic TTL expiry via time_to_live (250ms)
- Lock-free concurrent reads
- Non-blocking invalidation

Fixes the memory leak risk from unbounded HashMap and the
all-or-nothing eviction logic that cleared all entries at once.

Closes #743

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-28 03:01:32 +08:00
houseme 175566f037 docs(get): record default switch readiness (#3967)
* docs(get): record default switch readiness

* chore(docs): keep pr35 evaluation note local

* refactor(bench): trim pr35 readiness python
2026-06-28 01:12:44 +08:00
Zhengchao An 7ae9697d42 docs: close final architecture audit gaps (#3966) 2026-06-28 00:07:42 +08:00
Zhengchao An 79234c030d docs: close architecture migration validation (#3965) 2026-06-27 23:42:51 +08:00
Zhengchao An 82bbef0b60 test: cover runtime and repair preservation (#3964) 2026-06-27 23:34:59 +08:00
houseme bf03ff2869 feat(get): add limited opt-in rollout gates (#3963)
* feat(bench): harden cooled get ab harness

* feat(get): add limited opt-in rollout gates

* fix(get): tighten rollout gate fallbacks
2026-06-27 23:13:02 +08:00
Zhengchao An 66ad138505 docs: close phase 7 global split plan (#3962) 2026-06-27 22:25:16 +08:00
houseme 47d05a1d6a feat(bench): harden cooled get ab harness (#3960) 2026-06-27 22:23:09 +08:00
Zhengchao An c7dc3d7974 refactor: move runtime fallback defaults to owners (#3961) 2026-06-27 22:21:09 +08:00
houseme 20f56af09c feat(get): tune output response handoff (#3956) 2026-06-27 21:46:38 +08:00
Zhengchao An 68d5d1d41d refactor: isolate specialized runtime fallbacks (#3958) 2026-06-27 21:35:47 +08:00
Zhengchao An 672f6e9ea9 refactor: remove scalar runtime fallbacks (#3957) 2026-06-27 20:45:13 +08:00
Zhengchao An 243a20b14e refactor: remove service resolver fallbacks (#3955) 2026-06-27 19:35:43 +08:00
Zhengchao An 4732f4ee2d refactor: expose runtime fallback boundaries (#3954) 2026-06-27 18:57:51 +08:00
Zhengchao An cdebdeca61 refactor: clean app context fallback signatures (#3953) 2026-06-27 18:30:06 +08:00
Zhengchao An d66391636b refactor: remove optional runtime handle fallbacks (#3952) 2026-06-27 18:07:09 +08:00
Zhengchao An 430aa8e8cb refactor: remove notification config fallbacks (#3951) 2026-06-27 17:43:05 +08:00
Zhengchao An ab9e98a9ca refactor: align core runtime facade helpers (#3948) 2026-06-27 17:14:25 +08:00
houseme de86025f2c feat(get): overlap read verify decode (#3945) 2026-06-27 15:14:44 +08:00
Zhengchao An dff0e467f4 refactor: align app runtime facade helpers (#3947) 2026-06-27 15:14:29 +08:00
Zhengchao An 7979ecb545 refactor: align storage runtime facade helpers (#3946) 2026-06-27 14:55:37 +08:00
Zhengchao An 261e16a446 refactor: route admin runtime sources through current helpers (#3944) 2026-06-27 14:19:18 +08:00
Zhengchao An cc41a50efe refactor: route admin runtime consumers through app context (#3943) 2026-06-27 13:38:21 +08:00
houseme 58b76a3d45 feat(get): add codec engine ab matrix (#3940)
* upgrade version

* feat(get): add codec engine ab matrix

* chore(get): drop unrelated dependency drift

* upgrade version

* upgrade version

* fix cargo deny
2026-06-27 13:27:16 +08:00
Zhengchao An 996e58fd9a refactor: route admin runtime config through app context (#3942) 2026-06-27 13:10:00 +08:00
Zhengchao An 1b3dea012e refactor: route ecstore runtime globals through facade (#3941) 2026-06-27 12:27:03 +08:00
Zhengchao An d3f1ff36af docs: record global state cleanup plan (#3939) 2026-06-27 12:04:59 +08:00
Zhengchao An fe9c0daf0f docs: add architecture readiness matrix (#3938) 2026-06-27 11:23:07 +08:00
Zhengchao An e1a4b9e0b6 refactor: batch cluster lock and health readiness (#3936) 2026-06-27 10:51:06 +08:00
GatewayJ 675597ec16 fix(ecstore): handle stalled recovery reads and listings (#3790)
* fix(ecstore): handle stalled recovery reads and listings

* fix(rio): start HTTP stall timeout on read

* fix(ecstore): handle stalled reads and partial lists

* fix(ecstore): retire stalled shards and list errors

* fix(ecstore): preserve list merge lookahead entries

* fix(ecstore): bound zero-copy shard reads

* fix(ecstore): hedge stalled shard reads

* fix(ecstore): retire abandoned shard reads

* fix(ecstore): include part identity in metadata quorum

* fix(ecstore): validate heal shard sources

* fix(ecstore): verify reconstructed read shards

* chore(ecstore): log slow object read stages

* fix(heal): throttle auto heal during recovery

* fix(scanner): yield to foreground reads

* fix(scanner): track streaming object reads

* fix(ecstore): avoid false read heal fanout

* fix(ecstore): verify codec streaming reconstruction sources

* fix(ecstore): preserve quorum progress on slow shards

* fix(storage): restore read timeout facade

* fix(ecstore): retain fallback readers after quorum

* chore: allow decode helper argument lists

---------

Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-06-27 10:21:09 +08:00
Zhengchao An 3fb4dcd52e refactor: route cluster control plane readiness (#3935) 2026-06-27 09:47:30 +08:00
Zhengchao An 0a5b1b1b3a refactor: consolidate ecstore owner module layout (#3934)
* refactor: shrink ecstore root owner facades

* refactor: remove ecstore core store root shims

* refactor: move ecstore erasure owner modules

* refactor: remove ecstore root rpc facade

* refactor: move ecstore services domain modules
2026-06-27 09:03:20 +08:00
cxymds 080363f10f feat: harden site replication control plane (#3842) 2026-06-27 08:35:49 +08:00
houseme ae4aa4f5d4 feat(get): add rustfs codec decode engine (#3928) 2026-06-27 08:35:38 +08:00
cxymds 688b14b572 fix(ecstore): preserve multipart part quorum errors (#3920)
* fix(ecstore): preserve multipart part quorum errors

* fix(ecstore): honor confirmed missing part quorum

* fix(ecstore): add multipart quorum diagnostics
2026-06-27 08:33:43 +08:00
Zhengchao An 27bb9c75dc refactor: move ecstore rpc metadata modules (#3933) 2026-06-27 07:19:45 +08:00
Zhengchao An c6ecfae39e refactor: move ecstore owner layout modules (#3932) 2026-06-27 05:54:25 +08:00
Zhengchao An 61b1296972 refactor: move ecstore store init support (#3931) 2026-06-27 05:05:01 +08:00
Zhengchao An bdfcde1866 refactor: move ecstore store support modules (#3930) 2026-06-27 03:51:11 +08:00
Zhengchao An 07428c8a34 refactor: move ecstore store owner roots (#3929) 2026-06-27 02:25:04 +08:00
houseme 8994a8ff00 feat(get): add local-first shard reads (#3924)
* feat(get): observe shard locality cost

* feat(get): add local-first shard reads

----

Co-Authored-By: Heihutu <heihutu@gmail.com>
2026-06-27 01:08:36 +08:00
Zhengchao An e5706ead0e ci: close config model ownership guard (#3927) 2026-06-27 00:25:36 +08:00
Zhengchao An 840d21d201 fix(deps): remove vulnerable thrift dependency (#3926) 2026-06-27 00:16:51 +08:00
houseme c0ddc14bb8 feat(get): observe shard locality cost (#3922)
* feat(get): observe shard locality cost

* fix(storage): avoid request counter underflow panic

* fix(storage): import put guard in concurrency tests
2026-06-26 23:50:37 +08:00
Zhengchao An 741b4dab7f refactor: close external storage API boundary guard (#3925) 2026-06-26 23:20:25 +08:00
Zhengchao An a22384708e refactor: decouple server health helpers (#3923) 2026-06-26 22:40:22 +08:00
Zhengchao An 15c39db42f refactor: consolidate storage owner boundary cleanup (#3921)
* refactor: segment app usecase contracts by domain

* refactor: segment root storage contracts by domain

* refactor: segment root runtime facades by domain

* refactor: segment storage owner consumers by domain

* refactor: route storage owner runtime consumers by domain

* refactor: make storage owner rpc imports explicit

* refactor: remove storage owner wildcard imports (#3917)

* refactor: make storage owner root exports explicit (#3918)

* refactor: route storage facades through owner api (#3919)

* refactor: finalize storage owner phase branch
2026-06-26 22:18:00 +08:00
houseme 2c04807f05 feat(get): add guarded metadata early stop (#3916)
* feat(get): add v2 metrics compatibility harness

* feat(get): observe metadata fanout quorum (#3915)

* feat(get): observe metadata fanout quorum

* fix(app): use storage ECStore type in app boundary

* feat(get): add guarded metadata early stop
2026-06-26 21:56:23 +08:00
cxymds 7820a55fdc fix: decouple readiness from cluster health (#3912)
* fix: include foreground write pressure

* fix: split readiness and cluster health probes

* fix: publish ready on node readiness

* fix: bound cluster health collection

* test: pin health probe collector routing

* fix: qualify app storage ECStore type

* test(admin): narrow runtime capabilities topology assertion

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-26 21:55:18 +08:00
Zhengchao An 6469d6ada8 refactor: segment admin storage contracts by domain (#3914) 2026-06-26 20:16:51 +08:00
houseme 5a78a9c416 feat(get): add v2 metrics compatibility harness (#3913)
* feat(get): add v2 metrics compatibility harness

* feat(get): observe metadata fanout quorum (#3915)

* feat(get): observe metadata fanout quorum

* fix(app): use storage ECStore type in app boundary

----
Co-Authored-By: heihutu <heihutu@gmail.com>
2026-06-26 20:09:09 +08:00
Zhengchao An a010dce93c refactor: segment storage owner contracts by domain (#3911) 2026-06-26 18:34:32 +08:00
Henry Guo a8327f8901 fix(scanner): account versioned delete markers in usage (#3904)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-26 18:34:21 +08:00
houseme 0321e4c6ca perf(get): reduce metrics and streaming handoff overhead (#3907) 2026-06-26 18:12:51 +08:00
cxymds 30957e2b51 fix(ecstore): throttle data movement under read pressure (#3906) 2026-06-26 17:48:09 +08:00
Zhengchao An b38976d5ee refactor: segment ECStore storage contracts by domain (#3910) 2026-06-26 17:47:36 +08:00
Zhengchao An 72a589f0c0 refactor: segment RustFS storage API root exports (#3909) 2026-06-26 17:15:33 +08:00
Zhengchao An 72ae43cb90 refactor: segment external storage contract imports (#3908) 2026-06-26 16:44:59 +08:00
Zhengchao An 6efbfff2c5 refactor: segment residual storage api boundaries (#3905) 2026-06-26 15:56:38 +08:00
cxymds ae641a33ac feat(admin): expose global heal progress (#3897) 2026-06-26 15:19:48 +08:00
cxymds 90638bfc19 fix(heal): add backpressure to repair admission (#3900) 2026-06-26 15:19:37 +08:00
Zhengchao An 1f7e159388 refactor: segment external storage api boundaries (#3903) 2026-06-26 15:10:49 +08:00
Zhengchao An 738b805ec0 refactor: segment app storage api boundary (#3902) 2026-06-26 14:45:05 +08:00
Zhengchao An 2a1bddfcca refactor: segment storage api domain boundaries (#3901) 2026-06-26 14:04:28 +08:00
Zhengchao An 92c50156a3 refactor: centralize runtime source boundaries (#3899) 2026-06-26 13:22:00 +08:00
Zhengchao An a038582325 refactor: route app usecase context lookup (#3896) 2026-06-26 12:47:48 +08:00
Zhengchao An 2301787afa refactor: centralize admin runtime globals (#3895) 2026-06-26 12:19:57 +08:00
Zhengchao An 4e87c4427f refactor: route admin usecase construction boundary (#3894) 2026-06-26 11:39:32 +08:00
Zhengchao An 1d5bf2699a refactor: publish oidc through app context (#3893) 2026-06-26 10:17:30 +08:00
Zhengchao An c9614eb7cb refactor: route ecstore storage api boundaries (#3892) 2026-06-26 09:35:18 +08:00
cxymds 0b9c8a7731 fix(heal): align scanner repair safety with quorum errors (#3887)
* fix(heal): avoid task-level delete on repair failure

* fix(scanner): avoid recreate for scanner heal checks

* fix(heal): preserve typed quorum failures

* test(heal): satisfy clippy bool assertion
2026-06-26 08:58:27 +08:00
Zhengchao An 97dce107f0 refactor: route storage owner contract imports (#3891) 2026-06-26 08:58:07 +08:00
Zhengchao An 2aca607119 refactor: move storage owner boundary aggregation (#3890) 2026-06-26 07:52:49 +08:00
Zhengchao An e37e367390 refactor: route remaining external storage boundaries (#3889) 2026-06-26 06:22:51 +08:00
Zhengchao An 4d6ea453a7 refactor: route scanner heal storage boundaries (#3888) 2026-06-26 05:24:41 +08:00
houseme f7a9a7142b chore: update deps and e2e cache action (#3885)
* chore(deps): update workspace crate versions

* ci: update e2e s3tests cache action
2026-06-26 04:19:28 +08:00
Zhengchao An 19c925c480 refactor: expand storage api boundaries (#3886)
* refactor: route root storage contracts

* refactor: expand storage api boundaries
2026-06-26 04:19:02 +08:00
houseme 7bf411f465 obs(get): add GET read pipeline metrics (#3868) 2026-06-26 04:02:02 +08:00
Zhengchao An 33b7f48f37 refactor: route root storage facades (#3867) 2026-06-25 23:23:45 +08:00
Zhengchao An 987ea4919f refactor: route admin storage facades (#3866) 2026-06-25 22:37:54 +08:00
cxymds 86e9362459 fix(heal): canonicalize scanner object-dir repairs (#3864) 2026-06-25 22:07:38 +08:00
Zhengchao An 1c973bc1d9 refactor: route app facade boundaries (#3865)
* refactor: route app storage error helpers

* refactor: route app bucket owner facades

* refactor: route app runtime facades
2026-06-25 21:57:42 +08:00
Zhengchao An 60c00f7622 refactor: route app and admin storage helpers (#3863)
* refactor: route admin storage helper imports

* refactor: route app storage io helper imports
2026-06-25 21:12:51 +08:00
Henry Guo 30f781871d fix(scanner): preserve dirty usage convergence (#3844)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-25 21:09:12 +08:00
cxymds 4cdc6ae63f fix(admin): refresh persisted pool states (#3861) 2026-06-25 21:08:55 +08:00
houseme ef0dcec479 feat(ecstore): add deeper zero-copy ingest experiment (#3847)
* feat(storage): add multipart put stage metrics

* feat(scripts): add multipart put focus runner

* docs(operations): add multipart put server-path guides

* chore(scripts): add local rustfs restart helper

* docs(observability): add local metrics backend guide

* docs(observability): add localized multipart guides

* fix(ecstore): validate multipart batching path

* feat(obs): add erasure encode overlap metrics

* docs(ops): update overlap retest summary

* docs(ops): add batchblocks retest matrix

* docs(ops): extend overlap candidate summary

* docs(ops): capture 8-run overlap summary

* feat(storage): switch rename_data to msgpack map

* test(storage): add rename_data payload checks

* feat(object): add zero_copy_eager put path

* docs(ops): add zero_copy_eager put guide

* docs(ops): add deeper zero-copy next steps

* feat(ecstore): add bytesmut erasure ingest gate

* docs(ops): add bytesmut ingest summary

* docs(ops): extend bytesmut ingest matrix summary

* docs(ops): extend bytesmut larger-object summary

* docs(ops): capture bytesmut variability summary

* chore(scripts): add deeper zero-copy capture flow

* chore(scripts): add deeper zero-copy capture support

* docs(ops): add capture-backed bytesmut retest

* docs(ops): update deeper zero-copy retests

* chore(docs): keep issue-712 notes local only

Co-Authored-By: heihutu <heihutu@gmail.com>
2026-06-25 21:05:00 +08:00
houseme 96b1f5c373 feat(storage): optimize gt1g get read path (#3860)
* feat(storage): tune gt1g get read path

* docs(ops): add gt1g get benchmark guide

* feat(scripts): add gt1g get fallback runner

* test(storage): trim gt1g get helper warnings

* test(storage): reduce gt1g get helper type complexity
2026-06-25 20:12:08 +08:00
houseme a30357d21e feat(storage): extend PUT path tuning and observability (#3829)
* feat(storage): add multipart put stage metrics

* feat(scripts): add multipart put focus runner

* docs(operations): add multipart put server-path guides

* chore(scripts): add local rustfs restart helper

* docs(observability): add local metrics backend guide

* docs(observability): add localized multipart guides

* fix(ecstore): validate multipart batching path

* feat(obs): add erasure encode overlap metrics

* docs(ops): update overlap retest summary

* docs(ops): add batchblocks retest matrix

* docs(ops): extend overlap candidate summary

* docs(ops): capture 8-run overlap summary

* feat(storage): switch rename_data to msgpack map

* test(storage): add rename_data payload checks

* feat(object): add zero_copy_eager put path

* docs(ops): add zero_copy_eager put guide

* docs(ops): add deeper zero-copy next steps
2026-06-25 19:24:35 +08:00
Zhengchao An 3942186f3d refactor: route app storage helper imports (#3856) 2026-06-25 18:10:49 +08:00
Zhengchao An a21f3c1a3f refactor: route app s3 api helpers (#3854) 2026-06-25 16:50:17 +08:00
Zhengchao An 2dd98b1f84 refactor: remove app dto wildcard imports (#3853) 2026-06-25 16:07:49 +08:00
Zhengchao An fe0227e9dc refactor: remove app storage wildcard imports (#3851) 2026-06-25 15:10:51 +08:00
cxymds 55cbfe256b fix(heal): skip scanner object-dir recreate misses (#3839) 2026-06-25 14:52:24 +08:00
GatewayJ 06fdbecf9d fix(s3tables): harden warehouse index races (#3849) 2026-06-25 14:35:24 +08:00
cxymds 4cb28abfc9 fix(scanner): reduce non-actionable scan noise (#3840) 2026-06-25 14:35:10 +08:00
Zhengchao An bfb2a95e3d refactor: route admin context imports (#3848) 2026-06-25 14:27:25 +08:00
Zhengchao An 46fa28f542 refactor: centralize root runtime sources (#3846) 2026-06-25 13:19:52 +08:00
Zhengchao An 1c4bcd4372 refactor: centralize admin runtime sources (#3845) 2026-06-25 13:09:02 +08:00
Henry Guo 1c4a279909 feat(table-catalog): add durable strong backing (#3836) 2026-06-25 12:22:30 +08:00
Zhengchao An 928074a97d refactor: centralize server storage runtime sources (#3843) 2026-06-25 12:20:53 +08:00
Zhengchao An 839cab47ce refactor: centralize startup runtime sources (#3838) 2026-06-25 11:31:38 +08:00
GatewayJ fe5961cc16 fix(s3tables): harden warehouse index consistency (#3835) 2026-06-25 11:31:25 +08:00
Henry Guo 1d6a8259c5 feat(table-catalog): add partition-local compaction (#3832)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-25 11:14:40 +08:00
Zhengchao An 873aea21ca refactor: centralize app context runtime fallbacks (#3837) 2026-06-25 11:00:42 +08:00
Rokton 758677dafe fix(heal): Add podAnnotations to statefulset and deployment templates (#3831) 2026-06-25 09:39:11 +08:00
cxymds e6f04bed47 fix(heal): skip missing object-dir candidates (#3834) 2026-06-25 09:38:08 +08:00
cxymds c6ef37d5a7 fix(server): handle MinIO cluster health probe (#3833) 2026-06-25 09:37:57 +08:00
Zhengchao An ad6184b126 refactor: centralize iam runtime source helpers (#3828) 2026-06-24 22:49:24 +08:00
Zhengchao An a935854b32 refactor: centralize scanner runtime source helpers (#3827)
* refactor: batch lifecycle runtime source handles

* refactor: centralize scanner runtime source helpers
2026-06-24 22:05:03 +08:00
Zhengchao An 5cc8bd7952 refactor: centralize ecstore rpc test runtime sources (#3818) 2026-06-24 21:18:22 +08:00
Henry Guo 90f6371e29 fix(scanner): clear deleted bucket usage stats (#3822)
* fix(scanner): clear deleted bucket usage stats

* fix(scanner): preserve usage timestamp on bucket cleanup

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-24 21:08:32 +08:00
houseme 623fc801f1 feat(replication): support custom TLS for bucket targets (#3825)
* feat(replication): support insecure https bucket targets

* feat(replication): support custom ca bucket targets

* test(replication): satisfy e2e clippy for TLS helpers

* fix(replication): avoid native root panic for custom trust stores

* test(replication): decouple private IP target test from TLS roots

* test(replication): use target TLS client in private IP unit test
2026-06-24 20:23:20 +08:00
Henry Guo fcd0c9ec0f test(table-catalog): add atomic catalog backing fixture (#3821) 2026-06-24 19:05:13 +08:00
houseme efebcb66be feat(perf): add large PUT tuning and encode optimization (#3816) 2026-06-24 19:04:04 +08:00
houseme 3c6e9acfcb fix: route cluster imports through ecstore_cluster module (#3819) 2026-06-24 19:03:31 +08:00
cxymds 0ffd561daf fix(health): decouple liveness and lock RPC deadlines (#3824) 2026-06-24 18:34:51 +08:00
cxymds 57e12e56d2 fix(admin): correct pool lifecycle status mapping (#3823) 2026-06-24 17:05:07 +08:00
cxymds eef8f43251 fix: harden lock timeouts, health, and heal paths (#3815) 2026-06-24 15:48:06 +08:00
Zhengchao An 94034ef406 refactor: centralize ecstore batch sources and aliases (#3817) 2026-06-24 14:12:48 +08:00
houseme cbf526df87 refactor(admin): migrate OIDC consumers to app context (#3800) 2026-06-24 12:51:53 +08:00
GatewayJ 89dfb1357c fix(s3tables): harden catalog guard checks (#3801) 2026-06-24 12:51:40 +08:00
houseme 31b5db9a75 feat(admin): expose cluster snapshot view (#3803) 2026-06-24 12:51:26 +08:00
cxymds 5eda332fee fix(heal): recover shards after node rejoin (#3814) 2026-06-24 12:51:16 +08:00
Zhengchao An aa089f18f2 refactor: centralize ecstore accessor runtime sources (#3813) 2026-06-24 11:05:30 +08:00
Zhengchao An 1a8ff1ea3e refactor: centralize ecstore setup runtime sources (#3812) 2026-06-24 10:41:33 +08:00
Hiroaki KAWAI 55b402ec2e fix(scanner): avoid retrying usage-cache lock failures (#3809)
Avoid retrying scanner cache lock failures
2026-06-24 10:22:43 +08:00
Hiroaki KAWAI fb06897650 fix(lock): respect requested acquire timeout (#3804)
Respect requested lock acquire timeout

Co-authored-by: cxymds <cxymds@gmail.com>
2026-06-24 10:22:25 +08:00
Zhengchao An 6b1b3800d7 refactor: centralize ecstore owner runtime sources (#3811) 2026-06-24 10:00:36 +08:00
Zhengchao An 3f624bf06c refactor: centralize ecstore bucket runtime sources (#3810) 2026-06-24 09:41:11 +08:00
Zhengchao An 17ae335313 chore: split pre-pr quality gate (#3807) 2026-06-24 08:43:05 +08:00
Zhengchao An 5b6c486b13 refactor: centralize ecstore locality runtime sources (#3808) 2026-06-24 08:42:36 +08:00
Zhengchao An 1c04b86494 refactor: centralize ecstore bucket runtime sources (#3806) 2026-06-24 08:00:04 +08:00
cxymds c14a442586 fix(storage): harden rebalance and decommission state (#3730) 2026-06-24 07:59:39 +08:00
Zhengchao An 5046f788be refactor: centralize ecstore lifecycle runtime sources (#3805) 2026-06-24 07:23:13 +08:00
Zhengchao An 7e60432588 refactor: centralize ecstore runtime owner sources (#3798) 2026-06-24 06:34:06 +08:00
Zhengchao An 1735dcde9c refactor: centralize ecstore data-plane runtime sources (#3797) 2026-06-23 23:23:16 +08:00
Zhengchao An e59e1852ec refactor: centralize network client runtime sources (#3796) 2026-06-23 22:35:51 +08:00
Zhengchao An 726c26fa01 refactor: centralize RIO HTTP runtime sources (#3795) 2026-06-23 21:55:05 +08:00
houseme 0a00d8d500 perf(server): lighten internode data-plane stack (#3735)
* refactor(server): split internode dispatch scaffold

* test(server): cover internode dispatch prefix split

* refactor(server): name internode stack boundaries

* perf(server): skip internode request logging layer

* perf(server): skip internode trace layer

* perf(server): use lite internode request context

* feat(metrics): track internode rpc duration

* feat(ecstore): add put object stage summary logs

* test(metrics): update internode descriptor expectations

* fix(server): tighten internode path matching

* fix(pr): address review follow-up comments

* style(ecstore): simplify commit tail duration field

* refactor(ecstore): group put stage summary fields

* refactor(ecstore): inline put stage summary log

* fix(s3): return storage class for object attributes

* merge: sync latest main and resolve object attributes conflict

* fmt

* fix(server): remove duplicate rpc imports

* build(deps): bump memmap2 for RUSTSEC-2026-0186

* fix(s3select): align object_store with datafusion

* chore(deps): prune workspace dependencies

* perf(fuzz): optimize CI runtime with build/run split and matrix parallelization

  Separate fuzz harness compilation from execution to eliminate redundant
  builds across targets. Introduce matrix-based parallel execution for
  PR smoke and nightly fuzz jobs.

  Changes:

  - Split CI workflow into `fuzz-build` (compile once) and matrix run jobs
    (`pr-fuzz-smoke`, `nightly-fuzz-corpus`) that execute targets in parallel
  - Add `BUILD_ONLY` mode to run_ci_targets.sh / run_nightly_targets.sh
  - Add run_single_target.sh for matrix jobs (no build phase)
  - Optimize `local_metadata` fuzz target: reduce prefix iterations from
    8-10 (4 functions each) to 5 critical prefixes (parser-only), cutting
    per-iteration cost by ~3-5x
  - Move archive path validation (`validate_extract_relative_path`,
    `normalize_extract_entry_key`) from `rustfs` to `rustfs-utils::path`,
    eliminating `rustfs` binary crate dependency from fuzz harness
  - Remove `rustfs` from fuzz/Cargo.toml (drops significant transitive deps)
  - Add unit tests for archive path validation in rustfs-utils
  - Update fuzz/README.md with new workflow and script documentation

  Expected CI improvement: PR smoke wall-clock from ~120min (frequent
  timeout) to ~40min; nightly from ~180min to ~60min.

* refactor(fuzz): consolidate scripts and fix prefix test alignment

Replace three duplicated shell scripts (run_ci_targets.sh,
run_nightly_targets.sh, run_single_target.sh) with a single
parameterized run.sh that supports BUILD_ONLY, SKIP_BUILD, and
MAX_TOTAL_TIME environment variables.

Fix local_metadata fuzz target prefix testing: replace always-true
'len > 0' guard with lengths aligned to xl.meta binary layout
(4/5/8/12 bytes for magic+version+header fields). Remove redundant
empty-slice test.

Hoist RUSTFLAGS to workflow top-level env to eliminate per-job
duplication. Update README with unified script documentation.

Net: -118 lines, zero functionality loss.

* perf(fuzz): optimize CI runtime with build/run split and matrix parallelization

Restructure fuzz CI workflow to eliminate redundant compilation and
run targets in parallel via matrix strategy.

Workflow changes:
- Split into fuzz-build (compile once) and matrix run jobs
- PR smoke: 3 targets parallel, 60s each, timeout 30min (was 120min)
- Nightly: 3 targets parallel, 300s each, timeout 60min (was 180min)
- Pass compiled harness via actions/artifact between jobs
- Hoist RUSTFLAGS to workflow top-level env

Script consolidation:
- Replace 3 duplicated scripts with single parameterized run.sh
- Supports BUILD_ONLY, SKIP_BUILD, MAX_TOTAL_TIME env vars

Target optimizations:
- Remove rustfs binary crate from fuzz dependencies (was pulling
  979 transitive deps); move archive path validation to rustfs-utils
- Optimize local_metadata: reduce prefix iterations from 8-10x4
  calls to 5 prefixes with parser-only (no decompress), aligned
  with xl.meta binary layout (4/5/8/12 bytes)
- Add unit tests for archive path validation in rustfs-utils
- Update fuzz/README.md with unified script documentation

Expected: PR smoke wall-clock from ~120min (frequent timeout)
to ~40min; nightly from ~180min to ~60min.

* fix(rpc): resolve internode metrics via app context

* fmt

* ci: speed up fuzz smoke artifact restore

---------

Signed-off-by: houseme <housemecn@gmail.com>
2026-06-23 21:36:39 +08:00
Henry Guo 7bdb25ae9d feat(scanner): define replication boundary contract (#3630)
* feat(scanner): define replication boundary contract

* fix(scanner): propagate replication boundary metrics

* fix(scanner): preserve repair metadata on merge

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-23 21:35:36 +08:00
唐小鸭 eff656e086 fix(storage): restore legacy SSE-S3 read compatibility (#3584)
* Update .gitignore

* Fix. fixed SSE-S3 compatibility issues in large-scale testing

* fix

* fix(ecstore): reject whitespace bucket names

* Update replication_extension_test.rs

* style(ecstore): format bucket whitespace test

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <cxymds@gmail.com>
2026-06-23 21:35:17 +08:00
Zhengchao An 5c60f0cae9 refactor: centralize owner server config reads (#3793) 2026-06-23 21:27:30 +08:00
Zhengchao An 70a2441407 refactor: route notify dispatch through app context (#3789)
* refactor: route notify dispatch through app context

* refactor: route admin IAM globals through app context (#3791)

* refactor: centralize IAM root credential access (#3792)
2026-06-23 20:05:28 +08:00
houseme da565c11bc refactor: migrate readiness and site replication app context (#3788)
* refactor(runtime): route readiness lock clients via app context

* refactor(admin): route site replication iam via app context

* refactor(admin): route site replication oidc via app context
2026-06-23 19:37:34 +08:00
houseme 46775ae019 feat: harden runtime capability snapshots (#3784)
* feat(admin): expose runtime capability snapshots

* feat(runtime): refine workload admission snapshots

* test(ci): align architecture migration checks

* build(deps): bump memmap2 for RUSTSEC-2026-0186

* build(deps): refresh cargo deny lockfile
2026-06-23 18:10:49 +08:00
Zhengchao An fd2ab38581 refactor: route IAM reads through app context (#3786) 2026-06-23 17:57:55 +08:00
Zhengchao An 61cfd4fc13 refactor: route runtime consumers through app context (#3785) 2026-06-23 17:21:34 +08:00
Zhengchao An a62e2b6c94 refactor: route action credentials through app context (#3783) 2026-06-23 16:29:46 +08:00
houseme e42c6df0e8 fix(runtime): remove high-impact unwrap paths (#3755)
* fix(runtime): remove high-impact unwrap paths

* fix(runtime): propagate managed SSE metadata errors

* fix(runtime): add typed OPA config errors

* fix(runtime): harden config and credential helpers

* fix(runtime): remove SSE hmac unwraps

* fix(runtime): complete SSE helper error propagation

* fix(trusted-proxies): avoid legacy global init panics

* test(credentials): allow deprecated rpc token check

* fix(storage): harden object lock retention parsing

* chore(checks): refresh layer dependency baseline

* chore(checks): refresh layer dependency baseline

* Update layer-dependency-baseline.txt

Signed-off-by: houseme <housemecn@gmail.com>

* test(context): avoid clone on copy boot time

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-06-23 15:12:47 +08:00
Zhengchao An 825c01060c refactor: route admin config writes through app context (#3782) 2026-06-23 13:46:13 +08:00
Zhengchao An 4ef899dd5c refactor: route admin kms manager through app context (#3781) 2026-06-23 13:18:38 +08:00
Zhengchao An d8a53d090f refactor: route admin status metrics through app context (#3780) 2026-06-23 13:14:00 +08:00
Zhengchao An cb585c7fcf refactor: route admin replication stats through app context (#3779) 2026-06-23 13:09:50 +08:00
Zhengchao An 08d092562c refactor: route site replication tls through app context (#3778) 2026-06-23 12:56:54 +08:00
houseme a6878e8fce fix(runtime): remove startup panic fallbacks (#3754)
* fix(runtime): remove startup panic fallbacks

* test(runtime): cover buffer profile fallback safety

* test(runtime): reduce panic-style assertions

* fix(runtime): expose fallible env config setup

* test(runtime): simplify permit acquisition assertion

* test(runtime): tighten operation helper assertions

* fix(filemeta): stop panicking on invalid free version ids

* fix(init): satisfy buffer profile clippy lints

* fix(lock): harden fast lock config construction

* chore(checks): refresh layer dependency baseline

---------

Signed-off-by: houseme <housemecn@gmail.com>
2026-06-23 12:31:26 +08:00
houseme 583a23bdf2 fix(ecstore): replace panic-driven pool and set stubs (#3753)
* fix(ecstore): replace panic-driven pool and set stubs

* test(runtime): tolerate restricted local bind checks

* fix(ecstore): remove remaining trait stub placeholders

* fix(ecstore): tighten trait stub follow-up semantics

* chore: ignore local worktrees

* chore: update layer dependency baseline for resolve_* context entries

Add 7 accepted infra->app dependency entries introduced by recent
refactoring PRs (#3770, #3771, #3772) that route global state lookups
through app::context::resolve_* functions.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-06-23 12:31:17 +08:00
Zhengchao An c421e73fef refactor: route site replication iam through app context (#3777) 2026-06-23 12:20:57 +08:00
Zhengchao An 0ebd3911c8 refactor: route admin peer systems through app context (#3776) 2026-06-23 11:45:07 +08:00
Zhengchao An 2fe059811a refactor: route admin topology reads through app context (#3774)
* refactor: route admin topology reads through app context

* chore: accept app context layer baseline entries
2026-06-23 11:22:20 +08:00
Zhengchao An 0acc8f8b43 refactor: route admin runtime reads through app context (#3773) 2026-06-23 10:30:55 +08:00
Zhengchao An 9615086154 refactor: route RPC node globals through app context (#3772) 2026-06-23 10:06:54 +08:00
Zhengchao An 747db971ee refactor: route RPC IAM through app context (#3771) 2026-06-23 09:18:25 +08:00
Zhengchao An 087f794901 refactor: route readiness through app context (#3770) 2026-06-23 08:28:19 +08:00
Zhengchao An 7499dd085d refactor: collapse app notify thin compat boundaries (#3768)
* refactor: collapse app notify thin compat boundaries

* refactor: route app runtime consumers through context (#3769)
2026-06-23 08:13:58 +08:00
Zhengchao An 30559f7e1b refactor: collapse test fuzz ecstore thin bridges (#3765) 2026-06-23 07:04:51 +08:00
Zhengchao An 198fd4f150 refactor(runtime): route RustFS runtime consumers through storage owner (#3756) 2026-06-23 05:17:56 +08:00
Zhengchao An e0b79aa00c refactor: expose test fuzz owner symbols (#3752) 2026-06-23 01:11:46 +08:00
Zhengchao An 6da61b44e5 refactor: expose remaining external owner symbols (#3751) 2026-06-22 23:35:50 +08:00
Zhengchao An ec5f112205 refactor: expose scanner ECStore owner symbols (#3749) 2026-06-22 22:26:25 +08:00
Zhengchao An db7ff8f513 refactor: expose external ECStore owner symbols (#3748) 2026-06-22 21:42:54 +08:00
Zhengchao An 418b5d04f9 refactor: route external ECStore imports through owners (#3747) 2026-06-22 20:57:10 +08:00
Zhengchao An 88a87b3e8f refactor: centralize external ECStore facade aliases (#3746) 2026-06-22 20:37:13 +08:00
Zhengchao An 89805ffb4d refactor: route RustFS ECStore consumers via owner boundary (#3745) 2026-06-22 19:29:01 +08:00
houseme a53ccb2407 fix(ecstore): reject symlink path escapes (#3742)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-06-22 19:28:40 +08:00
houseme 2f85ef4455 fix(security): extend outbound egress guard (#3744) 2026-06-22 19:20:31 +08:00
houseme 64aef06c60 fix(replication): harden site-repl versioning sync (#3739) 2026-06-22 19:18:42 +08:00
Zhengchao An e57962d5e8 refactor: remove RustFS owner compat bridges (#3743) 2026-06-22 18:55:59 +08:00
Zhengchao An d3796d6c10 refactor: remove external owner compat bridges (#3741) 2026-06-22 18:28:35 +08:00
Zhengchao An 539c68778d refactor: remove standalone compat bridge modules (#3740) 2026-06-22 17:35:58 +08:00
Zhengchao An b63b076275 refactor: remove remaining compatibility bridges (#3738) 2026-06-22 16:53:14 +08:00
安正超 a66350b645 refactor: remove secondary compatibility bridges (#3737)
* refactor: remove app admin secondary compat bridges

* refactor: remove storage core compat bridge

* refactor: remove nested compat bridges

* refactor: remove admin handlers compat bridge

* refactor: remove runtime local compat bridges
2026-06-22 16:29:33 +08:00
安正超 599e3632d1 refactor: use relative fuzz compat consumers (#3736) 2026-06-22 15:44:59 +08:00
安正超 cca9e83a8b refactor: use relative standalone compat consumers (#3734) 2026-06-22 15:07:58 +08:00
安正超 7d81183cf1 refactor: use relative heal test compat consumers (#3733) 2026-06-22 14:55:12 +08:00
安正超 a3202eb6bb refactor: use relative app compat consumers (#3731) 2026-06-22 14:37:20 +08:00
安正超 1a709e14c0 refactor: use relative admin compat consumers (#3729) 2026-06-22 14:06:49 +08:00
houseme cc6909b08b fix(iam): verify sts temp-user persistence (#3722) 2026-06-22 13:54:57 +08:00
houseme 6353f2093d fix(multipart): include upload owner metadata (#3728) 2026-06-22 13:54:37 +08:00
houseme da5ff226ba fix(object): normalize storage class responses (#3726) 2026-06-22 13:54:12 +08:00
houseme 7bfa80cb0b fix(notify): normalize event time precision (#3725) 2026-06-22 13:53:56 +08:00
houseme 2c500d55b7 fix(server): add MinIO health endpoint aliases (#3724) 2026-06-22 13:53:38 +08:00
houseme ed45d1be2d fix(lock): recover stale distributed object guards (#3720)
fix(lock): reclaim expired stale object guards
2026-06-22 13:53:21 +08:00
安正超 c8de6cac75 refactor: use relative storage compat consumers (#3727) 2026-06-22 13:52:45 +08:00
安正超 da08e411f1 refactor: use relative local compat consumers (#3723) 2026-06-22 13:22:48 +08:00
cxymds 2f25cf606e fix(storage): harden rebalance decommission state (#3515) 2026-06-22 12:03:13 +08:00
安正超 2dcc32db5a refactor: use relative root compat paths (#3721) 2026-06-22 12:02:37 +08:00
安正超 30fa7e2ad4 refactor: use relative local compat paths (#3719) 2026-06-22 11:13:37 +08:00
安正超 abb5f41726 refactor: collapse compat facade self refs (#3718) 2026-06-22 10:36:51 +08:00
houseme baed4d5315 fix(object): bound snowball auto extract (#3715) 2026-06-22 09:54:19 +08:00
dependabot[bot] 5a601869f6 chore(deps): bump sysinfo from 0.39.3 to 0.39.4 in the dependencies group (#3712) 2026-06-22 09:53:57 +08:00
安正超 de158743c3 refactor: split compat facade imports (#3716) 2026-06-22 09:53:22 +08:00
安正超 89552e1df0 refactor: guard root compat facade aliases (#3714) 2026-06-22 09:17:11 +08:00
安正超 6ff2824cbb refactor: narrow remaining local compat exports (#3713) 2026-06-22 09:10:23 +08:00
安正超 6287483053 refactor: narrow local compat re-exports (#3711) 2026-06-22 08:51:08 +08:00
安正超 c7f7ba667f refactor: localize storage core compat consumers (#3710) 2026-06-22 07:50:55 +08:00
安正超 76af78385b refactor: localize owner storage compat consumers (#3709) 2026-06-22 07:19:22 +08:00
安正超 1b718498f4 refactor: retire root storage compat facade (#3708) 2026-06-22 07:06:08 +08:00
安正超 c13f42e9a0 refactor: wrap disk rpc compat trait methods (#3707) 2026-06-22 06:31:24 +08:00
安正超 7eb9a4759e refactor: wrap bucket compat trait methods (#3706) 2026-06-22 04:32:37 +08:00
安正超 b8a0fc4e04 refactor: prune root e2e raw facade paths (#3705) 2026-06-22 03:30:03 +08:00
安正超 d39815470e refactor: prune consumer storage compat paths (#3704) 2026-06-22 02:31:30 +08:00
安正超 e528ee9452 refactor: prune app admin raw facade paths (#3703) 2026-06-22 02:25:58 +08:00
安正超 2a18088ca5 refactor: centralize storage compat facade paths (#3702) 2026-06-22 01:20:22 +08:00
安正超 eb33e9f2ea refactor: prune outer compat signature aliases (#3701) 2026-06-22 00:15:34 +08:00
安正超 ef111a3e40 refactor: prune outer compat facade aliases (#3700) 2026-06-21 23:33:44 +08:00
安正超 6b3d96fde3 refactor: prune trait import compat re-exports (#3699) 2026-06-21 23:17:58 +08:00
安正超 f35821f75d test(s3): reclassify passing atomic conditional write (#3698) 2026-06-21 20:54:52 +08:00
houseme 9e6d0c7292 chore(deps): refresh workflow actions and crate pins (#3696) 2026-06-21 20:29:17 +08:00
安正超 a0e7957e92 refactor: prune admin app compat re-exports (#3697) 2026-06-21 20:28:43 +08:00
安正超 5c01641760 refactor: prune storage owner compat re-exports (#3695) 2026-06-21 19:36:10 +08:00
安正超 48ed331fc8 refactor: prune root runtime compat re-exports (#3694) 2026-06-21 18:42:25 +08:00
安正超 0cf9d07e40 refactor: prune test protocol compat aliases (#3693) 2026-06-21 18:09:39 +08:00
安正超 30b46640b4 refactor: prune edge compat aliases (#3692) 2026-06-21 17:41:07 +08:00
安正超 9d510e9d5b refactor: prune admin app compat aliases (#3690) 2026-06-21 16:40:40 +08:00
安正超 84d662c312 refactor: prune storage compat bucket aliases (#3689) 2026-06-21 15:56:13 +08:00
安正超 6e42afcea5 refactor: prune admin config compat aliases (#3688) 2026-06-21 15:21:25 +08:00
安正超 f4f303a773 refactor: prune root runtime compat aliases (#3687) 2026-06-21 14:39:22 +08:00
安正超 6bfed0bd43 refactor: prune root bucket compat modules (#3686) 2026-06-21 14:08:05 +08:00
安正超 32d7e55564 refactor: compose workload admission providers (#3685) 2026-06-21 13:36:12 +08:00
安正超 7b81f2ed53 refactor: consume storage concurrency policies (#3684) 2026-06-21 12:44:44 +08:00
安正超 60b0fbf075 refactor: bridge storage concurrency policies (#3683) 2026-06-21 11:56:51 +08:00
安正超 81f11df4dd refactor: add cluster status snapshots (#3682) 2026-06-21 11:25:44 +08:00
安正超 15cdeee775 refactor: add ecstore cluster read models (#3681) 2026-06-21 10:45:36 +08:00
安正超 d26d98104a refactor: prune ecstore root global facades (#3680) 2026-06-21 09:52:19 +08:00
安正超 d4150117c3 refactor: narrow startup and ecstore root facades (#3679)
* refactor: narrow startup owner visibility

* refactor: prune remaining ecstore root facades
2026-06-21 09:42:15 +08:00
安正超 0985225448 refactor: consolidate ecstore public facade (#3678)
* refactor: expand ecstore compatibility facade

* test: enforce ecstore api facade imports

* refactor: hide legacy ecstore layout modules

* refactor: hide ecstore facade root modules
2026-06-21 09:09:11 +08:00
安正超 77e005ee98 refactor: narrow startup compatibility shims (#3677) 2026-06-21 08:54:21 +08:00
houseme 5f3722e0a5 chore(deps): update flake.lock (#3676)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/49a4bd0' (2026-06-12)
  → 'github:NixOS/nixpkgs/3e41b24' (2026-06-16)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/5b929d8' (2026-06-13)
  → 'github:oxalica/rust-overlay/e8e2021' (2026-06-20)
2026-06-21 08:39:45 +08:00
安正超 74571cc4ca fix(format): remove whitespace regressions (#3675) 2026-06-21 08:39:26 +08:00
GatewayJ c47dea0009 fix(scanner): preserve complete usage snapshots (#3654) 2026-06-21 08:21:20 +08:00
安正超 796f293b80 test(ecstore): cover DNS resolver raw errors (#3674) 2026-06-21 08:17:45 +08:00
安正超 9b3fda9e30 refactor: route ecstore public surfaces through api (#3673) 2026-06-21 05:23:23 +08:00
安正超 28771f071e refactor: narrow startup orchestration visibility (#3672) 2026-06-21 04:13:55 +08:00
安正超 995af8ebdc refactor: split startup runtime service ownership (#3671)
* refactor: move startup kms context owner

* refactor: move startup iam runtime facade

* refactor: split startup metadata and notification owners

* refactor: split startup runtime service owners

* refactor: retire startup service components

* refactor: narrow startup owner module visibility
2026-06-21 02:53:58 +08:00
安正超 468fd81a8a refactor: move startup app context owner (#3670) 2026-06-21 01:56:27 +08:00
安正超 6912c6d000 refactor: make app context bootstrap explicit (#3669) 2026-06-21 00:28:52 +08:00
安正超 d7c6c07d77 refactor: keep embedded identity with startup config (#3668) 2026-06-21 00:05:50 +08:00
GatewayJ 3e7e39a59c fix(ecstore): retry transient endpoint DNS failures (#3652)
* fix(ecstore): retry transient endpoint DNS failures

* fix(ecstore): satisfy DNS retry clippy check

* fix(ecstore): harden endpoint DNS retry

* test(ecstore): stabilize DNS retry error test
2026-06-20 23:53:11 +08:00
安正超 6056a518b8 refactor: streamline embedded startup result shell (#3667) 2026-06-20 23:52:33 +08:00
Hiroaki KAWAI deee13901d fix(logging): suppress scanner leader eviction noise (#3653) 2026-06-20 22:23:42 +08:00
安正超 1c58ee0d7e refactor: return embedded identity from startup (#3666) 2026-06-20 22:20:26 +08:00
安正超 6d13a0f15a refactor: keep embedded builder as startup shell (#3665) 2026-06-20 22:17:31 +08:00
安正超 150ac967d3 refactor: extract embedded handle boundaries (#3664) 2026-06-20 22:12:50 +08:00
Henry Guo ed1986a5ac feat(table-catalog): harden credential and maintenance ops (#3631)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-20 22:11:18 +08:00
安正超 ebd4882ec1 refactor: extract embedded build orchestration (#3663) 2026-06-20 22:10:40 +08:00
安正超 0cf7e5cf03 refactor: extract embedded startup control helpers (#3661) 2026-06-20 21:57:03 +08:00
安正超 84e292c6da refactor: extract embedded startup server helpers (#3660) 2026-06-20 18:38:02 +08:00
安正超 3edff6a436 refactor: extract embedded runtime orchestration hooks (#3659) 2026-06-20 18:20:56 +08:00
安正超 d933d5e938 refactor: move ecstore rebalance flow modules (#3658) 2026-06-20 17:56:11 +08:00
安正超 1595b2bc3f refactor: move ecstore rebalance migration helpers (#3657) 2026-06-20 17:15:30 +08:00
安正超 6d82b6f855 refactor: move ecstore rebalance worker helpers (#3656) 2026-06-20 16:44:48 +08:00
安正超 f5f727e077 refactor: move ecstore rebalance metadata helpers (#3655) 2026-06-20 16:31:58 +08:00
安正超 f5bd31173d refactor: move ecstore pool-space builder (#3651) 2026-06-20 15:38:49 +08:00
安正超 1d9fd3d7ac refactor: move ecstore rebalance support helpers (#3650) 2026-06-20 13:34:23 +08:00
安正超 aad5379e0c refactor: move ecstore pool-space helpers (#3649) 2026-06-20 12:44:16 +08:00
安正超 3035cad082 refactor: move ecstore set heal helpers (#3648) 2026-06-20 12:03:22 +08:00
安正超 1ea7b36ab4 refactor: move ecstore endpoint layout owners (#3647) 2026-06-20 10:28:25 +08:00
安正超 16d667da0a refactor: move ecstore format layout owners (#3646) 2026-06-20 09:58:50 +08:00
安正超 88dfdda4a0 refactor: establish ecstore layout foundation (#3645) 2026-06-20 09:54:36 +08:00
安正超 4ba8058dbc refactor: expose extension runtime snapshots (#3644) 2026-06-20 09:48:49 +08:00
安正超 5ad47d1354 refactor: publish ops profiler runtime contract (#3643) 2026-06-20 09:42:13 +08:00
安正超 9a3d5bd2a0 refactor: reuse embedded lifecycle publication boundaries (#3642) 2026-06-20 09:15:00 +08:00
安正超 77d842ae34 refactor: reuse embedded runtime service boundaries (#3641) 2026-06-20 08:15:44 +08:00
安正超 131e9dc804 refactor: reuse embedded startup phase boundaries (#3640) 2026-06-20 06:58:48 +08:00
安正超 f8117eb46b refactor: extract startup tls material boundary (#3639) 2026-06-20 05:55:32 +08:00
安正超 46a02b6d03 refactor: extract startup runtime hook boundary (#3638) 2026-06-20 05:32:49 +08:00
安正超 5e96151d9c refactor: extract optional runtime sidecar boundary (#3637) 2026-06-20 04:28:33 +08:00
安正超 aa5de1c908 refactor: extract startup service component boundary (#3636) 2026-06-20 03:26:09 +08:00
安正超 7dfe372bd2 refactor: extract startup ready lifecycle boundary (#3635) 2026-06-20 02:25:20 +08:00
安正超 e9037f9eb0 refactor: extract startup shutdown lifecycle boundary (#3634) 2026-06-20 01:15:39 +08:00
安正超 eec1792cd2 refactor: extract optional runtime startup boundary (#3633) 2026-06-20 00:12:44 +08:00
安正超 74fcfddd7f refactor: extract optional runtime shutdown boundary (#3632) 2026-06-19 22:53:42 +08:00
安正超 e6391598f0 feat: add ops profiler capability snapshots (#3629) 2026-06-19 22:35:36 +08:00
安正超 ff638d1140 feat: add ops profiler extension schema (#3628) 2026-06-19 22:31:18 +08:00
Henry Guo 71809ba0bb test(scanner): add usage freshness validation evidence (#3627) 2026-06-19 21:34:12 +08:00
Henry Guo 2052336a53 fix(heal): track root erasure-set rebuild status (#3625) 2026-06-19 21:33:58 +08:00
Henry Guo 68a061c548 feat(table-catalog): add vendor compatibility profiles (#3624) 2026-06-19 21:33:31 +08:00
安正超 afe46c01e7 refactor: prune consumer ecstore object aliases (#3622) 2026-06-19 21:33:00 +08:00
安正超 08b8f58e64 refactor: prune notify ecstore object alias (#3620) 2026-06-19 20:11:56 +08:00
安正超 a5058e955d test: guard ecstore object api boundaries (#3619) 2026-06-19 19:04:20 +08:00
Henry Guo 8d91e2116f docs(table-catalog): document S3 Tables support matrix (#3616) 2026-06-19 18:37:08 +08:00
Henry Guo a689ff3c39 feat(scanner): wake cycles for dirty usage refresh (#3617) 2026-06-19 18:36:48 +08:00
安正超 54ffbedbc8 refactor: remove ecstore operation compat facades (#3608) 2026-06-19 17:01:12 +08:00
安正超 132c8ae77d feat: expose runtime workload owner snapshots (#3607) 2026-06-19 12:31:43 +08:00
安正超 7cb7aefc3b feat: expose replication workload admission snapshot (#3606) 2026-06-19 11:13:17 +08:00
安正超 00ca3b7c1c feat: expose heal workload admission snapshot (#3605) 2026-06-19 10:39:09 +08:00
houseme 8cf3c0bfbd fix(ecstore): support MinIO DARE fixture compatibility (#3590) 2026-06-19 10:13:39 +08:00
安正超 80b1fca02a feat: wire runtime capability snapshot providers (#3604) 2026-06-19 09:50:42 +08:00
安正超 b106b628c1 feat: expose local scheduler admission snapshots (#3603) 2026-06-19 09:14:50 +08:00
houseme 53ff19dd2d fix(kms): harden local master key storage (#3589) 2026-06-19 08:50:35 +08:00
Henry Guo 2c6881695f test(table-catalog): add Iceberg compatibility and failure coverage (#3581)
* test(table-catalog): add Iceberg client compatibility coverage

* test(table-catalog): add production failure coverage

* fix(table-catalog): generate valid failure probe requests

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-19 08:49:59 +08:00
安正超 56c3cf50ae feat: add scheduler workload admission contracts (#3602) 2026-06-19 08:41:22 +08:00
安正超 ada6f7587e refactor: flatten test harness storage compat aliases (#3596)
* refactor: flatten test harness storage compat aliases

* refactor: flatten rustfs storage compat aliases (#3597)

* refactor: prune runtime storage compat surface (#3598)

* refactor: flatten runtime secondary storage compat (#3599)

* docs: add scheduler placement profiling baselines (#3600)

* feat: add observability topology capability contracts (#3601)
2026-06-19 08:30:47 +08:00
安正超 b1c6578df1 refactor: narrow test harness compatibility surfaces (#3592) 2026-06-19 07:10:52 +08:00
安正超 b14e49e84e refactor: narrow IAM and Swift compatibility surfaces (#3587)
* refactor: narrow IAM and Swift compatibility surfaces

* refactor: narrow heal and scanner compatibility surfaces (#3588)

* refactor: narrow RustFS runtime compatibility surfaces (#3591)
2026-06-19 03:06:57 +08:00
houseme 9b1272529a fix(ecstore): avoid blocking fs in async paths (#3586) 2026-06-19 01:44:17 +08:00
安正超 a860f2b40c refactor: narrow storage compatibility surfaces (#3585)
* refactor: narrow storage compatibility surfaces

* refactor: narrow observability compatibility surfaces
2026-06-19 00:56:14 +08:00
安正超 f11b07bf83 chore: guard ECStore compat passthroughs (#3583) 2026-06-19 00:20:09 +08:00
安正超 205f964dc5 refactor: tighten storage compat store_api aliases (#3582)
* refactor: collapse storage compat store_api modules

* chore: guard storage compat store_api aliases
2026-06-18 22:50:02 +08:00
安正超 c28fee0013 refactor: continue storage api contract cleanup (#3580)
* refactor: move delete object contracts to storage api

* refactor: narrow store api compatibility exports

* refactor: route table catalog test through storage compat
2026-06-18 22:42:02 +08:00
Henry Guo 80144ec886 feat(scanner): expose usage freshness status (#3577)
* feat(scanner): expose usage freshness status

* fix(scanner): reset dirty usage cycle state

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-18 21:14:55 +08:00
Henry Guo 5d9fee5c0c feat(table-catalog): expose backing migration contract (#3574)
* feat(table-catalog): expose backing migration contract

* fix(table-catalog): require register auth for external sync

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-18 21:14:40 +08:00
安正超 7b0cb9e725 refactor: prune storage compatibility re-export allowances (#3579)
* refactor: prune storage compatibility re-export allowances

* fix: reject whitespace-padded dot path segments
2026-06-18 21:14:24 +08:00
安正超 f3fcdd4ba2 refactor: narrow remaining storage compatibility exports (#3578) 2026-06-18 19:18:32 +08:00
安正超 08371a6e09 refactor: narrow storage compatibility exports (#3576) 2026-06-18 18:31:14 +08:00
安正超 c098184c49 refactor: clean runtime and test storage boundaries (#3573)
* refactor: clean runtime observability select boundaries

* refactor: clean test harness fuzz storage boundaries
2026-06-18 18:09:25 +08:00
安正超 8313767f75 refactor: clean app storage admin runtime boundaries (#3572) 2026-06-18 16:42:51 +08:00
安正超 bdb2461b55 refactor: clean scanner heal runtime boundaries (#3571) 2026-06-18 16:12:27 +08:00
Henry Guo 51409c40ef feat(table-catalog): add external catalog bridge sync (#3569)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-18 15:30:41 +08:00
houseme 8cf11af649 fix(security): add shared outbound egress guard (#3567)
* fix(security): block unsafe outbound webhook targets

* chore: keep issue-3557 plan local only
2026-06-18 15:30:09 +08:00
houseme fc93a27974 fix(storage): harden local SSE-S3 fallback (#3564)
* fix: harden local SSE-S3 fallback

* test: update managed SSE-S3 assumptions

* chore: keep issue plan local only

* test(ci): seed local SSE key for s3-tests
2026-06-18 15:29:52 +08:00
安正超 c76e4dc6bc refactor: clean Swift ECStore boundaries (#3570) 2026-06-18 15:18:55 +08:00
安正超 0e7e2660bb refactor: clean remaining storage DTO imports (#3568) 2026-06-18 14:44:46 +08:00
安正超 acdf439371 refactor: clean external DTO consumers (#3566)
* refactor: clean external DTO consumers

* fix: handle empty erasure shard recovery
2026-06-18 14:22:44 +08:00
安正超 99941f7e7c refactor: clean external operation consumers (#3565) 2026-06-18 12:39:41 +08:00
houseme d2a135b397 fix(admin): require admin auth for metrics (#3562)
fix: require admin auth for metrics
2026-06-18 12:39:16 +08:00
GatewayJ a30cafa73f fix(filemeta): detect physical data dirs for overwrite cleanup (#3510) 2026-06-18 11:58:12 +08:00
安正超 24443c829d refactor: clean shared list operation consumers (#3563) 2026-06-18 11:57:46 +08:00
Henry Guo 46e200c22b feat(scanner): prioritize dirty usage refresh work (#3538) 2026-06-18 11:03:23 +08:00
houseme 32aca6d835 feat(fuzz): scaffold cargo-fuzz harness for smoke targets (#3537) 2026-06-18 11:02:56 +08:00
Henry Guo ea70d5697a test(table-catalog): harden smoke catalog probes (#3535) 2026-06-18 11:01:09 +08:00
安正超 57403525ee refactor: move heal and namespace contracts (#3560) 2026-06-18 11:00:21 +08:00
安正超 e5cad7ed20 refactor: move object operation contracts (#3559) 2026-06-18 09:48:13 +08:00
安正超 3fb4cb3d65 refactor: move list operations contract (#3550) 2026-06-18 08:48:18 +08:00
安正超 36f7ad6936 refactor: move walk options contract (#3549) 2026-06-18 08:18:26 +08:00
Henry Guo 604379d62f feat(table-catalog): harden table row-level commit conflicts (#3517)
* feat(table-catalog): harden snapshot conflict validation

* feat: harden table row-level commit conflicts

* fix: accept additional row-level snapshot shapes

* test(table-catalog): expand smoke catalog probes

* fix(table-catalog): satisfy row-level clippy checks

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-18 07:32:20 +08:00
安正超 24fa03e04b refactor: move object list response contracts (#3548) 2026-06-18 07:19:07 +08:00
安正超 80ed63484b refactor: move object precondition contracts (#3547) 2026-06-18 06:34:30 +08:00
安正超 54bbd05b25 refactor: move object list helper contracts (#3546) 2026-06-18 06:09:33 +08:00
houseme 87a3d107d6 test(property): add path validator invariants (#3541)
test(property): add path validator invariants for #3525
2026-06-18 02:14:24 +08:00
houseme bc769948ab test(property): add erasure recoverability invariants (#3540) 2026-06-18 01:34:07 +08:00
安正超 53337a6f71 refactor: move HTTP range helper contracts (#3533) 2026-06-18 00:47:23 +08:00
houseme cc84d9065e test(property): add filemeta invariants (#3536)
* test(property): add filemeta invariants for #3523

* docs: relocate issue-3518 property testing plan

* docs: keep issue-3518 property plan local only
2026-06-18 00:36:55 +08:00
houseme 841f86fe90 test(property): expand parser invariants (#3534)
* test(property): harden parser invariants for #3522

* test(property): satisfy clippy for #3522
2026-06-17 23:52:30 +08:00
安正超 919deeb816 refactor: move object option helper contracts (#3521) 2026-06-17 22:56:10 +08:00
Henry Guo a97122ab80 fix(heal): retry recoverable bucket heal failures (#3509)
* fix(heal): retry recoverable bucket heal failures

* fix(heal): keep merged heal tokens cancellable

* fix(heal): group queue processing context

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-17 22:45:29 +08:00
Henry Guo a9aba323c6 fix(scanner): restore prompt bucket usage scans (#3516)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-17 21:49:56 +08:00
houseme 8d24d9133b perf(put): comprehensive PUT performance optimization (#3514)
* perf(put): add eager path metrics and isolation tooling

* fix(decommission): persist progress adaptively (#3497)

Persist decommission progress after either the existing time interval or a migrated-item threshold, and flush progress baselines after bucket and terminal-state saves.

Also stabilize the OIDC discovery mock used by the pre-commit gate.

* refactor: move bucket operations contract (#3507)

* fix(s3): handle multipart flexible checksums (#3508)

* fix(io-core): avoid blocking on pooled buffer return

* perf(put): add slow inflight diagnostics

* perf(put): fix 16KiB regression with threshold and pool bypass

- Lower SMALL_EAGER_PUT_MAX_SIZE from 256KB to 8KB so objects >8KiB
  use the streaming BufReader path (matches baseline behavior)
- Add POOL_BYPASE_MAX_SIZE (16KiB) to bypass BytesPool for very small
  objects, avoiding Small-tier Mutex contention under high concurrency
- Add read_small_put_body_exact_direct() for direct Vec<u8> allocation
- Fix stale test assertions to match new 8KB threshold

Root cause analysis: the 16KiB regression was primarily caused by
instrumentation overhead in set_disk.rs (4x Instant::now() + metrics
per PUT), not BytesPool contention. Lowering the threshold eliminates
the eager-path overhead for 16KiB+ objects.

* perf(put): gate stage metrics behind observability flag

Add put_stage_metrics_enabled() AtomicBool switch in io-metrics crate.
When disabled (default), record_put_object_path() and
record_put_object_stage_duration() are no-ops, avoiding unnecessary
histogram/counter macro overhead in the PUT hot path.

The flag is set to true during startup when OTEL metric export is
enabled (rustfs_obs::observability_metric_enabled() == true).

This eliminates the per-request metrics overhead that contributed
to the 16KiB PUT regression when metrics collection is not active.

* perf(put): comprehensive optimization - restore eager path, cache env, remove UUID

Change 1: Restore SMALL_EAGER_PUT_MAX_SIZE from 8KB to 1MB
- The try_lock() fix (d13a189e3) eliminates the blocking that caused
  service health timeouts under 512KiB c64 load
- Eager path with BytesPool is now safe for objects up to 1MB
- Recovers the eager path benefit for 32KiB-256KiB objects

Change 2: Adjust POOL_BYPASE_MAX_SIZE from 16KB to 4KB
- With eager path restored to 1MB, objects 4KB-1MB benefit from pool reuse
- Only ≤4KB objects bypass the pool (allocation cost negligible)

Change 3: Cache RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES via OnceLock
- Eliminates per-encode std::env::var() syscall
- Env var still works (read once at first use)

Change 4: Replace Uuid::new_v4() with Uuid::nil() in Erasure construction
- _id field is unused in hot paths (documented in code)
- Eliminates CSPRNG syscall per PUT request

Change 5: Add concurrency-aware buffer sizing to PUT path
- Reuses get_concurrency_aware_buffer_size() from GET path
- Reduces buffer size under high concurrency (0.4x at >8 concurrent)
- Lowers memory pressure for >1MB streaming PUTs

* chore: add pyroscope feature flag and clean up imports

- Add pyroscope feature flag forwarding to rustfs-obs
- Remove unused allow(non_upper_case_globals) in globals.rs
- Sort imports and fix Cargo.toml formatting consistency

* style: fix import ordering and code formatting

- Sort imports alphabetically in globals.rs, encode.rs
- Fix indentation in erasure_coding encode/erasure
- Clean up HashReader formatting in object_usecase.rs

* fix(test): use tokio::test for request_logging_layer tests

The tests call tokio::spawn via RequestContextLayer, which requires a
Tokio runtime. Changed from #[test] + futures::executor::block_on to
#[tokio::test] + .await, and replaced tracing::subscriber::with_default
with tracing::subscriber::set_default to support async.

* fix(bench): normalize no-space throughput/latency parsing in to_bps/to_ms

When a benchmark tool prints throughput without a separator (e.g. 123MiB/s),
awk '{print $2}' returns empty because the whole string is one field,
causing to_bps to return N/A and losing valid measurements in CSV output.

Insert a space between number and unit via sed before awk field splitting.
Same fix applied to to_ms for latency values like '50ms'.

Also add TODO comment on PUT path noting that get_concurrency_aware_buffer_size
reads ACTIVE_GET_REQUESTS instead of PUT concurrency (PR #3514 review).

Refs: PR #3514 review comments by chatgpt-codex-connector

* fix(metrics): correct POOL_BYPASS comments and separate PUT vs generic stage metrics

- Fix 3 comment-code mismatches: POOL_BYPASS_MAX_SIZE is 4KiB, not 16KiB
- Add generic record_stage_duration() with separate histogram
  (rustfs_internal_stage_duration_ms) for non-PUT paths
- Replace record_put_object_stage_duration with record_stage_duration in
  metacache_set, store_list_objects, and bucket_lifecycle_ops to avoid
  polluting PUT-specific dashboards with listing/lifecycle timings
- Fix flaky test: serialize tests mutating PUT_STAGE_METRICS_ENABLED with
  METRICS_FLAG_LOCK mutex and explicitly set desired state at test start

Refs: PR #3514 review comments by chatgpt-codex-connector

* style: apply cargo fmt to metacache_set.rs

---------

Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-06-17 21:19:11 +08:00
majinghe a58692f550 fix(heal): bind service account to sts and deployment (#3513) 2026-06-17 18:36:24 +08:00
安正超 38bbba4c5c fix(s3): handle multipart flexible checksums (#3508) 2026-06-17 11:54:58 +08:00
安正超 ed55857bca refactor: move bucket operations contract (#3507) 2026-06-17 09:10:38 +08:00
cxymds 5b36ef5556 fix(decommission): persist progress adaptively (#3497)
Persist decommission progress after either the existing time interval or a migrated-item threshold, and flush progress baselines after bucket and terminal-state saves.

Also stabilize the OIDC discovery mock used by the pre-commit gate.
2026-06-17 08:17:59 +08:00
安正超 1830911973 fix(s3): return content disposition on get object (#3504) 2026-06-17 02:23:13 +08:00
安正超 0c259547d1 refactor: move multipart DTO contracts (#3505) 2026-06-17 00:33:06 +08:00
安正超 2ff69ae21c refactor: remove ecstore bucket DTO aliases (#3503) 2026-06-16 22:10:07 +08:00
cxymds d094d91925 fix(site-replication): harden service account sync (#3500) 2026-06-16 21:20:13 +08:00
Henry Guo cd96491b39 feat(table-catalog): add refs and views semantics (#3502)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-16 21:19:27 +08:00
安正超 966756d788 test: guard storage contract cleanup (#3501) 2026-06-16 21:18:52 +08:00
cxymds c405470f73 test(decommission): cover tier lifecycle recovery flows (#3494)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-16 16:48:24 +08:00
cxymds 0f37c5675e fix(ecstore): bound decommission bucket concurrency (#3498) 2026-06-16 15:11:54 +08:00
cxymds ba0ba0d35e fix(lifecycle): persist remote tier delete journal tasks (#3493)
fix: persist remote tier delete journal tasks
2026-06-16 10:38:41 +08:00
Henry Guo b5d19e6595 test(scanner): close parity validation harness (#3486) 2026-06-16 08:46:22 +08:00
Adnan Hussain Turki 401e876209 fix(admin): reject access keys and names containing any whitespace (#3488) 2026-06-16 08:46:05 +08:00
cxymds 156e21c90e fix(lifecycle): make tier cleanup recoverable (#3491) 2026-06-16 08:45:49 +08:00
Henry Guo d23ed02b3f feat(table-catalog): add parquet compaction commit (#3487)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-16 08:02:18 +08:00
安正超 0baf90ee88 refactor: remove storage api facade (#3490) 2026-06-16 07:46:32 +08:00
安正超 c26593fa7a refactor: clean config storage boundaries (#3489) 2026-06-16 06:35:32 +08:00
安正超 fbab160c2b refactor: narrow replication storage consumers (#3485) 2026-06-15 22:29:38 +08:00
Henry Guo 65c724c786 feat(table-catalog): add manifest reachability cleanup (#3484)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-15 22:29:23 +08:00
Henry Guo b387689f26 feat(heal): expose scanner-aware operations status (#3483)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-15 22:28:53 +08:00
Ramakrishna Chilaka 956bd19417 perf(ecstore): reuse erasure-decode shard buffers across stripes (#3482)
* perf(ecstore): reuse erasure-decode shard buffers across stripes

ParallelReader::read allocated and zero-filled a fresh
`vec![0u8; shard_size]` per shard on every erasure stripe. Erasure::decode
builds one ParallelReader and loops stripes, so those buffers can be
reused: BitrotReader::read overwrites buf[..n] and the caller truncates to
n, so leftover bytes from the prior stripe are never observed. The decode
loop now hands each stripe's shard buffers back to the reader, which
reuses them (resized to shard_size) on the next stripe. The heal path,
which consumes its buffers into Bytes, never hands them back and is
unchanged.

This path runs on every object GET. Streaming-decode benchmark
(Erasure::decode end-to-end, median):

  4+2 16MiB 1MiB-block:  -9.6% verify-on,  -23.8% verify-off
  6+3 24MiB 1MiB-block:  -9.7% verify-on,  -22.5% verify-off
  4+2  4MiB 64KiB-block: -5.5% verify-on,  -16.3% verify-off

Adds a streaming-decode benchmark covering the ParallelReader path and a
regression test that decodes a multi-stripe object with missing data
shards (so reconstructed buffers are recycled) with bitrot verification
both on and off, asserting byte-exact output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* perf(ecstore): tighten decode buffer reuse and bench harness

Address PR review feedback on the erasure-decode buffer-reuse change:

- Only claim a recycled shard buffer inside the `Some(reader)` branch of
  `ParallelReader::read`, so the take is co-located with the read that
  uses it and missing shards no longer touch the recycle slot.
- Move per-shard `BitrotReader` construction into Criterion `iter_batched`
  setup so reader/UUID construction stays out of the timed decode path,
  and assert decode success plus full output length each iteration.

Re-measured with the cleaner harness (median, before -> after, all
p < 0.05): verify-on -5.3%..-11.7%, verify-off -15.8%..-25.3%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* perf(ecstore): drop dead recycle slots and tidy decode bench

Address the second PR review pass:

- In `Erasure::decode`, clear recycled slots for missing-shard (None-reader)
  indices before handing buffers back. `ParallelReader::read` only reuses
  slots with an active reader, and `decode_data` always re-allocates the
  reconstructed shard, so retaining those buffers only held memory that could
  never be reused in degraded reads.
- Move the output sink into the benchmark's `iter_batched` setup and validate
  decode success/output length once per config instead of inside the timed
  loop, so neither sink construction nor assertions touch the measurement.

Re-measured with the final harness (median, before -> after, all p < 0.05):
verify-on -5.2%..-9.8%, verify-off -19.4%..-25.8%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 22:28:39 +08:00
Hamza 49c0f13120 Update podman instructions in README (#3479)
Added correct volume mounting option in rootless podman command.

Signed-off-by: Hamza <23738574+w4hf@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-15 19:18:17 +08:00
houseme adec195486 chore(scanner): enable pulsar telemetry feature (#3481) 2026-06-15 18:43:14 +08:00
Henry Guo 7ce9838027 feat(table-catalog): add maintenance worker runtime (#3480) 2026-06-15 18:42:29 +08:00
安正超 f782f94f6a refactor: narrow storage namespace consumers (#3477) 2026-06-15 18:32:21 +08:00
Henry Guo 307d788da1 feat(table-catalog): add production iceberg surfaces (#3473) 2026-06-15 16:53:14 +08:00
Henry Guo d28aedfbb3 feat(scanner): expose replication repair kind metrics (#3476) 2026-06-15 16:52:39 +08:00
安正超 5063524b36 ci: guard migration loss-prevention coverage (#3475) 2026-06-15 16:32:41 +08:00
Henry Guo dd6b4c35ad fix(scanner): harden lifecycle and tiering backlog (#3469)
* fix(scanner): expose lifecycle transition backlog

* fix(scanner): expose lifecycle expiry backlog

* fix(scanner): preserve lifecycle backlog states

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-15 15:01:21 +08:00
安正超 57260c8314 refactor: centralize startup command bootstrap (#3471) 2026-06-15 14:55:33 +08:00
Henry Guo 941b8afee8 feat(table-catalog): add snapshot maintenance foundation (#3470)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-15 14:08:28 +08:00
houseme 73a48c06e5 refactor(logging): align API response boundary events (#3466)
* refactor(logging): align api response boundary events

* refactor(logging): align heal admin boundary events

* refactor(logging): restore admin callsite line numbers

* refactor(logging): simplify audit target error logging

* refactor(logging): restore rpc callsite line numbers
2026-06-15 14:07:54 +08:00
安正超 218f473fb5 refactor: centralize startup lifecycle runtime (#3468) 2026-06-15 14:07:24 +08:00
安正超 e26eea7f36 refactor: centralize startup runtime services (#3467) 2026-06-15 13:24:17 +08:00
安正超 3e723a2476 refactor: centralize startup storage runtime (#3465) 2026-06-15 12:12:27 +08:00
Henry Guo 46fb4bdc2f feat(scanner): add source-aware maintenance controls (#3461)
* feat(scanner): add source-aware maintenance controls

* fix(scanner): preserve source control between cycles

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-15 12:12:05 +08:00
Henry Guo f68967de7a feat(table-catalog): add maintenance reachability reports (#3462)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-15 12:11:53 +08:00
dependabot[bot] fbf1f9e5b8 build(deps): bump brotli from 8.0.3 to 8.0.4 in the dependencies group (#3458)
* build(deps): bump brotli from 8.0.3 to 8.0.4 in the dependencies group

Bumps the dependencies group with 1 update: [brotli](https://github.com/dropbox/rust-brotli).


Updates `brotli` from 8.0.3 to 8.0.4
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/compare/8.0.3...8.0.4)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): bump s3s from cf4c3346 to c59b62f7ba in the dependencies group

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-15 12:06:01 +08:00
abdullahnah92 a19560da41 fix(bucket-repl): persist MRF retry queue to disk and reload on startup (#3456)
* fix(bucket-repl): persist MRF retry queue to disk and reload on startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(bucket-repl): address three blocking MRF issues from review

1. MRF replay loses delete operations — add `MrfOpKind` discriminator to
   `MrfReplicateEntry` (Object | Delete, default=Object for backward
   compat).  `DeletedObjectReplicationInfo::to_mrf_entry` now persists
   `op=Delete`, `version_id`, `delete_marker_version_id`, and
   `delete_marker`.  `start_mrf_processor` branches on `op`: delete
   entries skip `get_object_info` and replay via
   `schedule_replication_delete` with `ReplicationType::Heal`; object
   entries follow the existing heal path.

2. `flush_mrf_to_disk` cleared the in-memory batch even on encode/write
   failure — changed return type to `bool` and callers now only
   `pending.clear()` on `true`, so a transient storage error retries
   on the next tick instead of silently dropping the batch.

3. Add focused tests: encode/decode roundtrips for object, delete-marker,
   versioned-delete, and mixed-batch entries; a routing test confirming
   op-kind propagates correctly and that the default is Object for
   legacy files; a legacy-compat test verifying old entries round-trip
   cleanly through the new format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: fix clippy redundant-clone in MRF tests

Replace &[entry.clone()] with std::slice::from_ref(&entry) in two
encode_mrf_file call sites flagged by clippy's redundant_clone lint
under --all-targets --features rio-v2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(bucket-repl): strengthen legacy MRF compat test with hand-built msgpack

The previous mrf_legacy_file_without_op_field_decoded_as_object test
round-tripped through encode_mrf_file, so it exercised the new format
and never touched a truly-legacy payload.

Replace it with a hand-built msgpack payload that genuinely omits the
"op", "deleteMarker", and "deleteMarkerVersionID" keys — exactly what
the old binary would have written before MrfOpKind existed.  The test
now fails if #[serde(default)] is removed from the op field, which
proves real backward compatibility rather than round-trip stability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-15 11:56:50 +08:00
安正超 c99d6086cd refactor: centralize startup storage bootstrap (#3464) 2026-06-15 11:23:16 +08:00
安正超 6508f88d3a refactor: centralize startup listen bootstrap (#3460) 2026-06-15 09:41:11 +08:00
abdullahnah92 c2e0792f6f fix(scanner): skip startup delay when replication is active so failed… (#3455)
fix(scanner): skip startup delay when replication is active so failed objects heal promptly after restart

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-15 09:02:21 +08:00
安正超 513f06f268 refactor: centralize startup server preflight (#3459) 2026-06-15 08:32:44 +08:00
安正超 2288b1bb63 refactor: centralize startup runtime bootstrap (#3457) 2026-06-15 07:57:36 +08:00
Henry Guo 73e534682e feat(table-catalog): add catalog recovery diagnostics (#3444)
* feat(table-catalog): add commit recovery observability

* feat(table-catalog): harden catalog recovery diagnostics

* test(table-catalog): cover recovery admin route policy

* test(table-catalog): update recovery route count

* fix(table-catalog): satisfy duration clippy lint

* fix(obs): promote span request id field

* Update local.rs

Signed-off-by: houseme <housemecn@gmail.com>

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-15 07:06:29 +08:00
Henry Guo 2c5615d2ea feat(scanner): expose distributed metrics (#3452)
* feat(scanner): expose distributed metrics

* docs(scanner): clarify distributed metrics collection

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-15 07:05:43 +08:00
Ramakrishna Chilaka e4c1ee9941 fix(server): actionable 501 for virtual-hosted-style S3 requests (#3442) 2026-06-15 06:49:31 +08:00
安正超 f8e300bf3b refactor: centralize startup protocol bootstrap (#3450) 2026-06-15 06:48:55 +08:00
houseme 036741cb1c refactor(request-id): align contracts and lock field names (#3454)
* refactor(request-id): align header log and trace contracts

* test(contract): lock external request-id field names

* chore(deps): drop unused rustfs-ecstore links

Co-Authored-By: heihutu <heihutu@gmail.com>
2026-06-15 01:59:11 +08:00
houseme efaf07d323 feat: preserve request ids across async recovery logs (#3451)
* feat(obs): promote request ids in structured logs

* refactor(tracing): propagate spans into request tasks

* test(ecstore): baseline recovery monitor log chains

* fix(replication): reduce startup resync log noise

* chore(docs): stop tracking local recovery baseline

* chore(obs): polish request id logging cleanup
2026-06-14 23:16:04 +08:00
安正超 9499391370 refactor: centralize startup service bootstrap (#3448) 2026-06-14 21:59:17 +08:00
安正超 b2e6cc520b docs: update security advisory lessons (#3447) 2026-06-14 21:51:54 +08:00
安正超 b49057c8e3 refactor: centralize startup readiness bootstrap (#3446) 2026-06-14 21:24:07 +08:00
安正超 8d23ce06c6 refactor: consolidate app object store fallback (#3445) 2026-06-14 20:43:42 +08:00
houseme fc894c9b50 fix(obs): harden startup and shutdown logging (#3443) 2026-06-14 20:16:00 +08:00
安正超 323302255c refactor: route ecstore internals through object resolver (#3441) 2026-06-14 19:55:21 +08:00
cxymds bf3a3a6189 fix(rebalance): ignore source cleanup failures (#3440) 2026-06-14 19:50:26 +08:00
cxymds 8f6b1d47b5 chore(obs): standardize runtime logging batch one (#3438) 2026-06-14 18:55:14 +08:00
安正超 bed4c7288b refactor: route server storage lookups through resolver (#3439) 2026-06-14 18:42:21 +08:00
Henry Guo 9372ee7032 feat(table-catalog): bridge table data-plane policy (#3436)
* feat(table-catalog): bridge table data-plane policy

* test(table-catalog): harden vended credential smoke

* test(table-catalog): cover data-plane policy denials

* fix(table-catalog): protect relocated warehouse scope

* fix(table-catalog): skip invalid warehouse entries

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-14 18:13:02 +08:00
GatewayJ e29c136ad5 fix(table-catalog): stabilize standard commit metadata names (#3435) 2026-06-14 18:03:42 +08:00
安正超 84f7027e90 refactor: add object store resolver for standalone crates (#3437) 2026-06-14 18:02:39 +08:00
安正超 59f99a30e2 refactor: prefer app context object store in admin zip (#3434) 2026-06-14 16:37:08 +08:00
GatewayJ 3928117c8f fix(policy): preserve IAM policy readback shape (#3431) 2026-06-14 16:36:28 +08:00
Henry Guo fc17e75fb2 test(table-catalog): verify vended credential data-plane scope (#3429) 2026-06-14 16:35:54 +08:00
安正超 5e68cf8a29 refactor: prefer app context object store in ecfs (#3433) 2026-06-14 15:52:26 +08:00
cxymds 046d5386ba feat: stream object zip downloads (#3380)
* feat: stream object zip downloads

* fix: stream zip downloads page by page

* fix: prepare zip downloads before streaming

---------

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-14 14:26:44 +08:00
安正超 bdeee173c8 refactor: prefer app context object store in admin (#3432)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-14 13:49:03 +08:00
houseme e8012bd1ba refactor(logging): normalize admin telemetry and error messages (#3430) 2026-06-14 13:27:10 +08:00
Henry Guo dc82efbab4 test(scanner): add validation harness (#3428)
* test(scanner): add validation harness

* fix(scanner): harden validation harness

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-14 12:40:05 +08:00
houseme 61a5500232 chore(deps): update flake.lock (#3423)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/cbb5cf3' (2026-06-06)
  → 'github:NixOS/nixpkgs/49a4bd0' (2026-06-12)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/27b7e78' (2026-06-06)
  → 'github:oxalica/rust-overlay/5b929d8' (2026-06-13)
2026-06-14 11:46:45 +08:00
安正超 6fcd62ba56 refactor: prefer app context object store in usecases (#3425) 2026-06-14 09:38:12 +08:00
安正超 bed875d3e0 fix(logging): remove guardrail script trailing whitespace (#3424) 2026-06-14 08:35:59 +08:00
houseme efa89a98ed refactor(logging): standardize protocol and observability events (#3419)
* refactor(logging): standardize object capacity events

* refactor(logging): standardize protocol server events

* refactor(logging): standardize swift protocol events

* refactor(logging): standardize observability events

* refactor(logging): move masking helper and extend guardrails
2026-06-14 07:14:45 +08:00
abdullahnah92 22460243bf fix(bucket-repl): honor op_type in replicate_object so ExistingObject… (#3420)
fix(bucket-repl): honor op_type in replicate_object so ExistingObject resync respects DISABLED targets

replicate_object was calling filter_target_arns with hard-coded
op_type: Object and existing_object: false regardless of what was
stored in roi.op_type. This meant that a resync worker setting
roi.op_type = ExistingObject (resync_bucket, line 889) had no effect
on target filtering: all configured targets were included, even ones
whose rule had ExistingObjectReplicationStatus::DISABLED.

Fix: pass op_type: roi.op_type and derive existing_object from it
(true only for ExistingObject, not Heal — Heal intentionally bypasses
the existing-object opt-out to repair past failures).

Also add warn! logs at all four MRF channel-overflow sites that were
previously silently returning Missed with no observability.

Verified with a live two-instance test: after resync, objects reached
the ENABLED target and were correctly blocked from the DISABLED target.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-14 07:14:11 +08:00
cxymds 339a192a09 fix(ecstore): preserve bucket visibility during rebalance (#3418)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-14 03:37:54 +08:00
Henry Guo 357287d626 feat(table-catalog): issue scoped table credentials (#3413)
* feat(table-catalog): issue scoped table credentials

* fix(table-catalog): harden credential vending defaults

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-14 02:20:28 +08:00
houseme 7da10db852 refactor(logging): standardize heal and scanner events (#3414)
* refactor(logging): standardize heal and scanner events

* chore(git): untrack local logging governance note

* chore(git): ignore local logging governance note
2026-06-14 01:47:39 +08:00
houseme 9059a9c68d refactor(logging): standardize concurrency and trusted proxy events (#3417)
* refactor(logging): standardize concurrency and proxy events

* chore(logging): extend guardrails for concurrency and proxies

* feat(skill): add rustfs logging governance skill
2026-06-14 01:00:26 +08:00
安正超 88d2c6ad5c test: cover app context resolver compatibility (#3416) 2026-06-14 00:24:08 +08:00
abdullahnah92 cb37a64a81 fix(site-replication): bidirectional sync, pre-existing data back-fill, and related fixes (#3401)
* fix(site-repl): clamp replication_cfg_mismatch to owning deployments and add resync status branch

Fix 3: replication_cfg_mismatch was set on ALL deployments for a bucket when
any replication-config discrepancy existed, even on deployments that simply have
no config. mc computes "in sync" as max_buckets minus per-deployment mismatch
entries; with N deployments all flagged for 1 bucket, the result underflows to
1-N = -1. Now only deployments that own a replication config are flagged.

Fix 4: SiteReplicationResyncOpHandler accepted "start" and "cancel" but returned
an empty body (and later a parse error on the client) for any other operation
string. Add a SITE_REPL_RESYNC_STATUS="status" arm that returns the stored
SRResyncOpStatus or an explicit {"status":"not-found"} object so mc never sees
an unexpected end of JSON input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(site-repl): derive sync_state from reachability and replication rule completeness

PeerInfo.sync_state was set to SyncStatus::Unknown at every construction site
and never updated from real signals. build_status_info now tracks which peers
were reachable during the metainfo fetch phase and, after all bucket stats are
merged, derives sync_state as:
  - Enable  : reachable AND no replication_cfg_mismatch for any bucket
  - Disable : reachable BUT at least one bucket has incomplete/missing rules
  - Unknown : unreachable (fetch failed)

The "Sync" column in mc admin replicate status will now reflect actual state
rather than always showing Unknown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(site-repl): add SRRotateServiceAccountHandler for split-brain svc-acct repair

When site-replicator-0 gets desynced (different secret on different peers), calls
via the old service account return 403, blocking remove and replicate operations
with no in-band recovery path.

SiteReplicationRemoveHandler already purges local state unconditionally before
sending peer notifications, so force-remove works locally even when peers reject
the 403. The new POST /v3/site-replication/rotate-svc-acct endpoint provides the
missing recovery path: it generates a fresh service-account secret, applies it
locally, and pushes a peer/join to every member. Each peer's SRPeerJoinHandler
accepts the join idempotently (update if exists, create otherwise), repairing the
desynced credential without a full teardown + restart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(site-repl): back-fill pre-existing buckets and objects on replicate add

SiteReplicationAddHandler and SRPeerJoinHandler both called persist_site_replication_state
and returned success without doing anything for buckets that existed before the
sites were linked. Only buckets created after the link (via site_replication_make_bucket_hook)
ever replicated.

Introduce backfill_existing_buckets_after_add which, after persisting the new
state, iterates every local bucket and:
  1. Ensures versioning is enabled (required by replication).
  2. Reconciles bucket targets so a target entry exists for each remote peer.
  3. Reconciles the replication config so a rule pointing to each peer is present.
  4. Broadcasts a make-bucket-hook to peers (idempotent) so they create the bucket.
  5. Kicks start_site_bucket_resync toward every remote peer so pre-existing
     objects travel across.

Errors per bucket are logged but never abort the overall add — manual resync
remains available as a fallback. Both the initiator and the receiving (join) side
run the backfill so convergence happens regardless of which side held data first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(site-repl): reconcile replication config to restore bidirectional replication

Root cause: ensure_site_replication_bucket_replication_config bailed with Ok(())
the moment any replication config was found on the bucket. When bucket B was
propagated to site-2 via the make-with-versioning bucket-op, site-2's
configure-replication step loaded the freshly-written config and immediately
returned, never adding the reverse-direction rule pointing back to site-1. Result:
objects uploaded to site-2 failed with "replication head_object fallback failed
... service error" because no rule targeted the originating site.

Fix: drop the early-return. Instead load the existing rules, build the full
desired config via build_site_replication_config, and MERGE — adding only the
rules that are absent (identified by their "site-repl-<deployment_id>" id). Re-
number priorities after each merge to avoid conflicts. Existing non-site-repl
rules are preserved. The write is skipped entirely when all desired rules are
already present, so repeat calls remain cheap.

Together with Fix 1 (backfill), any file written to ANY member now replicates to
ALL members. The offline-and-recover case also benefits: when a peer returns,
the per-bucket rules are complete and the resyncer can catch up.

Also register the new rotate-svc-acct route in route_policy and
route_registration_test so the route inventory assertions stay green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(replication): remove broken resync worker-signal gate

The resync_bucket task opened a new broadcast receiver via
worker_rx.resubscribe(), which positions the receiver at the current
write-head of the ring buffer — past all 10 bootstrap signals written
in ReplicationResyncer::new().  Every spawned resync task therefore
blocked on recv() forever, making `mc admin replicate resync start`
report "started" while no objects ever moved.

Remove the dead wait entirely.  Each resync_bucket call is already
spawned on-demand (tokio::spawn in start_bucket_resync / load_resync),
so no additional gate is needed.  The per-object concurrency limit is
already enforced by the inner mpsc worker channels (line ~877).  Also
remove the now-dead worker_tx/worker_rx fields, bootstrap loop, and
signal-send in resync_bucket_mark_status.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(site-repl): document rotate-svc-acct idempotency and partial-failure behavior

* style: cargo fmt and remove redundant clones

* fix(site-repl): preserve object-lock state when back-filling existing buckets

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-13 23:34:39 +08:00
Henry Guo 0d16a86d9a docs(scanner): add benchmark runbook (#3412)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-13 22:39:13 +08:00
安正超 6b0d7e06c4 refactor: split app context module (#3411)
* refactor: split app context module

* docs: update app context split verification
2026-06-13 22:36:28 +08:00
安正超 af291afb91 chore(dev): streamline local verification gates (#3410) 2026-06-13 21:51:33 +08:00
安正超 982812614f test: preserve background config reload assumptions (#3409) 2026-06-13 21:39:02 +08:00
Henry Guo 49a893f010 feat(table-catalog): add credential endpoint boundary (#3407)
* feat(table-catalog): add credential scope boundary

* feat(table-catalog): add credential endpoint boundary

* fix(table-catalog): remove redundant scope clone

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-13 21:26:59 +08:00
安正超 6b1e114b39 feat(obs): add metrics runtime controller status (#3408) 2026-06-13 20:48:34 +08:00
安正超 76b375c478 feat(runtime): add allocator reclaim controller status (#3406) 2026-06-13 19:03:49 +08:00
GatewayJ 42d9d5247d fix(policy): normalize IAM policy readback (#3402) 2026-06-13 18:47:08 +08:00
安正超 624de31143 feat(runtime): add memory observability controller pilot (#3405) 2026-06-13 17:42:06 +08:00
Maksim Vykhota e201a881ec fix(cli): remove autogenerated help Clap subcommand (#3289)
* Fix(CLI): remove autogenerated `help` Clap subcommand

* fix(cli): preserve rustfs help behavior

---------

Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
2026-06-13 16:58:20 +08:00
GatewayJ bda0b1f3dd feat(table-catalog): add REST exists endpoints (#3395) 2026-06-13 13:54:39 +08:00
Henry Guo 74604201bb test(table-catalog): add client conformance baseline (#3400)
* test(table-catalog): add client conformance baseline

* docs(table-catalog): remove internal roadmap labels

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-13 13:54:07 +08:00
安正超 8f8064df80 feat(runtime): add memory observability status snapshot (#3397)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-13 10:02:16 +08:00
houseme 0d68851f7a refactor(logging): standardize rustfs runtime events (#3396)
* fix(rpc): adjust log levels and reduce noise in storage RPC layer

Issue #682: pool rebalance normal flow logs were using warn! instead
of info!. Additionally, several high-frequency RPC entry points (ping,
write_stream, read_at, walk_dir) were logging at info! level, causing
unnecessary log noise in production.

Changes:
- load_rebalance_meta: warn! → info! for normal flow (3 occurrences)
- ping body decode: info! → debug! (fires on every health check)
- write_stream/read_at/walk_dir entry: info! → debug!
- metrics.rs: lowercase error messages, add structured error field
- http_service.rs: add structured error field to walk_dir failure
- Add tracing::instrument fields for start_rebalance context

* refactor(logging): standardize rustfs runtime events

* build(deps): bump workspace dependency versions

* build(deps): pin time to 0.3.47

* build(deps): update postgres client versions
2026-06-13 08:39:47 +08:00
Henry Guo b38e71fac3 feat(table-catalog): retain protected ref metadata (#3394) 2026-06-12 23:55:30 +08:00
escapecode b6973636b6 docs(sftp): document server operations (#3391)
Adds an operator guide for the SFTP server: recommended
configuration, the path model, host keys on Unix and Windows, the
environment variable reference, session cleanup behaviour, IAM
policy requirements per SFTP operation, client compatibility
notes, multipart upload sizing and cleanup, and the log lines
worth alerting on.

Corrects documentation the platform change left stale. The module
overview and the UnsupportedPlatform error text still described
Windows as unsupported. A source comment referenced a document
that does not exist in the repository and now points at the new
guide. The changelog adds the two host-key reload variables
missing from its environment list, describes the banner variable
as the SSH identification string, and corrects the upload size
cap and compliance case count.
2026-06-12 22:52:00 +08:00
Henry Guo f1a35bb9e2 fix(heal): require disk RPC before remote recovery (#3392)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-12 22:45:58 +08:00
安正超 f7bf4fd3e1 feat(targets): gate external plugin flow (#3393) 2026-06-12 22:17:54 +08:00
唐小鸭 8afb963d21 feat(ecstore): optimize the triggering conditions of the compression module (#3387)
feat. Optimize the triggering conditions of the compression module

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-12 21:18:44 +08:00
安正超 5f7c991359 feat(targets): add extension hook diagnostics contracts (#3390)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-12 21:17:34 +08:00
escapecode 7a8514bdfa feat(sftp): add macOS and Windows platform support (#3372)
The session watchdog now selects its detection method per
platform. It previously probed kernel TCP state through a
Linux-only procfs path, so on macOS every healthy idle session
was killed within a minute, and on Windows the watchdog never
spawned at all, leaving wedged sessions with no cleanup. Linux
keeps its fast-kill watchdog unchanged. Other platforms get a
silence-only backstop that kills a session only at the
documented 30-minute idle ceiling.

The host-key loader now has a Windows arm. It loads OpenSSH
format host keys from the configured directory and logs a
one-time warning to restrict NTFS ACLs on the key directory,
the same operator-managed approach FTPS, WebDAV, KMS, and IAM
already use on Windows. Startup previously aborted with
UnsupportedPlatform because the Unix mode-bit permission check
has no Windows equivalent. tokio's io-uring feature is now
enabled only in Linux builds. io-uring is a Linux kernel
interface and enabling it unconditionally broke the Windows
build.

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-12 18:08:23 +08:00
安正超 e80b72ae79 feat(targets): gate sidecar runtime policy (#3388)
* feat(targets): gate sidecar runtime policy

* fix(admin): add extension route policy specs
2026-06-12 17:42:12 +08:00
安正超 434b71e569 feat(extension): add admin views (#3386) 2026-06-12 15:54:42 +08:00
安正超 1e7c376586 feat(targets): add extension schema adapter (#3385) 2026-06-12 15:27:55 +08:00
安正超 7d6d56a547 feat(extension): add schema contracts (#3384) 2026-06-12 15:07:17 +08:00
安正超 249a0b56ea feat(targets): require plugin artifact attestations (#3383) 2026-06-12 14:13:41 +08:00
安正超 8df38ea12c ci(docker): add release image scan report (#3382) 2026-06-12 13:11:59 +08:00
安正超 212a0913be ci(release): emit sbom and provenance assets (#3381) 2026-06-12 12:56:02 +08:00
cxymds 4f954397d7 fix: support regular users in access key info (#3285)
* fix: support regular users in access key info

* refactor: extract access key identity helpers

* refactor: add typed access key identity model

* test: cover info access key identity contract

* test: cover derived access key response contracts

* fix: remove needless borrow in access key test

* fix: block fallback on explicit admin deny

* fix: restrict service account access key fallback

* fix: correct info access key identity handling
2026-06-12 12:02:43 +08:00
安正超 99e68f82a2 ci(audit): report unpinned workflow actions (#3379) 2026-06-12 11:33:23 +08:00
安正超 fc32b76c0e fix: avoid mutating signed request headers (#3378) 2026-06-12 11:13:34 +08:00
安正超 559bf9d9d7 ci(audit): wire cargo deny supply-chain gate (#3377) 2026-06-12 11:11:40 +08:00
安正超 08c586d843 security(admin): reject unknown JSON ingress fields (#3376) 2026-06-12 10:21:50 +08:00
wood 9904a4f9eb fix(obs): fix grafana layout and wrong version of tempo (#3368)
* fix(obs): fix grafana dashboard layout errors

Signed-off-by: w0od <dingboning02@163.com>

* fix(docker): align the correct tempo version

Signed-off-by: w0od <dingboning02@163.com>

---------

Signed-off-by: w0od <dingboning02@163.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-12 09:27:32 +08:00
Henry Guo 19ff378e9b fix(scanner): preserve newer usage snapshots (#3370)
* fix(scanner): preserve newer usage snapshots

* fix(table-catalog): require namespace locking backend

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-12 07:06:08 +08:00
Henry Guo 51ef87ed19 fix(heal): recover renewed disk health checks (#3366)
* fix(heal): recover renewed disk health checks

* test(heal): cover replaced remote disk rebuild

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-12 07:05:52 +08:00
安正超 c3055f9335 security(kms): require explicit dev defaults opt-in (#3369)
* security(kms): require explicit dev defaults opt-in

* test(kms): satisfy clippy dev defaults checks
2026-06-12 07:05:20 +08:00
安正超 a85cc0354c refactor(storage-api): remove namespace lock from StorageAPI (#3365)
* refactor(storage-api): remove namespace lock from StorageAPI

* test(scanner): wait for runtime budget cancellation

---------

Co-authored-by: loverustfs <hello@rustfs.com>
2026-06-11 22:42:12 +08:00
houseme 82af181dcf refactor(logging): unify governance runtime events (#3367) 2026-06-11 22:26:02 +08:00
GatewayJ b5676dcc8e fix(table-catalog): support PyIceberg REST commits (#3342)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-11 22:22:13 +08:00
安正超 7146c893cb refactor(storage-api): remove legacy bucket DTO re-export (#3364) 2026-06-11 21:24:56 +08:00
houseme 0a987d870b refactor(logging): reduce runtime noise (#3363) 2026-06-11 19:49:01 +08:00
安正超 a0b6636b61 refactor(config): remove legacy accessor re-export (#3362) 2026-06-11 19:32:57 +08:00
Henry Guo 2e3c777307 fix(heal): skip transient usage cache heal errors (#3359) 2026-06-11 19:16:32 +08:00
安正超 b4524033e3 refactor(config): move global config accessors (#3360) 2026-06-11 19:16:02 +08:00
安正超 ed3851782c refactor(config): remove legacy model re-export (#3357) 2026-06-11 18:09:01 +08:00
houseme f6137f9f77 refactor(auth): tighten startup log redaction (#3358) 2026-06-11 18:08:41 +08:00
安正超 69549634ea refactor(config): migrate scanner config consumer (#3356) 2026-06-11 17:30:26 +08:00
houseme 704c95a22d refactor(server): consolidate request transport logs (#3354) 2026-06-11 17:07:02 +08:00
安正超 ca58d7f0ec refactor(config): migrate server config consumers (#3353) 2026-06-11 17:04:50 +08:00
安正超 2205991180 refactor(config): extract server config model (#3351) 2026-06-11 16:11:17 +08:00
houseme 7d38b0cf90 refactor(obs): unify local and otlp log output (#3352)
wip(obs): start lg-002 output unification
2026-06-11 16:10:54 +08:00
houseme 8d337c4e87 feat(obs): add logging redaction guard rails (#3349)
* feat(obs): add logging redaction guard rails

* chore(docs): keep logging plans out of lg-001 pr

* test(obs): guard against unmasked access key logs

* test(obs): enforce masked access key log patterns
2026-06-11 15:24:32 +08:00
安正超 dd50359161 refactor(storage): narrow table catalog bounds (#3350) 2026-06-11 14:32:28 +08:00
安正超 2ead90d31b fix(security): add authorization check to ListRemoteTargetHandler (GHSA-796f-j7xp-hwf4) (#3346)
fix(security): add authorization check to ListRemoteTargetHandler

ListRemoteTargetHandler only verified that request credentials existed
but did not check whether the caller has replication or administrator
permissions. This allowed any authenticated user to list remote
replication target configuration, potentially leaking remote target
credentials (accessKey, secretKey, sessionToken).

Add validate_replication_admin_request() call with
GetBucketTargetAction, consistent with the other replication admin
handlers that already perform this check.

Fixes: GHSA-796f-j7xp-hwf4

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-11 05:50:14 +00:00
安正超 d60deba9b6 fix(security): add IAM authorization to FTP read/metadata/cwd handlers (GHSA-3g29-xff2-92vp) (#3347)
fix(security): add IAM authorization to FTP read/metadata/cwd handlers

The FTP frontend's get() (RETR), metadata() (SIZE/MDTM), and cwd()
(CWD) handlers dispatched directly to the storage backend without
calling authorize_operation(). This allowed any authenticated FTP user,
including those with explicit Deny policies, to read arbitrary objects
and probe bucket existence regardless of IAM policy.

Add authorize_operation() calls matching the pattern used by the
write-path handlers (put, del, list, rmd) and the WebDAV driver:
- get(): S3Action::GetObject
- metadata() for files: S3Action::HeadObject
- metadata() for directories: S3Action::HeadBucket
- cwd(): S3Action::HeadBucket

Fixes: GHSA-3g29-xff2-92vp

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-11 05:48:53 +00:00
安正超 d4ee44f49f refactor(scanner): narrow cache storage bounds (#3348) 2026-06-11 13:52:35 +08:00
安正超 2f7cc7cb9e refactor(storage): narrow resync metadata bounds (#3345) 2026-06-11 04:41:15 +00:00
houseme ee96e194a3 build(deps): bump s3s for header parsing fix (#3344) 2026-06-11 04:28:36 +00:00
houseme a49c6b4c2e fix(logging): clarify recovery and metacache warn messages (#3341)
* fix(logging): clarify recovery and metacache warn messages

Clarify cached gRPC connection eviction messages so they describe automatic reconnection instead of implying a persistent cluster fault.

Retitle metacache resolution logs to remove the misleading decommission_pool prefix and demote routine reconciliation traces to debug while preserving actionable warning paths.

* fix(logging): avoid overpromising reconnect success
2026-06-11 04:19:22 +00:00
Henry Guo 7191a3abae docs(scanner): document runtime scanner controls (#3339)
* docs(scanner): document runtime scanner controls

* docs(scanner): split English and Chinese README

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com>
2026-06-11 12:04:34 +08:00
安正超 686068d8bd refactor(storage): narrow metadata bounds (#3343) 2026-06-11 12:04:02 +08:00
安正超 6b86e3875c refactor(storage): remove old admin surfaces (#3340) 2026-06-11 11:28:09 +08:00
安正超 87968275a7 refactor(storage): route maintenance inventory reads (#3337) 2026-06-11 10:32:48 +08:00
安正超 f325b9f714 refactor(ecstore): route admin read internals (#3336) 2026-06-11 08:43:42 +08:00
安正超 94c53af264 refactor(storage): use admin API for observability reads (#3335) 2026-06-11 08:13:15 +08:00
安正超 8ae0cad667 refactor(admin): use storage admin reads (#3334) 2026-06-11 07:48:38 +08:00
安正超 b48d7b1fa5 refactor(admin): use storage admin info (#3333) 2026-06-11 07:12:35 +08:00
安正超 8a90bda16a refactor(admin): use storage admin drive counts (#3332) 2026-06-11 06:36:18 +08:00
安正超 4b1714438e feat(storage-api): bind ecstore admin contract (#3331) 2026-06-11 05:49:14 +08:00
Henry Guo 474fab5097 fix(table-catalog): validate committed metadata identity (#3327)
* fix(table-catalog): validate committed metadata identity

* fix(table-catalog): preserve legacy metadata identity commits

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-10 16:49:06 +00:00
Henry Guo 91f80a5585 feat(scanner): expose lifecycle transition status (#3326)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-10 16:45:02 +00:00
安正超 d9ddd1bedc feat(storage-api): add disk inventory contract (#3330) 2026-06-10 16:33:31 +00:00
houseme b1339a98ed fix(scanner): delay deep verify for fresh objects (#3329)
Gate scanner-triggered deep heal behind a short new-object cooldown and retry transient bitrot shard size mismatches during local verification.

Add targeted tests for cooldown mode selection and size-mismatch classification, and log when deep scans are downgraded or size-mismatch retries occur.
2026-06-10 15:50:44 +00:00
houseme 3fed21c68a fix(startup): add disk init diagnostics (#3324)
* ci(build): remove macOS x86_64 target

* ci(build): reduce workflow timeout

* fix(startup): add disk init diagnostics
2026-06-10 14:24:12 +00:00
Henry Guo 64c0ede026 feat(table-catalog): add product API surface (#3320)
* feat(table-catalog): add product API surface

* fix(table-catalog): harden product API commits

* fix(table-catalog): close product API review gaps

* fix(table-catalog): validate rollback metadata before commit

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-10 07:58:22 +00:00
Henry Guo 66fd55a8e0 feat(scanner): expose pacing pressure status (#3319)
* feat(scanner): expose pacing pressure status

* fix(scanner): preserve merged pause pressure

* fix(scanner): default missing primary pressure

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-10 07:33:41 +00:00
安正超 bb5d9565a6 feat(storage-api): add bucket DTO contract (#3314)
* feat(storage-api): add bucket DTO contract

* ci(build): increase workflow timeout to 90 minutes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-10 07:16:14 +00:00
houseme b9c924a6ed build(ci): update macOS runner and bump regex/uuid (#3318)
* ci(build): use macos-26-intel for x86_64 job

* build(deps): bump regex and uuid

* ci(build): disable cross for macos x86_64 job

---------

Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com>
2026-06-10 04:42:10 +00:00
houseme 68400933b5 chore(release): prepare 1.0.0-beta.8 (#3317)
* chore(release): prepare 1.0.0-beta.8

* chore(release): align release assets for 1.0.0-beta.8
2026-06-10 04:34:39 +00:00
安正超 0dc99a1abb docs: inventory KMS development defaults (#3304) 2026-06-10 12:46:08 +08:00
Henry Guo d7a7d1fc1e feat(table-catalog): add REST catalog alias (#3316)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-10 03:55:58 +00:00
Henry Guo 62e9c5b94f feat(scanner): add adaptive pacing controls (#3315)
* feat(scanner): add adaptive pacing controls

* fix(scanner): bound pacing delay status precision

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-10 03:46:02 +00:00
安正超 f80162b5c9 docs: decide config model boundary (#3307) 2026-06-10 10:02:53 +08:00
Henry Guo 2eafd9c602 feat(table-catalog): add credential response boundary (#3305)
* feat(table-catalog): add credential response boundary

* fix(table-catalog): enforce scoped catalog resources

* fix(table-catalog): reduce admin auth scope arguments

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-10 01:15:51 +00:00
安正超 a73c90c811 security: redact IAM and target debug secrets (#3306) 2026-06-10 09:03:50 +08:00
安正超 3795d44b86 feat(storage-api): add error code contract (#3313) 2026-06-10 09:03:29 +08:00
安正超 5fef105484 docs: define background controller contract (#3312) 2026-06-10 07:30:21 +08:00
安正超 bc95b9fb9e test: guard table catalog ingress unknown fields (#3308) 2026-06-09 20:06:36 +00:00
安正超 45391ac776 feat(storage-api): add contract crate (#3310) 2026-06-09 20:06:15 +00:00
安正超 f9a5e6d7e6 docs: inventory background services (#3311) 2026-06-10 04:05:33 +08:00
安正超 03eb10b07f feat: add KMS config redaction safeguards (#3303) 2026-06-09 14:08:58 +00:00
Henry Guo 7ec16e197c feat(table-catalog): add metadata maintenance control plane (#3302)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-09 22:08:28 +08:00
Julien Pervillé 992de65c58 feat(helm): add priorityClassName attribute (#3301)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-09 22:07:41 +08:00
Maksim Vykhota c4d0d395af Refactor(CLI): avoid unnecessary unreachable! macro in CLI parsing (#3288)
* Refactor(CLI): avoid unnecessary `unreachable!` macro in CLI parsing

* Fix: clippy lint

---------

Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-09 22:01:51 +08:00
Henry Guo e79c793803 feat(heal): add scanner-aware bitrot controls (#3297)
* feat(heal): add scanner-aware bitrot controls

* fix(config): register heal defaults

* test(admin): avoid heal config queue conflict

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-09 13:43:18 +00:00
Henry Guo 0cdcd1eb7b feat(table-catalog): internalize catalog backing paths (#3295)
* feat(table-catalog): internalize catalog backing paths

* fix(table-catalog): clean internal catalog on bucket delete

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-09 12:01:25 +00:00
cxymds 55590e38fb fix(config): accept Kafka SASL keys in legacy admin (#3300) 2026-06-09 12:01:15 +00:00
安正超 f5bb034ec8 feat(kms): migrate KMS handlers to dedicated actions (#3298)
feat: migrate KMS handlers to dedicated actions
2026-06-09 12:01:02 +00:00
安正超 4fec606dc4 feat: add KMS action taxonomy (#3294) 2026-06-09 05:06:15 +00:00
Henry Guo ad9bf41fc8 feat(heal): expose scanner heal admission outcomes (#3292)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-09 03:39:57 +00:00
Henry Guo 51c26278a4 feat(table-catalog): support standard REST create commit (#3287)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-09 02:18:33 +00:00
安正超 b9a3a2245a feat: add admin route policy inventory (#3286)
* feat: add admin route policy inventory

* fix: align table route policy actions

* fix: align load table route policy action
2026-06-09 02:17:50 +00:00
houseme aabda41ae5 build: upgrade Rust baseline to 1.96.0 (#3291)
* build: upgrade Rust baseline to 1.96.0

* ci: pin Rust workflows to 1.96.0

* remove

* ci: pin setup action to Rust 1.96.0

* build: pin toolchain to Rust 1.96.0
2026-06-08 20:32:37 +00:00
houseme 9354b939ca refactor(zip): simplify archive extraction path (#3290)
* refactor(zip): simplify archive extraction path

* build(deps): align datafusion and http updates

* fix(test): restore pre-commit compatibility
2026-06-08 20:03:02 +00:00
Henry Guo 6413df4b7f feat(replication): expose scanner repair outcomes (#3278)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-08 14:24:27 +00:00
cxymds 452c003341 fix(scanner): ignore missing rebalance metadata (#3282)
* fix(ecstore): harden rebalance data movement

* fix(ecstore): preserve failed rebalance status

* fix(ecstore): avoid rebalance walkdir total timeout

* fix(ecstore): retry rebalance listing timeouts

* fix(ecstore): restrict data movement resume target

* refactor(ecstore): simplify multipart movement target

* fix(ecstore): restore resume target checks

* perf(ecstore): speed up rebalance bucket merges

* fix: keep rebalance listing alive on transient failures

* fix(scanner): ignore missing rebalance metadata

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-08 14:24:16 +00:00
Henry Guo 8c3e52efb8 feat(table-catalog): refine table catalog permissions (#3283)
* feat(table-catalog): refine table catalog permissions

* fix(policy): scope table admin resources

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-08 14:23:52 +00:00
唐小鸭 f7724d223b feat(rio): rio_v2 is compatible with minio for storing data. (#3115)
* Set up a compatibility layer for replacing old Rio components with new ones.

* fix(rio). compress range

* feat(rio). Add the experimental feature rio_v2 to support minio data at the binary level.

* feat(rio_v2): add sse-c test

* test compression component

* simple fix

* fix minlz encode

* fix metadata

* fix kms key cache error

* Update launch.json

* ci: set nix crate download user agent

* fix: gate obs pyroscope backend

* ignore minio test

* fix encrypt check

* fix

* fix

* fix

* Update object_usecase.rs

* Update ci.yml

* fix

* ci add rio-v2 test

* fix

* ci fix

* fix

* Reconstructed into a more reasonable compatibility mode

* fix

* fix

---------

Signed-off-by: houseme <housemecn@gmail.com>
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-06-08 11:59:14 +00:00
cxymds 9504dff595 fix(ecstore): harden rebalance data movement (#3234)
* fix(ecstore): harden rebalance data movement

* fix(ecstore): preserve failed rebalance status

* fix(ecstore): avoid rebalance walkdir total timeout

* fix(ecstore): retry rebalance listing timeouts

* fix(ecstore): restrict data movement resume target

* refactor(ecstore): simplify multipart movement target

* fix(ecstore): restore resume target checks

* perf(ecstore): speed up rebalance bucket merges

* fix: keep rebalance listing alive on transient failures

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-08 10:40:26 +00:00
Michael 2fc1cf90e0 feat(helm): add option to disable log PVCs and mounts (#3189)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-08 10:35:12 +00:00
houseme ed73952cb6 perf(ecstore): improve erasure write diagnostics and single-block performance (#3280)
* docs(object-capacity): add localized crate docs

* fix(ecstore): improve quorum and transport diagnostics

* perf(ecstore): add safe single-block write fast path

* refactor(ecstore): collapse layered small write paths

* chore(docs): keep issue 662 design note tracked

* fix(docs): restore issue 662 design note

* chore(docs): keep issue 662 design local only

* feat(obs): add internode reliability metrics and dashboard

* feat(obs): extend internode diagnostics and service logging

* fix(docs): use AGENTS guide filename

* perf(ecstore): reuse owned buffer in small encode

* fix(ecstore): tighten small write diagnostics

---------

Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-08 09:45:56 +00:00
cxymds ddd35badad fix(ecstore): avoid offline disks on admin timeout (#3263)
* fix(ecstore): avoid offline disks on admin timeout

* fix(ecstore): handle poisoned admin cache locks

* fix(ecstore): recover poisoned admin cache on success

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-06-08 08:35:36 +00:00
Henry Guo f40dd2f93c fix(scanner): publish partial usage for compacted scans (#3277)
* fix(scanner): publish partial usage for compacted scans

* fix(scanner): publish first partial usage immediately

* fix(scanner): skip startup delay for cold usage cache

* fix(scanner): tighten cold usage publish gate

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-08 06:18:35 +00:00
Henry Guo 4751a9f4b9 feat(table-catalog): add recovery diagnostics (#3275)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-08 03:59:13 +00:00
安正超 3d0e6ce0da feat: add security governance policy contracts (#3271)
* feat: add security governance policy contracts

* fix: require signatures for release assets

* docs: update policy verification counts

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-08 02:42:03 +00:00
安正超 78df276d25 fix: return 503 on lock contention instead of 500 (#3274)
* fix(ecstore): skip hidden metadata in walk limit

* fix(ecstore): match walk limit to visible versions

* fix(ecstore): avoid decoding all versions for limit

* fix: return 503 on lock contention instead of 500

When concurrent PUTs to the same key contend for the namespace write
lock, lock timeout and conflict errors were wrapped as
StorageError::other(...) → StorageError::Io(...), which fell through
to S3ErrorCode::InternalError (500) in the error mapping.

Only QuorumNotReached was correctly mapped to ServiceUnavailable (503);
all other lock errors (Timeout, AlreadyLocked, etc.) became 500.

Fix: map_namespace_lock_error now returns StorageError::Lock(err) for
non-quorum lock errors, and StorageError::Lock is mapped to
S3ErrorCode::ServiceUnavailable in the HTTP error conversion.

Affected paths:
- crates/ecstore/src/set_disk/lock.rs
- crates/ecstore/src/store/object.rs
- crates/ecstore/src/store/bucket.rs (make_bucket, delete_bucket)
- rustfs/src/app/object_usecase.rs (copy_object)

Regression test: test_concurrent_put_same_key_never_returns_500

* test: fail on unexpected lock contention errors

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-08 02:20:21 +00:00
houseme c479f3d0cb fix(ecstore): gate rustix fs diagnostics on windows (#3267)
* fix(ecstore): gate rustix fs diagnostics on windows

* fix(ecstore): improve windows disk validation diagnostics

* build(deps): bump workspace dependency versions
2026-06-08 00:05:55 +00:00
Henry Guo c452dd8ad7 feat(table-catalog): add metadata maintenance cleanup (#3266)
* feat(table-catalog): add metadata maintenance dry run

* feat(table-catalog): add metadata-only maintenance delete

* fix(table-catalog): protect recent metadata cleanup candidates

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-08 00:05:41 +00:00
安正超 7a9bf707ee feat: add security governance contract types (#3270) 2026-06-07 23:14:11 +00:00
安正超 c03c0ebc36 test: add admin route matrix guard (#3268) 2026-06-08 05:30:18 +08:00
安正超 dee550a831 ci: add architecture migration rule checks (#3264)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-07 16:23:38 +00:00
Henry Guo f49df41db9 fix(lifecycle): harden scanner ILM expiry accounting (#3257)
* fix(lifecycle): harden scanner ILM expiry accounting

* fix(scanner): gate ILM action accounting on enqueue

* fix(metrics): avoid scanner source work argument list

* fix(scanner): gate local ILM accounting on enqueue

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-07 16:18:34 +00:00
houseme cebea9a238 fix(ecstore): add disk validation diagnostics (#3265)
Log local endpoint disk-topology details during physical disk independence checks.
Include canonical paths, st_dev major:minor values, and resolved device ids in validation output to make Docker and bind-mount startup failures easier to diagnose.

Also assert the new diagnostics are present in the shared-disk regression test.
2026-06-07 15:32:38 +00:00
唐小鸭 74296761fa fix(replication): repair site replication setup (#3252)
* fix: repair site replication setup

* Optimized site status collection

---------

Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-07 13:19:50 +00:00
安正超 6f4d0b54a1 fix(ci): install ripgrep for script checks (#3260) 2026-06-07 22:03:25 +08:00
安正超 4254633c9e docs: update security advisory lessons (#3262) 2026-06-07 22:02:56 +08:00
安正超 e5b2bcc088 docs: complete config helper inventory (#3261) 2026-06-07 22:02:21 +08:00
Henry Guo dd0ef1f766 feat(table-catalog): tighten REST load/register compatibility (#3245)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-07 21:54:15 +08:00
安正超 3c71d5ef1c docs: inventory ecstore config consumers (#3259) 2026-06-07 21:44:55 +08:00
安正超 241e45d3b0 docs: add admin route action snapshot (#3258) 2026-06-07 21:20:16 +08:00
安正超 0f9584c8d9 docs: add startup timeline baseline (#3256) 2026-06-07 20:18:30 +08:00
安正超 ae9d25879d ci: stabilize architecture layer guard (#3255) 2026-06-07 19:26:35 +08:00
Marcelo Bartsch f00898d070 fix(tier): recover (#3182)
* fix(tier): stop sending nil/garbage versionId to warm backend S3

Three bugs caused NoSuchVersion errors when reading tiered objects:

1. warm_backend_s3sdk: GET and DELETE ignored rv/range opts entirely —
   fixed to forward version_id and byte-range to the SDK request.

2. version.rs (MetaObject + MetaDeleteMarker): transition_version_id was
   parsed with unwrap_or_default(), turning invalid/wrong-length bytes
   into Uuid::nil(). The nil UUID was then serialized and sent as
   ?versionId=00000000-... to the tier backend -> NoSuchVersion.
   Fixed: .and_then(.ok()).filter(!is_nil()) so only valid non-nil UUIDs
   are forwarded as versionId.

3. bucket_lifecycle_ops: add debug/error logs in
   get_transitioned_object_reader to record tier, tier_object, and
   tier_version_id before and on failure of the tier GET.

Also adds tier transition fields to dump_fileinfo example for offline
xl.meta inspection, and fixes Docker build (cargo path + entrypoint).
Adds CLAUDE.md with tier architecture and debugging notes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* more fixes for versionId

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Marcelo Bartsch <marcelo@bartsch.cl>

* remove branch

* Add tests and fix cargo path, add load to build-docker

* update documentation (CLAUDE.md)

* more fixes for recover

* More fixes to ILM recover

* final fix

* chore: add missing-shard first-scene diagnostics (#3213)

chore(ecstore): add missing-shard first-scene diagnostics

Log rename_data quorum context behind RUSTFS_ISSUE3031_DIAG_ENABLE so partial-disk success can be correlated with later missing shard reads.

Also log put_object commit success and tmp cleanup boundaries to capture when successful quorum writes are followed by tmp_dir cleanup.

* fix test anmd fmt

* fix cargo path
fix test

* fix(tier): format copy_object self-copy guard

---------

Signed-off-by: Marcelo Bartsch <marcelo@bartsch.cl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-06-07 09:38:28 +00:00
安正超 069d1e5a75 test: bypass proxy for embedded readiness probe (#3254) 2026-06-07 09:04:29 +00:00
Henry Guo 4ae070c8cc feat(scanner): expose scan partial source status (#3247)
* feat(scanner): expose scan partial source status

* fix(scanner): expose partial source in aggregated metrics

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-07 06:40:04 +00:00
安正超 f8aa4fa221 docs: add architecture migration guardrails (#3253) 2026-06-07 14:43:55 +08:00
cxymds 61f0dfbc40 fix(ecstore): invalidate wiped disk id cache (#3251) 2026-06-07 04:25:38 +00:00
cxymds 04b5e8f988 fix(replication): normalize local site endpoint port (#3249)
* fix(replication): normalize local site endpoint port

* fix(replication): respect runtime console endpoint

* fix(replication): allow valid peer endpoint ports

---------

Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-07 04:19:04 +00:00
cxymds 316540aaf8 fix(docker): align TLS compose healthchecks (#3246)
* fix(docker): align TLS compose healthchecks

* fix(docker): keep simple compose TLS opt-in
2026-06-07 01:27:03 +00:00
cxymds dd5e2c8ae9 docs(docker): clarify bind mount permission setup (#3248)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-07 09:33:24 +08:00
houseme e73b1c11c0 chore(deps): update flake.lock (#3250)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/e9a7635' (2026-05-29)
  → 'github:NixOS/nixpkgs/cbb5cf3' (2026-06-06)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/85570ef' (2026-05-30)
  → 'github:oxalica/rust-overlay/27b7e78' (2026-06-06)
2026-06-07 01:07:23 +00:00
GatewayJ 4a28e3d671 fix: clean old data dirs on object overwrite (#3244)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-06-07 00:18:56 +00:00
GatewayJ 8c742ede14 fix(admin): format policy JSON and improve error messages in service … (#3242)
* fix(admin): format policy JSON and improve error messages in service account API

- Use serde_json::to_string_pretty for policy serialization to match MinIO behavior
- Replace technical error messages with user-friendly ones for policy validation
- Fixes rustfs/rustfs#3233 (policy JSON not formatted)
- Fixes rustfs/rustfs#3232 (error messages not shown to users)

* Delete scripts/tempfile.zip

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-06-06 16:39:59 +00:00
Henry Guo 7d84e89c36 feat(table-catalog): wire REST commit handler (#3239)
* feat(table-catalog): wire REST commit handler

* fix(table-catalog): hide MVP commit endpoint from config

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-06 13:38:12 +00:00
安正超 e2fe99d8bc refactor(ecstore): add server configuration accessors (#3238)
* refactor(ecstore): add server configuration accessors

Phase 4 of global singleton consolidation. Add ECStore accessor
methods for server-level configuration globals.

New methods:
- port() — delegates to global_rustfs_port()
- host() — delegates to GLOBAL_RUSTFS_HOST
- addr() — delegates to GLOBAL_RUSTFS_ADDR
- region() — delegates to get_global_region()
- endpoints() — delegates to get_global_endpoints()
- server_config() — delegates to get_global_server_config()
- storage_class() — delegates to get_global_storage_class()
- tier_config_mgr() — delegates to get_global_tier_config_mgr()
- notification_system() — delegates to get_global_notification_sys()
- bucket_metadata_sys() — delegates to get_global_bucket_metadata_sys()

1160 tests pass, clippy clean, formatting clean.

* fix(ecstore): avoid duplicate server accessors
2026-06-06 21:38:45 +08:00
Henry Guo 83a4e5712e feat(scanner): track scan cycle source work (#3240)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-06 12:53:48 +00:00
Henry Guo fade13d950 feat(table-catalog): wire REST catalog MVP handlers (#3227)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-06 10:47:49 +00:00
Henry Guo b36a730e48 feat(scanner): expose checkpoint and source work status (#3230)
* feat(scanner): expose checkpoint and source work status

* fix(scanner): count ignored checkpoints once per scan

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-06 10:47:39 +00:00
houseme 6db4c3538d fix(allocator): restore validated jemalloc target gating (#3236)
* fix(allocator): restore validated jemalloc target gating

Restrict the global allocator and jemalloc profiling paths to linux-gnu-x86_64 so Linux ARM64 builds fall back to mimalloc again.

Update the runtime profiling, allocator reclaim, and admin profiling handlers to use the same target gating and avoid exposing jemalloc-only code on unsupported targets.

Verification:
- cargo fmt --all
- cargo fmt --all --check
- cargo check -p rustfs
- make pre-commit

* fix(profiling): restore non-jemalloc platform behavior

Restore CPU profiling support on macOS while keeping jemalloc-backed memory profiling restricted to validated linux-gnu-x86_64 targets.

Also restore Unix log directory permission hardening so the allocator regression fix does not roll back unrelated observability behavior on other supported platforms.

Verification in progress:
- cargo fmt --all
- cargo check -p rustfs
- make pre-commit (running)

* fix(profiling): qualify periodic memory log macros

Use explicit tracing macro paths in periodic jemalloc memory profiling logging so builds do not fail when the local target configuration excludes those branches from the current import set.

Verification:
- cargo fmt --all
- cargo check -p rustfs

* fix(profiling): gate unsupported memory helper

Restrict the unsupported memory profiling helper to non-linux-gnu-x86_64 targets so Linux builds do not emit dead_code warnings for an unreachable fallback.

Verification:
- cargo fmt --all
- cargo check -p rustfs
2026-06-06 08:22:29 +00:00
安正超 60f14cb0f1 fix(bucket-encryption): populate default KMS key for SSE-KMS without key ID (#3225)
* fix(bucket-encryption): populate default KMS key for SSE-KMS without key ID

When PutBucketEncryption receives SSE-KMS configuration without a specific
KMS key ID, query the KMS service for the default key and populate
KMSMasterKeyID before storing. This ensures GetBucketEncryption responses
include the key ID, which S3 clients like mc rely on to distinguish SSE-KMS
from SSE-S3 in their display logic.

Fixes #3039

* test(kms): cover empty bucket encryption key ID

* fix(bucket-encryption): require default KMS key
2026-06-06 02:57:48 +00:00
安正超 06acf49672 refactor(ecstore): add accessor methods for service globals (#3226)
* refactor(ecstore): add accessor methods for service globals

Phase 3 of global singleton consolidation. Add ECStore accessor
methods that delegate to process-global service singletons,
providing a unified API through ECStore.

New methods:
- notification_system() — delegates to notification_sys global
- bucket_metadata_sys() — delegates to bucket metadata global
- endpoints() — delegates to endpoints global
- region() — delegates to region global
- tier_config_mgr() — delegates to tier config global
- server_config() — delegates to server config global
- storage_class() — delegates to storage class global

1161 tests pass, clippy clean, formatting clean.

* fix: use imported functions instead of fully-qualified paths

Address review comments: use imported functions instead of
crate:: paths to avoid unused import warnings.

1160 tests pass, clippy clean.

* fix(ecstore): remove unused service imports
2026-06-05 15:29:38 +00:00
Henry Guo 6d06b574f2 feat(table-catalog): add REST catalog route surface (#3211)
* feat(table-catalog): add REST catalog route surface

* fix(table-catalog): avoid test-only router import

* fix(table-catalog): align unsupported REST responses

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-05 13:52:13 +00:00
安正超 4d50c72e4c fix(ecstore): skip hidden metadata in walk limit (#3224)
* fix(ecstore): skip hidden metadata in walk limit

* fix(ecstore): match walk limit to visible versions

* fix(ecstore): avoid decoding all versions for limit
2026-06-05 13:50:02 +00:00
Henry Guo 0d445afda9 feat(scanner): add versioned scan checkpoints (#3220)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-05 13:49:41 +00:00
GatewayJ f1b8f8312d fix(server): normalize empty request content length (#3215)
* fix(server): normalize empty S3 request content length

* fix(server): normalize empty console content length

* fix(server): keep console constants test scoped

* test(server): align empty body route expectations
2026-06-05 02:03:07 +00:00
安正超 8185376b90 refactor(ecstore): migrate config globals into ECStore struct fields (#3219)
* refactor(ecstore): migrate config globals into ECStore struct fields

Phase 2 of global singleton consolidation. Add server_config and
storage_class fields to ECStore, sharing the same underlying data
as the process-global LazyLock statics.

New ECStore fields:
- server_config: RwLock<Option<Config>>
- storage_class: RwLock<storageclass::Config>

New async accessor methods:
- get_server_config() / set_server_config()
- get_storage_class() / set_storage_class()

Fields initialized with defaults in ECStore::new(), synced from
globals after init() completes. Global functions preserved for
backward compatibility.

1160 tests pass, clippy clean, full workspace compiles.

* fix: address PR #3219 review comments - delegate to globals

Accessors now delegate to process-global statics to avoid state drift:
- get_server_config() delegates to get_global_server_config()
- set_server_config() updates both global and local field
- get_storage_class() delegates to get_global_storage_class()
- set_storage_class() updates both global and local field
- Removed stale sync code from init()
- Changed fields to std::sync::RwLock for sync access

1160 tests pass.

* fix: correct import order for cargo fmt

* fix: simplify config accessors - delegate to globals without local state

Remove local server_config and storage_class fields from ECStore.
Accessors now purely delegate to process-global statics, eliminating
the state drift risk identified in review.

1160 tests pass, formatting clean.
2026-06-05 01:06:08 +00:00
安正超 1dd3839a9f fix(signer): address post-merge review comments (#3216)
* fix: address PR #3150 review comments

- Restore get_host_addr as best-effort wrapper (String return type)
- Replace bare expect("err") with descriptive messages in unsigned trailer
- Simplify aws-chunked header construction
- 20 signer + 96 io-core tests pass

* fix(signer): preserve host fallback

* fix(signer): avoid signer fallback panics

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-04 22:45:50 +00:00
安正超 d6120f5788 refactor(ecstore): migrate mutable globals into ECStore struct fields (#3214)
* refactor(ecstore): migrate mutable globals into ECStore struct fields

Phase 1 of global singleton consolidation. Move mutable globals from
lazy_static into ECStore struct fields as the first step toward
dependency injection and multi-instance support.

New ECStore fields:
- is_erasure, is_dist_erasure, is_erasure_sd (erasure type flags)
- local_disk_map, local_disk_id_map, local_disk_set_drives
- root_disk_threshold
- tier_config_mgr, event_notifier, bucket_monitor

New accessor methods:
- is_erasure(), is_dist_erasure(), is_erasure_sd()
- update_erasure_type()
- tier_config_mgr(), event_notifier(), bucket_monitor()

Global functions in global.rs preserved for backward compatibility.
1151 ecstore tests pass.

* fix: address PR #3214 review comments

- Sync ECStore fields from globals after init()
- Enforce DistErasure => is_erasure invariant in update_erasure_type()
- Change bucket_monitor from Option to OnceLock for deferred initialization
- Restrict TypeLocalDiskSetDrives to pub(crate)
- 1151 tests pass

* fix: address PR #3150 review comments

- Restore get_host_addr as best-effort wrapper (String return type)
- Replace bare expect("err") with descriptive messages in unsigned trailer
- Simplify aws-chunked header construction
- 20 signer + 96 io-core tests pass

* fix(ecstore): format store imports

* fix(ecstore): keep migrated accessors in sync

* fix(signer): preserve host fallback and unsigned trailer errors

* fix(ecstore): defer migrated global accessors

* fix(signer): box unsigned trailer signing errors

* fix(ecstore): avoid lock awaits during sync

* fix(ecstore): narrow phase one globals

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-04 17:27:21 +00:00
houseme 0a74629c48 chore: add delete-objects lock batch diagnostics (#3218)
* chore(ecstore): add missing-shard first-scene diagnostics

Log rename_data quorum context behind RUSTFS_ISSUE3031_DIAG_ENABLE so partial-disk success can be correlated with later missing shard reads.

Also log put_object commit success and tmp cleanup boundaries to capture when successful quorum writes are followed by tmp_dir cleanup.

* chore(ecstore): add delete-objects lock batch diagnostics

Log DeleteObjects batch-lock request size, distributed lock quorum summary, and failed object keys behind RUSTFS_ISSUE3031_DIAG_ENABLE.

Include a Chinese operator checklist under docs/ that explains which logs to capture and how to interpret DeleteObjects lock_batch timeout incidents.

* chore: remove lock batch checklist from repo

Keep docs/issue-658-deleteobjects-lock-batch-checklist-zh.md only in the local checkout and drop it from the PR history.
2026-06-04 17:00:58 +00:00
安正超 3bd89944c2 perf(erasure): remove UUID from clone + increase encode inflight budget (#3212)
* perf(erasure): remove UUID from clone + increase encode inflight budget

Two targeted optimizations for the erasure encoding hot path:

1. Erasure::clone() no longer generates Uuid::new_v4() per clone.
   The _id field is unused in the hot path; reusing the original ID
   eliminates a CSPRNG call per block encode (100 calls for a 100MB
   object with 1MB blocks).

2. Default RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES raised from 8MB
   to 32MB. This increases the encode pipeline depth from ~5 to ~20
   blocks, allowing more read-ahead between the encoder and disk
   writer stages. The per-request memory bound is still controlled
   by the 8-block hard cap and the env var override.

3. Added encode_data_owned() utility method for zero-copy encoding
   when the caller already owns a heap buffer (Vec<u8> → BytesMut
   via Bytes::try_into_mut). Not used in the hot path yet but
   available for future callers.

All 1157 ecstore tests pass. Criterion micro-benchmarks show no
regression (< 2% variance). Single-machine warp E2E tests were
inconclusive due to high variance; a dedicated multi-disk test
environment is needed for reliable E2E comparison.

Ref: https://github.com/rustfs/backlog/issues/659

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: update Cargo.lock

* fix(erasure): align encode inflight cap and tests

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 13:49:36 +00:00
houseme fde519910d chore: add missing-shard first-scene diagnostics (#3213)
chore(ecstore): add missing-shard first-scene diagnostics

Log rename_data quorum context behind RUSTFS_ISSUE3031_DIAG_ENABLE so partial-disk success can be correlated with later missing shard reads.

Also log put_object commit success and tmp cleanup boundaries to capture when successful quorum writes are followed by tmp_dir cleanup.
2026-06-04 10:35:33 +00:00
Henry Guo 542720a1f7 feat(scanner): add partial scan resume hints (#3207)
* feat(scanner): add partial scan resume hints

* test(scanner): cover clearing scan resume hints

* fix(scanner): apply resume hint to combined child order

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-04 09:21:30 +00:00
Henry Guo a7be7c558d feat(table-catalog): add object-backed catalog store (#3206)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-04 07:50:13 +00:00
Henry Guo 6850457707 feat(scanner): add runtime scanner controls and status (#3203)
* feat(scanner): add runtime scanner controls and status

* fix(scanner): validate persisted scanner config

* docs(scanner): clarify start delay behavior

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-04 05:16:26 +00:00
houseme f708c22b0e fix: make ecstore trash cleanup idempotent (#3205)
* fix(http): reduce header timeout log noise

Log peer_addr for HTTP connection errors so idle or probe traffic can be traced back to the source.

Downgrade HeaderTimeout connection logs from warn to info because these timeouts are often caused by incomplete probe traffic rather than a service-side fault.

* fix(ecstore): suppress spurious warn logs for missing trash cleanup sources

Extract `reliable_rename_inner` with a `warn_on_failure` flag so that
`rename_all_ignore_missing_source` can skip the warn log when the source
is absent. This completes the idempotent cleanup fix — the previous
implementation still emitted a `reliable_rename failed` warning before
the NotFound was caught and silenced.

Also restore the `recursive` branching in `move_to_trash` so that
non-recursive deletions use `fs::rename` directly (preserving original
semantics), while recursive deletions use the idempotent helper.
2026-06-04 04:21:31 +00:00
Alexander Kharkevich 528c3278b7 fix(iam): allow colons and dots in STS claim policy names (#3164)
`is_safe_claim_policy_name` rejected any character other than
`[a-zA-Z0-9_-]`, silently dropping policy names containing colons.
This breaks Kubernetes workload identity where `claim_name=sub`
resolves to `system:serviceaccount:<namespace>:<sa-name>` — a valid
policy name that can be created via the admin API but is then
unreachable during STS session authorization.

Add `:` and `.` to the allowed character set. These characters are:
- Used in K8s service account `sub` claims (colons)
- Used in Java/DNS-style group names from OIDC providers (dots)
- Already accepted by the `add-canned-policy` admin API endpoint

Require at least one alphanumeric character to prevent meaningless
names (`.`, `..`, `-`, `_`, `:`, etc.) from resolving.

Still rejected: `/`, `\`, whitespace, `$`, `;`, `{`, `}` and other
chars that could enable path traversal or injection.

Signed-off-by: Alexander Kharkevich <alex@mara.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: GatewayJ <835269233@qq.com>
2026-06-04 03:39:50 +00:00
Michael 45e3c01857 feat(helm): add topology spread constraints configuration to StatefulSet (#3187)
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-04 03:20:51 +00:00
Henry Guo 013b5b7966 feat(table-catalog): add catalog store entry models (#3201)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-04 03:20:46 +00:00
GatewayJ 785d53fce8 fix(s3select): validate scan_range protocol and parquet overlap (#3176)
* fix(s3select): enforce scan range protocol and parquet overlap

* fix(s3select): select row groups by start offset

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 15:57:17 +00:00
安正超 7b57d5b217 perf(signer): reduce String allocations in V4 signing path (#3197)
* perf(signer): reduce String allocations in V4 signing path

- v4_ignored_headers: HashMap<String, bool> -> HashSet<&'static str>
- Header lookups: k.to_string() -> k.as_str() (zero-copy)
- Query params: Vec<(String, String)> -> Vec<(&str, &str)> (zero-copy)
- Canonical request: single String with push_str instead of 6 intermediate Strings + join
- 20 signer tests pass

* fix(signer): align ignored header set type

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 15:40:35 +00:00
安正超 0fb02049ac fix(ecstore): remove dead _buf field from Erasure struct (#3196)
* fix(ecstore): remove dead _buf field from Erasure struct

_buf was allocated in new(), clone(), and Default but never read or written.
Each clone() was allocating block_size bytes (typically 10MB) for nothing.

- Remove _buf from struct, Default, Clone, new_with_options
- 51 erasure tests pass

* fix: remove empty line after doc comment

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 15:40:29 +00:00
安正超 a63aaf933d fix(notify): normalize lock ordering to prevent deadlock (#3199)
* fix(notify): normalize lock ordering to prevent deadlock

replace_targets() and shutdown() acquired target_list before replay_workers,
opposite to runtime_view.rs order. This is an ABBA deadlock pattern.

- Swap to replay_workers -> target_list order (canonical)
- Add lock order comments
- 82 notify tests pass

* docs(notify): update runtime facade lock order

* docs(notify): clarify runtime facade lock order

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 15:37:59 +00:00
安正超 1838922f07 fix(security): add deny_unknown_fields to deserialization structs (#3198)
* fix(security): add deny_unknown_fields to deserialization structs

Prevent silent acceptance of malformed or adversarial payloads.

- policy: Policy, BucketPolicy, Statement, BPStatement, PrincipalObject
- notify: S3KeyFilter custom deserializer rejects unknown child elements
- update: VersionInfo (remote HTTP response)

26 policy + 82 notify tests pass.

* fix(update): keep version response forward compatible

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 15:33:28 +00:00
Henry Guo e0a03ce10b feat(policy): add table catalog admin actions (#3200)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 15:25:26 +00:00
Henry Guo ad1a489f75 feat(scanner): add scanner budget progress controls (#3185)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-03 14:37:58 +00:00
houseme f49827fc58 fix(iam): prevent transient IAM walk timeout from crashing startup (#3188)
* fix(iam): prevent transient IAM walk timeout from crashing startup

  IAM startup performs a blocking full metadata walk on `.rustfs.sys/config/iam/`.
  When that distributed walk times out (e.g. disk pressure after cluster reboot),
  the old code treated the failure as fatal and exited the process, causing a
  systemd restart loop.

  Changes:
  - Add `startup_iam.rs`: attempt IAM init, enter degraded mode on failure,
    spawn background retry task with exponential backoff (5s→10s→20s→30s cap)
  - Log level escalates to ERROR after 12 retries (~5 min) to aid diagnosis
  - `/health/ready` returns 503 until IAM recovers; IAM-dependent ops return
    `IamSysNotInitialized` (existing fail-closed behavior preserved)
  - Fix admin path boundary matching: `/minio/administrator` no longer falsely
    matches as admin prefix
  - Normalize Content-Length: 0 for admin GET requests with empty body

  Fixes #3175

* fix(iam): move constant assertion into const block

Fixes clippy::assertions-on-constants warning on
IAM_RETRY_ESCALATION_THRESHOLD assertion.

* fix(iam): address PR review comments

- Replace OnceLock with AtomicU64 sentinel for test isolation;
  add reset_test_failure_counter() for integration tests
- Use u32::try_from() instead of `as u32` narrowing cast in
  compute_backoff_interval
- Rename misleading test; update to verify finalize retry behavior
- Restructure spawn_iam_recovery_task into init-retry and
  finalize-retry phases so transient readiness failures are retried
  instead of leaving the server permanently degraded

* fix(iam): gate test hooks behind debug_assertions

- reset_test_failure_counter() now stores sentinel (u64::MAX) to
  correctly trigger env var re-read on next call
- RUSTFS_TEST_IAM_FAIL_INIT_ATTEMPTS only honored in debug builds
- RUSTFS_TEST_IAM_RETRY_INTERVAL_MS only honored in debug builds

* test(iam): cover deferred bootstrap recovery

Add a dedicated embedded deferred-IAM integration test in a separate test binary to avoid process-global startup collisions.

Strengthen startup IAM recovery coverage with focused unit tests and keep the existing embedded smoke test isolated while carrying the manual test license header update in the same change set.

* fix(startup): tighten deferred IAM recovery path

Adopt follow-up review feedback by silencing misleading app-context warnings after IAM recovery, reusing boundary-aware path prefix checks in the readiness gate, and tying deferred IAM recovery retries to server shutdown tokens.

Keep the deferred IAM embedded integration coverage and startup recovery unit coverage green after the follow-up hardening.

* refactor(startup): simplify IAM recovery task

Collapse the deferred IAM recovery implementation back to a concrete production flow instead of keeping boxed callback seams in the runtime path.

Keep only stable backoff unit coverage in startup_iam and rely on the embedded deferred bootstrap integration test for end-to-end recovery behavior.

* refactor(startup): trim IAM recovery test scaffolding

Keep the concrete deferred IAM recovery path intact while removing bulky test-only async loop scaffolding from startup_iam.

Retain the stable backoff unit checks and rely on the embedded deferred bootstrap integration test for end-to-end recovery coverage.

* fix: apply code review improvements from PR #3188 review

- Simplify RecoveryFuture type alias by removing unnecessary lifetime
- Fix finalize_iam_recovery to return Err if app context unavailable
- Update bootstrap_or_defer_iam_init doc comment to reflect Err case
- Use boundary-aware has_path_prefix for admin path matching in utils.rs
- Add test for adminx boundary rejection in utils.rs and layer.rs
- Improve embedded deferred IAM test with timeout wrapper

* style: merge has_path_prefix import into existing use block

* fix(iam): address final review follow-ups

- fix main startup readiness publication to pass ServiceStateManager correctly
- centralize IAM test env keys in rustfs_config and reuse them in runtime/tests
- keep deferred IAM bootstrap validation aligned with the final review fixes

* fix: isolate listing timeouts from drive health

Keep walk_dir scanner timeouts request-scoped instead of marking local drives faulty.

Add regression coverage for follow-up bucket info, set-level list_path, and system-prefix listings after prior walk timeouts.

* test(iam): gate deferred bootstrap test to debug

Align the deferred IAM embedded integration test with debug-only IAM fault injection hooks so release-profile runs do not assert deferred bootstrap behavior that cannot be triggered.

* test(ecstore): bound prior walk timeout regressions

- set walk_dir stall timeout explicitly in prior-timeout listing tests
- keep the system-prefix follow-up listing scoped to the same base dir
- assert the expected directory entry so the timeout regression test stays fast and stable

* fmt
2026-06-03 14:37:25 +00:00
Henry Guo 0b69f363d6 fix: prioritize manual heal queue admission (#3192)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 14:21:20 +00:00
houseme 20bb5dc4a2 fix(ecstore): retry transient walk dir stream errors (#3194)
* fix(ecstore): retry transient walk dir stream errors

Retry RemoteDisk walk_dir once when the first failure looks transport-related so restart-time HttpReader stream errors do not immediately count as hard listing failures.

Improve HttpReader request and stream error messages with method and URL context, and add regression coverage for retry recovery and diagnostics.

* fix(ecstore): avoid retry after partial walk dir stream

Stop retrying walk_dir after copy_stream_with_buffer has already written partial bytes into the destination writer.

Keep the retry only for open_walk_dir failures and add a regression test that proves partial stream failures are returned without issuing a second stream.
2026-06-03 13:38:22 +00:00
Alexander Kharkevich ce6fcf39b1 feat(oidc): add HIDE_FROM_UI option to exclude providers from console login (#3162)
Add `RUSTFS_IDENTITY_OPENID_HIDE_FROM_UI[_<SUFFIX>]` setting that
removes a provider from the login page while keeping it fully
functional for STS AssumeRoleWithWebIdentity and site-replication.

Changes:
- Add `hide_from_ui: bool` to `OidcProviderConfig`
- Add `list_visible_providers()` that filters hidden providers
  (used by console login and /v3/oidc/providers endpoint)
- Keep `list_providers()` unfiltered for site-replication/admin config
- Extract `normalize_provider_config(config) -> config` to deduplicate
  field normalization (accepts the struct directly, not 18 parameters)
- Add `parse_enable_state()` helper for consistent EnableState parsing
- Plumb through admin API request structs (`#[serde(default)]`)
- Expose in `OidcConfigView` for admin GET config round-trip
- Persist via `upsert_persisted_provider_config()`

Note: adding `hide_from_ui` to the public `OidcProviderConfig` struct
is a source-level change for code constructing it with struct literals.
This is acceptable for the current pre-1.0 release cycle.

Signed-off-by: Alexander Kharkevich <alex@mara.com>
Co-authored-by: GatewayJ <835269233@qq.com>
2026-06-03 13:37:44 +00:00
Henry Guo be98c1f86a feat(table): add catalog boundary primitives (#3173)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-03 13:16:59 +00:00
houseme 66b8699927 chore(release): prepare 1.0.0-beta.7 (#3184)
* chore(release): prepare 1.0.0-beta.7

* chore(release): align release assets for 1.0.0-beta.7
2026-06-03 04:18:50 +00:00
GatewayJ cc9e4bb207 fix(admin): normalize empty admin GET content length (#3160)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-06-03 04:12:04 +00:00
Henry Guo cc07946782 feat(scanner): add cycle budget observability (#3166)
* feat(scanner): add cycle budget observability

* fix(scanner): clear cycle state after budget stop

* test(scanner): stabilize timeout-based scanner tests

* fix(scanner): keep cycle ILM counts scanner-only

* test(scanner): avoid test-only pending import

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-06-03 03:32:41 +00:00
Henry Guo 3d9caff3a4 fix(object-lock): allow locked objects to receive new versions (#3179)
* fix(object-lock): allow locked objects to receive new versions

* fix(object-lock): validate copy destination writes

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 02:50:53 +00:00
houseme 29fbdc2dbf feat(ecstore): add object lock diagnostics (#3178)
* feat(ecstore): add object lock diagnostics

Add configurable namespace lock diagnostics for object operations so production contention can be traced by operation, owner, and key.

Wrap object read/write lock acquisition in diagnostic guards across get, head, put, delete, copy, and multipart flows, and log slow acquisition and long hold durations behind new RUSTFS_OBJECT_LOCK_DIAG_* settings.

Verification:

- make pre-commit

* feat(obs): expose object lock diagnostics metrics

Add Prometheus metrics and Grafana panels for object namespace lock diagnostics, covering slow acquire counts, slow hold counts, acquire duration, hold duration, and the diagnostics-enabled state.

Adopt PR review feedback by keeping diagnostic guards alive through the guarded operation so long-hold warnings and metrics are emitted, reusing shared env helpers, and reducing default-path overhead when diagnostics are disabled.

Verification:

- cargo check -p rustfs-ecstore -p rustfs-io-metrics

- cargo test -p rustfs-ecstore store::object -- --nocapture

- make pre-commit

* perf(obs): reduce object lock diag overhead

Avoid repeated environment parsing on hot object-lock paths by caching the diagnostics-enabled flag, and stop allocating label strings for object lock metrics by recording static labels directly.

Strengthen io-metrics tests by using a local recorder and asserting that the expected object lock diagnostic counters, gauges, and histograms are emitted.

Verification:

- cargo check -p rustfs-ecstore -p rustfs-io-metrics

- cargo test -p rustfs-io-metrics -- --nocapture

- make pre-commit
2026-06-03 02:10:27 +00:00
安正超 0dbf0b13a8 fix(lock): align distributed acquisition retries (#3177)
* fix(lock): align distributed acquisition retries

* fix(lock): retry remote lock RPC timeouts

* fix(lock): quiet retryable acquisition logs

* fix(lock): enforce attempt acquisition deadline

* fix(ci): install unzip in setup action

* fix(lock): handle acquisition review followups

* fix(ci): use nextest install-action shorthand

* fix(lock): preserve hard failures on attempt timeout

* fix(lock): retry attempt timeout when quorum remains possible

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 01:58:47 +00:00
安正超 d817bd4449 fix(ci): stabilize Rust setup action (#3163)
* fix(ecstore): restore windows endpoint validation build

* fix(ci): install unzip for protoc setup

* fix(ci): use nextest install-action shorthand

* fix(ci): restore cargo-nextest action ref

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-03 01:51:30 +00:00
Michael 054523695f fix(helm): add apiVersion and kind to PersistentVolumeClaim metadata (#3170) 2026-06-03 09:27:43 +08:00
安正超 ae2d3c4025 ci: restore ubicloud runners for heavy jobs (#3183) 2026-06-03 08:38:17 +08:00
安正超 14aaab9406 ci: use hosted runners for light jobs (#3181) 2026-06-02 23:17:56 +08:00
Henry Guo 15be56f242 docs(table): add S3 table concept model (#3172)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-02 23:05:30 +08:00
安正超 7cc730d9c0 ci: isolate s3 test runner state (#3180) 2026-06-02 22:40:37 +08:00
cxymds a55ead42e7 fix(admin): prefer explicit TLS for site replication endpoint (#3171) 2026-06-02 11:37:59 +00:00
houseme 0d00b886ac fix(rio): map truncated put bodies to incompletebody (#3168)
* fix(rio): surface incomplete put bodies

Propagate incomplete PUT request bodies as IncompleteBody instead of allowing erasure encode to treat truncated input as a normal EOF.\n\n- mark premature EOFs in HardLimitReader with an explicit IncompleteBody error\n- preserve EOF error chains through read_full and map them to S3 IncompleteBody\n- stop erasure encode from swallowing UnexpectedEof on truncated input\n- add regression tests for reader, erasure encode, and API error mapping\n\nRefs: rustfs/backlog#654

* fix(s3): honor decoded length for aws chunked put

Use x-amz-decoded-content-length for aws-chunked PutObject requests so trailer-checksum uploads are sized against the decoded payload instead of the wire-encoded content-length.\n\n- prefer decoded content length for aws-chunked put bodies\n- add a regression test covering the size selection logic\n- keeps the incomplete body fix working for truly truncated uploads while restoring checksum trailer compatibility\n\nRefs: rustfs/backlog#654

* fix(io): follow up review comments on incompletebody handling

Address PR review feedback by restoring read_full's existing EOF contract, adding a dedicated read_full_or_eof helper for erasure encoding, covering nested incomplete-body error chains, and documenting plus hardening aws-chunked size selection.\n\n- keep read_full returning early EOF on empty reads\n- use read_full_or_eof only in erasure encoding paths\n- detect aws-chunked via content-encoding or transfer-encoding\n- add nested error-chain and aws-chunked regression tests\n\nRefs: rustfs/backlog#654

* fix(rio): surface incomplete put bodies

Propagate incomplete PUT request bodies as IncompleteBody instead of allowing erasure encode to treat truncated input as a normal EOF.\n\n- mark premature EOFs in HardLimitReader with an explicit IncompleteBody error\n- preserve EOF error chains through read_full and map them to S3 IncompleteBody\n- stop erasure encode from swallowing UnexpectedEof on truncated input\n- add regression tests for reader, erasure encode, and API error mapping\n\nRefs: rustfs/backlog#654

* fix(s3): honor decoded length for aws chunked put

Use x-amz-decoded-content-length for aws-chunked PutObject requests so trailer-checksum uploads are sized against the decoded payload instead of the wire-encoded content-length.\n\n- prefer decoded content length for aws-chunked put bodies\n- add a regression test covering the size selection logic\n- keeps the incomplete body fix working for truly truncated uploads while restoring checksum trailer compatibility\n\nRefs: rustfs/backlog#654

* fix(io): follow up review comments on incompletebody handling

Address PR review feedback by restoring read_full's existing EOF contract, adding a dedicated read_full_or_eof helper for erasure encoding, covering nested incomplete-body error chains, and documenting plus hardening aws-chunked size selection.\n\n- keep read_full returning early EOF on empty reads\n- use read_full_or_eof only in erasure encoding paths\n- detect aws-chunked via content-encoding or transfer-encoding\n- add nested error-chain and aws-chunked regression tests\n\nRefs: rustfs/backlog#654

* fix(rio): reject bytes beyond hard limit

* fix(ecstore): reject zero-sized erasure blocks
2026-06-02 11:31:51 +00:00
唐小鸭 480babc0af fix: window error (#3167)
fix window error
2026-06-02 05:09:57 +00:00
Henry Guo 1d46047d6f feat(scanner): expand scanner observability metrics (#3159)
* feat(scanner): expand scanner observability metrics

* chore(scanner): align bucket-drive metric wording

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-06-02 01:29:53 +00:00
cxymds 39a283fbce fix(windows): handle zfs volume paths (#3157)
* fix(windows): handle zfs volume paths

* fix(windows): address volume path review

* refactor(windows): share fallback path resolution

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-06-01 13:04:40 +00:00
CptOfEvilMinions e91e513ab3 feat: Helm chart support extra volumes (#2982) 2026-06-01 16:52:35 +08:00
Henry Guo f3bd838925 feat(scanner): expose cycle progress metrics (#3152)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-01 07:54:39 +00:00
dependabot[bot] cbb6f9c76f chore(deps): bump the dependencies group with 6 updates (#3151)
* chore(deps): bump the dependencies group with 6 updates

Bumps the dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [hyper](https://github.com/hyperium/hyper) | `1.10.0` | `1.10.1` |
| [serial_test](https://github.com/palfrey/serial_test) | `3.4.0` | `3.5.0` |
| [snafu](https://github.com/shepmaster/snafu) | `0.9.0` | `0.9.1` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.1` | `1.23.2` |
| [dial9-tokio-telemetry](https://github.com/dial9-rs/dial9-tokio-telemetry) | `0.3.12` | `0.3.13` |
| [pyroscope](https://github.com/grafana/pyroscope-rs) | `2.0.5` | `2.0.6` |


Updates `hyper` from 1.10.0 to 1.10.1
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.10.0...v1.10.1)

Updates `serial_test` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/palfrey/serial_test/releases)
- [Commits](https://github.com/palfrey/serial_test/compare/v3.4.0...v3.5.0)

Updates `snafu` from 0.9.0 to 0.9.1
- [Changelog](https://github.com/shepmaster/snafu/blob/main/CHANGELOG.md)
- [Commits](https://github.com/shepmaster/snafu/compare/0.9.0...0.9.1)

Updates `uuid` from 1.23.1 to 1.23.2
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.1...v1.23.2)

Updates `dial9-tokio-telemetry` from 0.3.12 to 0.3.13
- [Release notes](https://github.com/dial9-rs/dial9-tokio-telemetry/releases)
- [Changelog](https://github.com/dial9-rs/dial9/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dial9-rs/dial9-tokio-telemetry/compare/dial9-tokio-telemetry-v0.3.12...dial9-tokio-telemetry-v0.3.13)

Updates `pyroscope` from 2.0.5 to 2.0.6
- [Release notes](https://github.com/grafana/pyroscope-rs/releases)
- [Changelog](https://github.com/grafana/pyroscope-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/grafana/pyroscope-rs/compare/lib-2.0.5...lib-2.0.6)

---
updated-dependencies:
- dependency-name: hyper
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: serial_test
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: snafu
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: uuid
  dependency-version: 1.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: dial9-tokio-telemetry
  dependency-version: 0.3.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: pyroscope
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(profiling): pin pyroscope to 2.0.5, add jemalloc memory profiling, unify platform gates

Pin pyroscope to =2.0.5 to avoid duplicate `perf_signal_handler` symbol
caused by 2.0.6 vendoring pprof-rs internally. 2.0.5 depends on external
pprof-pyroscope-fork (v0.1500.4), which Cargo unifies with the workspace's
pprof-pyroscope-fork — a single symbol instance, no linker collision.

Add jemalloc_backend for continuous memory profiling export to Pyroscope,
alongside the existing pprof-based CPU profiling. Both CPU and memory
profiling now work on linux and macos.

Unify platform gates from `cfg(all(target_os = "linux", target_env = "gnu",
target_arch = "x86_64"))` to `cfg(any(target_os = "linux",
target_os = "macos"))`, switching macOS global allocator from mimalloc to
jemalloc.

* style: format cfg attributes in allocator_reclaim.rs

* chore(deps): ignore pyroscope 2.x in dependabot, add profile_type tag

Add pyroscope 2.x to dependabot ignore list to prevent auto-upgrade
past 2.0.5 (>=2.0.6 vendors pprof-rs, causing duplicate symbol conflict).

Add profile_type=cpu tag to CPU profiling agent for differentiation
from memory profiling agent in Pyroscope UI.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-06-01 07:21:47 +00:00
majinghe 4d2f13af6f chore(action): add self-host runner support. (#3155) 2026-06-01 15:11:50 +08:00
安正超 97dd825468 fix(ecstore): optimize ObjectInfo clone and fix critical TODOs (#3149)
* fix(ecstore): optimize ObjectInfo clone and fix critical TODOs

## ObjectInfo hot-path clone optimization (issue #653 item 2)
- Wrap user_defined (HashMap), user_tags (String), parts (Vec) in Arc
- Clone cost reduced from ~20 heap allocations to O(1) ref count bump
- Updated 11 downstream access sites with explicit deref where needed

## Critical TODO/FIXME fixes (issue #653 item 7)
- rpc/peer_s3_client.rs: descriptive error message for empty peer response
- set_disk/write.rs: concurrent rollback deletes via tokio::spawn + join_all
- store_list_objects.rs: resolved FIXME with explanation + 4 regression tests
- store/bucket.rs: namespace write locks for make_bucket and delete_bucket

Skipped TODOs (too risky without broader context):
- multipart.rs:30 nslock — causes lock timeout in existing tests
- bucket.rs:88 cached list_bucket — needs cache invalidation strategy
- bucket.rs:149 replication delete — needs replication subsystem integration

1134 tests pass (1 flaky under parallel execution, passes individually).

* fix: address PR #3149 review comments

- Preserve NamespaceLockQuorumUnavailable variant in bucket create/delete
- Fix cancellation test to actually test cancellation (keep senders open)
- Update ObjectInfo consumers in app/ for Arc-backed fields
- Update bucket_usecase.rs user_tags to Arc<String>

* fix: update ObjectInfo Arc consumers

* fix: address ecstore merge review comments

* fix: cancel blocked merge output sends

* fix(ecstore): satisfy ObjectInfo clone clippy
2026-06-01 00:44:58 +00:00
安正超 bd571a575f fix(io-core,signer): replace unwrap() with proper error handling (#3150)
* fix(io-core,signer): replace unwrap() with proper error handling

## io-core (issue #653 item 1)
- pool.rs: replace 8 .lock().unwrap() with poisoned recovery
- pool.rs: replace semaphore acquire unwrap with graceful fallback
- deadlock_detector.rs: replace 5 .lock().unwrap() with match/ok

## signer (issue #653 item 1)
- Add SignV2Error enum, try_pre_sign_v2, try_sign_v2
- Replace 14 unwrap() in v2 signing with ? propagation
- Add try_streaming_sign_v4, replace 5 expect("err") with descriptive errors
- get_host_addr returns Result instead of panicking

All backward-compat wrappers preserved. 95 io-core + 18 signer tests pass.

* fix: address signer and pool review comments

* test: update signer v2 string-to-sign test

* fix: address signer and pool review followups

* fix: tighten signer host and pool fallback
2026-05-31 23:40:20 +00:00
GatewayJ 9ce9ec22d1 fix(ecstore): tighten object copy rename handling (#3131)
* fix(ecstore): tighten object copy rename handling

* fix(ecstore): narrow copy lock lifetime

* test(ecstore): cover reverse copy concurrency

* fix(multipart): ignore preconditions for internal lookup

* fix(ecstore): clean precondition lock bindings

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-31 23:19:21 +00:00
Henry Guo 76da2a48d0 feat(scanner): expose cycle observability controls (#3147)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-31 21:55:46 +00:00
安正超 a14657f517 fix(scanner,data-usage): fix add() logic inversion and usize underflow in reduce_children_of (#3142)
* fix(scanner,data-usage): fix add() logic inversion and usize underflow in reduce_children_of

Bug #1: add() had inverted logic — returned early when children were present,
making the recursive child traversal dead code. Leaf compaction path was
completely non-functional. Fixed by recursing into children when present and
collecting leaves only at leaf nodes.

Bug #2: reduce_children_of used  which underflows in debug
(panic) or release (wraps to usize::MAX, compacts entire cache). Fixed with
saturating_sub.

Both bugs existed identically in crates/scanner and crates/data-usage.

Added 5 tests covering add() subtree traversal, edge cases, and
reduce_children_of compaction behavior.

Closes rustfs/backlog#652

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: collect internal nodes as compaction candidates, not leaves

The previous fix for add() collected leaf nodes (children empty) as
compaction candidates. This is incorrect: total_children_rec returns 0
for leaves, so compacting them never decrements  — the
compaction loop makes no progress.

Collect internal nodes (children non-empty) instead. Compacting an
internal node removes its subtree via delete_recursive, which actually
reduces the total child count. Leaf nodes are skipped since they have
no children to remove.

Updated tests to match the corrected semantics.

* refactor: rename leaves→candidates, add data-usage regression tests

- Rename misleading  variable/parameter to  in
  add() and reduce_children_of() for both crates
- Add 4 regression tests in crates/data-usage covering add() candidate
  selection and reduce_children_of() compaction + saturating_sub

* fix: address data usage compaction review feedback

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-31 21:52:53 +00:00
houseme 315c2e33f0 test(admin): add route registration coverage for config admin APIs (#3148)
Add test assertions for all MinIO-compatible config admin routes
(get/set/del-config-kv, help-config-kv, list/clear/restore-config-history-kv,
GET/PUT /v3/config) and verify their compatibility alias paths.

Relates to rustfs/backlog#608
2026-05-31 18:13:08 +00:00
Henry Guo a99ef64db2 feat(scanner): add scanner budgets and progress metrics (#3145)
* fix(scanner): preserve maintenance scan cadence

* feat(scanner): add scanner concurrency budget

* feat(scanner): expose scanner runtime progress

* fix(scanner): address scanner review feedback

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-31 16:42:38 +00:00
Henry Guo a8557ceb0e fix(rebalance): require target goal before completion (#3141)
* fix(rebalance): require target goal before completion

Stop treating pools within a fixed tolerance of the rebalance goal as completed before any data has moved.

Add a regression test for the two-pool imbalance reported in issue 3137.

* chore(rebalance): clarify goal completion wording

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-31 14:54:12 +00:00
安正超 81b899e9c6 docs: update security advisory lessons (#3143) 2026-05-31 23:01:35 +08:00
安正超 b5e565c0ce chore(agents): add Rust code quality rules and skill (#3144)
* docs: update security advisory lessons

* chore(agents): add Rust code quality rules and skill

Add rules derived from full-project code review (48 findings across
7 dimensions) to prevent recurring issues in agent-generated code.

AGENTS.md changes:
- crates/AGENTS.md: error type design, concurrency, recursion safety,
  type casting, test quality rules
- root AGENTS.md: serde safety, naming conventions
- crates/ecstore/AGENTS.md: allocation discipline, lock ordering,
  recursion safety, dead code policy
- crates/notify/AGENTS.md: lock ordering for runtime_view/facade

Skill changes:
- code-change-verification: add Rust-specific checks (unwrap, as cast,
  clone, lock order, recursion, error types, test assertions)
- security-advisory-lessons: add serde deserialization safety pattern
- NEW rust-code-quality: automated scan + manual review checklist
2026-05-31 22:19:36 +08:00
houseme 8577bd825e feat(admin): restore config admin compatibility (#3133)
* feat(admin): restore config admin compatibility

Co-authored-by: weisd <im@weisd.in>

* fix(admin): align config admin clean rebuild

Co-authored-by: weisd <im@weisd.in>

* fix(admin): align config history and peer signals

* fix(admin): harden config admin mutations

* fix(admin): tighten config review follow-ups

* perf(admin): reuse env snapshot in config render

* fix(ecstore): clean up config admin and listing error handling

Remove redundant is_all_volume_not_found check in list_merged, add
storage class encode/decode roundtrip tests, fresh boot integration
test, and config admin clean rebuild improvements.

Co-authored-by: hehutu <heihutu@gmail.com>

* fix(admin): sync global server config on mutation and reload

Change GLOBAL_SERVER_CONFIG from OnceLock to RwLock so config mutations
(set/del/restore/reload) are visible to readers without restart. Call
set_global_server_config after every store save and on snapshot reload.
Register storage_class as a dynamic config subsystem.

Co-authored-by: hehutu <heihutu@gmail.com>

* style: apply rustfmt to config and admin tests

Co-authored-by: hehutu <heihutu@gmail.com>

* fix(test): update signal_service test for storage_class dynamic subsystem

storage_class is now a valid dynamic config subsystem, so the
"requires object layer" test should expect "storage layer not initialized"
instead of "unsupported dynamic config subsystem".

Co-authored-by: hehutu <heihutu@gmail.com>

* fix(config): publish storage_class runtime config on dynamic reload

Change GLOBAL_STORAGE_CLASS from OnceLock to RwLock so runtime updates
are possible. apply_storage_class_runtime_config now actually publishes
the parsed config via set_global_storage_class instead of dropping it.

Addresses review feedback: storage_class was marked as dynamically
applied but the parsed result was discarded, so mc admin config set
returned config_applied=true while the runtime kept using stale parity
settings until restart.

Co-authored-by: hehutu <heihutu@gmail.com>

* fix(admin): harden config init, history ordering, and env redaction

- Change GLOBAL_SERVER_CONFIG from RwLock<Config> to RwLock<Option<Config>>
  initialized with None, preserving "not initialized" detection via None
- Move save_server_config_history before save_server_config_to_store in
  SetConfigKVHandler, DelConfigKVHandler, and SetConfigHandler so a
  restore point exists before mutations are persisted
- Redact sensitive env override values with *redacted* instead of
  silently omitting the line, improving admin visibility
- Add code comment explaining VolumeNotFound removal rationale in
  list_merged for listing paths

Co-authored-by: hehutu <heihutu@gmail.com>

* fix(config): keep in-memory config in sync after set/restore/reload

GLOBAL_SERVER_CONFIG was a OnceLock set once at startup and never
updated. After mc admin config set writes to the store, any fallback
to get_global_server_config() returned stale init-time data. Similarly,
reload_runtime_config_snapshot read from the store but discarded the
result.

- Replace OnceLock with RwLock for GLOBAL_SERVER_CONFIG and
  GLOBAL_STORAGE_CLASS so they can be updated at runtime
- Add set_global_server_config / set_global_storage_class setters
- Call set_global_server_config after every config save (set-kv,
  del-kv, set-config, restore-history)
- Re-apply dynamic subsystems (storage_class, audit_webhook,
  audit_mqtt) and signal peers in reload_runtime_config_snapshot
  and full-config operations
- Fix render_selected_config scope boundary check: track per-scope
  line count instead of checking global lines.is_empty()
- Include STORAGE_CLASS_SUB_SYS in is_dynamic_config_subsystem so
  apply_storage_class_runtime_config is reachable

Co-authored-by: hehutu <heihutu@gmail.com>

* fix(storageclass): use CLASS_RRS key in lookup_config for RRS parity

lookup_config used kvs.get(RRS) where RRS="REDUCED_REDUNDANCY", but the
admin config path writes the key as CLASS_RRS="rrs". This caused RRS
values to never be read back, always falling back to default parity.

- Changed kvs.get(RRS) to kvs.get(CLASS_RRS) in lookup_config
- Added regression tests verifying RRS read/write consistency

Co-authored-by: hehutu <heihutu@gmail.com>

* fix(config): add peer-side logging and don't swallow apply errors

- Add tracing::warn! in reload_dynamic_config_runtime_state and
  reload_runtime_config_snapshot when config read or subsystem apply
  fails, so on-host diagnostics show which signal failed and why
- Change `let _ = apply_dynamic_config_for_subsystem(...)` to
  `if let Err(err) = ... { warn!(...) }` in reload_runtime_config_snapshot
  so per-subsystem failures are logged instead of silently swallowed
- Remove weak test global_server_config_returns_none_before_init that
  had no meaningful assertion due to shared global state

Co-authored-by: hehutu <heihutu@gmail.com>

* style: apply rustfmt to config and storageclass tests

Co-authored-by: hehutu <heihutu@gmail.com>

---------

Co-authored-by: weisd <im@weisd.in>
Co-authored-by: hehutu <heihutu@gmail.com>
2026-05-31 11:50:13 +00:00
Henry Guo 92104cb354 fix(scanner): reduce single-disk scanner churn (#3135)
* fix(scanner): slow single-disk default scans

* fix(scanner): reduce single-disk scanner churn

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-31 05:05:04 +00:00
Henry Guo d921af6ef7 fix(heal): normalize completed root heal state (#3140)
Clear persisted background heal active state after a deep scanner cycle completes so root heal status does not remain in progress between cycles.

Treat path-based stop for an already missing heal task as stopped while keeping client-token cancellation strict.

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-31 04:47:36 +00:00
安正超 e12d63234a test(targets): cover Kafka check SASL validation (#3136)
test(targets): cover kafka check sasl validation

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-31 04:47:21 +00:00
houseme d951c09cac chore(deps): update flake.lock (#3139)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/3d8f0f3' (2026-05-23)
  → 'github:NixOS/nixpkgs/e9a7635' (2026-05-29)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/d9973e2' (2026-05-23)
  → 'github:oxalica/rust-overlay/85570ef' (2026-05-30)
2026-05-31 01:59:51 +00:00
安正超 cf55743579 perf: reduce spawn_blocking contention in PUT path (#3132)
* perf: reduce spawn_blocking contention in PUT path (~23% throughput gain)

Flame graph profiling identified tokio blocking pool mutex contention
as the #1 bottleneck (17.3% of CPU time). Each spawn_blocking call must
acquire parking_lot::raw_mutex to enqueue work. With 16 concurrent PUTs
× 4 disks × 3+ spawn_blocking per disk, this became a serialization point.

Optimizations applied:
- Merge make_dir_all + file write into single spawn_blocking
- Merge read_file + parse + write + rename into single spawn_blocking
  for inline objects (small files)
- Optimize reliable_rename to try rename first, mkdir only on ENOENT
- Optimize remove/remove_std to try remove_file first, EISDIR fallback
- Add encode_inline_small fast path for small objects
- Parallelize bitrot writer creation with join_all

Benchmark (4KiB PUT, 4-disk EC, 16 concurrent, 8 rounds, randomized A/B):
  Baseline: ~950 obj/s → Optimized: ~1173 obj/s (+23%)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address review comments

- Return old_data_dir for non-inline rename_data path (was incorrectly None)
- Restore delete_all cleanup of PUT temp data on failure paths
- Fix cargo fmt formatting

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style: apply rustfmt from stable 1.96.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: collapse nested if-let chains for clippy compliance

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address Copilot review comments

- fs.rs: handle macOS EPERM from remove_file on directories
- os.rs: restore NotFound=Ok(()) semantics on first rename attempt
- local.rs: use try-rename-then-mkdir pattern for inline rename_data

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test: add unit tests for encode_inline_small fast path

* test: fix comment and line length in encode_inline_small tests

* fix: revert reliable_rename and write_all_internal to match original

Restore the original reliable_rename logic (check parent exists, then
rename in loop) and the original write_all_internal (make_dir_all outside
spawn_blocking). The optimization changes caused a CI-only test failure
in capacity_dirty_scope_test that could not be reproduced locally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: update comment and fix formatting for CI

- Fix macOS/BSD comment to accurately say macOS only
- Fix encode_inline_small test formatting to match rustfmt 1.96.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: propagate inline rename errors

* fix: retry rename when parent missing

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-31 01:28:48 +00:00
weisd ca4793f93e fix(s3): preserve listing pagination parity (#3117)
* fix: preserve S3 listing pagination parity

S3 listing responses need stable wire semantics across encoded listings, version markers, and quorum-sensitive metadata reads. This tightens marker handling, response encoding, and version-marker pagination while keeping the changes scoped to listing paths and regression tests.

Constraint: Branch, code, commit, and PR text must avoid restricted upstream project naming.
Constraint: Verification required Rust 1.95 toolchain path because Homebrew cargo-clippy resolved to 1.94.
Rejected: Treat null version-id-marker as no marker | repeats or skips the marker boundary for null-version listings.
Rejected: Compare encoded next-marker values directly | encoded output can diverge from raw listing order.
Confidence: high
Scope-risk: moderate
Directive: Do not change listing marker semantics without covering V1, V2, versions, encoded responses, and null-version markers together.
Tested: cargo fmt --all --check
Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings
Tested: make pre-commit with LC_ALL=en_US.UTF-8 and Rust 1.95 PATH
Tested: git diff --check
Not-tested: live distributed object-store compatibility test against a remote cluster

* fix: keep listing echo fields raw

S3 compatibility tests expect list response echo fields such as Prefix, Delimiter, StartAfter, and Marker to preserve the request value even when URL encoding is requested. Keep URL encoding scoped to object keys and common prefixes while preserving the next-marker raw comparison fix.

Constraint: PR branch was updated with latest main before this fix.

Constraint: Do not use restricted upstream project naming in commit text.

Rejected: Encode all response string fields | breaks compatibility tests for unreadable prefix values.

Confidence: high

Scope-risk: narrow

Tested: cargo test -p rustfs list_objects --lib

Tested: cargo fmt --all --check

Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings

Tested: make pre-commit

Tested: git diff --check

Not-tested: live CI s3 compatibility rerun before push

* fix: accept empty listing continuation token

S3 compatibility tests treat an empty ListObjectsV2 continuation token as an explicit empty echo value, not as an invalid base64 token. Preserve the request echo while skipping decoded-token pagination for the empty string.

Constraint: Keep invalid non-empty continuation tokens rejected before store lookup.

Rejected: Drop empty continuation tokens from the response | compatibility tests assert the empty echo field is present.

Confidence: high

Scope-risk: narrow

Tested: cargo test -p rustfs list_objects --lib

Tested: cargo fmt --all --check

Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings

Tested: make pre-commit

Tested: git diff --check

Not-tested: live CI s3 compatibility rerun after this push

* fix: delete explicit null object versions

S3 compatibility cleanup lists unversioned objects with VersionId=null and then sends that value back through DeleteObjects. The API layer keeps null as an internal sentinel, but the storage delete path must map it back to the stored null version instead of treating it as a real UUID.

Constraint: Keep the wire response able to echo null version IDs.

Rejected: Treat VersionId=null the same as an absent version id everywhere | versioned and suspended buckets need explicit null version semantics.

Confidence: high

Scope-risk: moderate

Tested: cargo test -p rustfs-ecstore delete_file_info_version_id_maps_explicit_null_version_to_stored_null

Tested: cargo test -p rustfs normalize_delete_objects_version_id_preserves_explicit_null_marker --lib

Tested: cargo fmt --all --check

Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings

Tested: make pre-commit

Tested: git diff --check

Not-tested: live CI s3 compatibility rerun after this push

* fix: keep version marker scoped to marker key

Version listing cleanup can delete the key returned as the previous page marker before requesting the next page. When the marker key is no longer present, the version marker must not be applied to the first later key, otherwise each cleanup page can skip one null-version object and leave the bucket non-empty.

Constraint: Preserve version-marker pagination for marker keys that still exist.

Rejected: Drop version markers whenever the marker key was supplied | multi-version marker keys still need intra-key pagination.

Confidence: high

Scope-risk: narrow

Tested: cargo test -p rustfs-ecstore version_marker_is_applied_only_when_key_marker_entry_is_present

Tested: cargo test -p rustfs-ecstore delete_file_info_version_id_maps_explicit_null_version_to_stored_null

Tested: cargo test -p rustfs normalize_delete_objects_version_id_preserves_explicit_null_marker --lib

Tested: cargo fmt --all --check

Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings

Tested: make pre-commit

Tested: git diff --check

Not-tested: live CI s3 compatibility rerun after this push

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-30 11:19:59 +00:00
Henry Guo 8e809f005d fix(scanner): support PBS subfolder alert threshold (#3129)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-30 11:03:13 +00:00
Henry Guo 5bdbd66d09 feat(targets): support Kafka SASL auth (#3128)
* feat(targets): support Kafka SASL auth

* fix(targets): infer Kafka SASL when enable is blank

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-30 11:01:17 +00:00
安正超 97cd19becc fix(sse): handle case-insensitive encryption metadata (#3127)
* fix(sse): handle case-insensitive encryption metadata

* test(ecstore): cover case-insensitive managed material resolution
2026-05-30 02:38:53 +00:00
houseme d5f9467368 ci(build): enable cross-compilation for macOS x86_64 target (#3125)
Set cross=true for the macos-x86_64 build matrix entry so that the
  x86_64-apple-darwin target is cross-compiled on macOS ARM runners
  instead of relying on a native x86_64 macOS runner.
2026-05-29 23:34:47 +08:00
wood 58ac19f7a4 fix(sse): optimize is_encrypted for old metadata compatibility (#3113)
Signed-off-by: w0od <dingboning02@163.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-29 11:59:08 +00:00
安正超 ac97ceb744 fix(config): restore default credential startup (#3114)
* fix(config): restore default credential startup

* fix: align e2e credentials with server env

* fix(config): restore default credential consistency

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-29 11:52:46 +00:00
houseme 5d74637968 fix(utils): tolerate bavail greater than bfree on Linux (#3119)
* fix(utils): tolerate bavail greater than bfree on Linux

Treat f_bavail > f_bfree as a compatibility edge case instead of fatal corruption during startup disk info probing.

- keep corruption checks for other invalid counter relationships
- log a warning and clamp reserved blocks to 0 when bavail exceeds bfree
- add linux unit coverage for the compatibility and overflow paths

Refs #3025

* fix(utils): cap bavail to bfree for Linux statfs edge

* fix(utils): rate-limit Linux statfs compatibility warning

* test(utils): cover Linux statfs capacity math

Expand Linux statfs capacity math tests around normal, equal, zero, and fully-free block counter combinations.

Refs #3025

* fix(utils): avoid statfs warning guard reallocations

* test(utils): align statfs reserved block expectations

* fmt

* fix(utils): avoid warning guard path allocation

Check the warn-once set before allocating a PathBuf so repeated bavail/bfree compatibility warnings stay low-overhead.

Refs #3025
2026-05-29 11:20:47 +00:00
houseme 9f78cd3896 chore(deps): bump workspace dependencies (#3118)
* chore(deps): bump workspace dependencies

- bump direct dependency versions in Cargo.toml\n- refresh Cargo.lock to resolve updated crates

* fix(deps): avoid pyroscope and pprof linker collision

- downgrade pyroscope from 2.0.6 to 2.0.5\n- keep a single pprof implementation path via pprof-pyroscope-fork\n- fix duplicate perf_signal_handler during test linking
2026-05-29 09:36:19 +00:00
houseme 2b82432f9e fix(ecstore): send valid ping body in remote locker (#3112)
* fix(ecstore): send valid ping body in remote locker

Build ping requests with a flatbuffer payload so health checks remain compatible with the ping response parser after restart.

* fix(bench): use multi-host warp target during failover

Normalize comma-separated warp host lists in run_object_batch_bench and let four-node failover bench pass BENCH_WARP_HOSTS so rolling restart does not pin load to a single restarting node.

* feat(health): add compat health probes with busy/KMS checks

  - Add /health/live liveness probe endpoint
  - Add busy protection (429) for readiness probes, gated by RUSTFS_HEALTH_COMPAT_BUSY_CHECK_ENABLE
  - Add KMS readiness check for /health/ready, gated by RUSTFS_HEALTH_COMPAT_KMS_READY_CHECK_ENABLE
  - Add lock quorum status caching with TTL to reduce RPC pressure
  - Consolidate health response building into build_health_response_parts
  - Register /health/live in console router and readiness gate
  - Remove MinIO references from newly added health code

* fix(health): decouple kms readiness from lock quorum
2026-05-29 08:02:50 +00:00
GatewayJ c257043b63 fix(iam): serialize IAM cache writes (#3105)
* fix(iam): serialize IAM cache writes

* fix(iam): timestamp rebuilt group memberships

* fix(iam): publish cache updates atomically

* fix(iam): reuse policy cache snapshots

* fix(iam): commit missing user notification cache updates atomically

* fix(iam): remove unused cache membership rebuild wrapper

---------

Co-authored-by: 季宏伟 <jihongwei@jihongweis-MacBook-Pro.local>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-29 08:02:42 +00:00
houseme ede813070f fix(site-replication): refresh TLS peer client and tls inspect alias (#3109)
* fix(site-replication): refresh peer TLS client cache

- rebuild site-replication peer reqwest client when outbound TLS generation changes\n- keep cached client/failed state per generation for low-overhead requests\n- accept --tls-path as alias of tls inspect --path for operator compatibility\n- add targeted parser/cache tests\n\nRefs #2723

* docs(issue-2723): add private-ca cert rotation retest

- add executable retest checklist for private CA site-replication flows\n- cover tls inspect compatibility (--path and --tls-path)\n- include post-rotation and post-restart pass criteria\n\nRefs #2723

* test(site-replication): isolate global TLS cache test

- serialize generation-sensitive peer TLS cache test\n- snapshot and restore global outbound TLS generation and static cache\n- avoid order-dependent interactions with parallel test execution\n\nRefs #2723

* chore: remove docs file from tracking, keep locally

The docs directory is already in .gitignore but this file was
previously committed. Remove it from git index to prevent docs
content from being included in the PR.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(site-replication): cover ready client cache hit path

Add a focused unit test for unchanged-generation Ready cache entries in site_replication_peer_client_cache_hit to validate steady-state client reuse behavior highlighted in PR #3109 review feedback.

---------

Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-05-29 07:17:55 +00:00
安正超 b436272642 fix: tolerate stalled listing readers after quorum (#3110)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-29 08:57:57 +08:00
houseme 1de0ba916d fix(ecstore): reduce restart-time startup race noise (#3108)
Treat empty ping bodies as liveness probes, add a startup cleanup barrier for early walk_dir calls, and delay immediate background cleanup/capacity timers to reduce transient restart-time VolumeNotFound noise.

Also downgrade expected missing-path producer results during startup from generic errors to warnings while preserving existing storage semantics.
2026-05-28 15:14:30 +00:00
houseme 088c4bda43 fix(ecstore): harden issue3031 multipart validation path (#3106)
* fix(ecstore): harden issue3031 multipart validation path

- clear stale multipart part destinations before rename fan-out
- add repeated part overwrite regression coverage
- reduce remote disk startup false-fault escalation to suspect-first
- refine remote locker diagnostics and lower scanner leader-lock log noise
- add a dedicated 4-node issue3031 docker validation script

* refactor(admin): inline console version json macro

- drop the unused serde_json::json import in admin console
- call serde_json::json! inline in version_handler
- keep the console version response behavior unchanged

* fix(remote-disk): recover suspect health on probe success

- record probe success during remote disk health checks so suspect drives recover
- use async_with_vars for the remote disk health probe test
- make the missing-listener test assert the state transition more robustly
2026-05-28 14:26:31 +00:00
GatewayJ 8d20e89bf8 fix(iam): avoid stale cache replacement on walk errors (#3094)
* fix(iam): avoid stale cache replacement on walk errors

* fix(iam): guard full reload cache commits

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 季宏伟 <jihongwei@jihongweis-MacBook-Pro.local>
2026-05-28 09:54:06 +00:00
houseme 28bac7fbd6 chore(release): prepare 1.0.0-beta.6 (#3104)
* chore(release): prepare 1.0.0-beta.6

* ci(nix): harden flaky crate fetch handling

* ci(nix): drop magic cache and force fallback

* ci(nix): set explicit user-agent for crate fetch

* ci(nix): adopt determinate nix workflow stack

* ci(nix): add nix user-agent suffix for fetches

* ci(nix): add flakehub cache and align determinate actions

* ci(nix): pin determinate actions to release tags

* ci(nix): disable flakehub auth path in CI cache

* ci(nix): restore stable magic cache baseline

* ci(nix): trust local magic cache substituter

* ci(nix): stop forcing Node24 for JS actions

* ci(nix): drop manual localhost cache config

* ci(nix): adopt latest determinate flakehub stack

* ci(nix): record latest determinate workflow state
2026-05-28 09:21:16 +00:00
cxymds 527faad575 docs(readme): add Discord community links (#3103) 2026-05-28 12:22:17 +08:00
houseme dd1ffbaee7 fix(lock): isolate retry attempt lock ids (#3102)
* fix(lock): isolate retry attempt lock ids

Generate a fresh lock id for each distributed lock retry attempt so late pending cleanup from an earlier attempt cannot release a lock acquired by a later retry.

Also add a regression test covering late-success cleanup versus retried acquisition.

* style(lock): normalize retry race regression test

Normalize the distributed lock retry race regression test formatting without changing behavior.

* test(lock): remove quorum-order assumption

Make the retry race regression test assert the safety property without assuming which delayed client enters the retry quorum first.
2026-05-28 03:56:12 +00:00
GatewayJ 247973f34c fix(lifecycle): make transition worker resize nonblocking (#3090)
Co-authored-by: 季宏伟 <jihongwei@jihongweis-MacBook-Pro.local>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-28 01:36:08 +00:00
安正超 d9c683decc fix: retry namespace lock quorum contention (#3098)
* fix: retry namespace lock quorum contention

* fix(lock): treat remote lock rpc failures as hard

* fix(lock): classify lock contention timeout and avoid hard rollback blocks

* fix(lock): handle namespace lock quorum timeouts

* fix(lock): refine quorum contention handling

* fix(lock): refine unrecoverable quorum handling logs

* fix: address namespace lock quorum review feedback

* test: stabilize batch quorum timing tests

* fix(lock): classify remote quorum failures

* fix: drop remote lock timeout grace period

* fix: satisfy clippy on lock quorum error

* fix: classify remote lock rpc timeouts as hard failures

* fix: fast fail impossible lock quorum attempts

* fix: retry on transient timeout with pending cleanup

* fix: evict remote lock timeout connections

* refactor: avoid duplicate timeout eviction warning

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-28 01:35:00 +00:00
overtrue f77e979e4a fix: satisfy clippy for Snowball traversal test 2026-05-28 09:16:00 +08:00
overtrue 95836a0a4d fix: reject Snowball extract path traversal 2026-05-28 08:32:13 +08:00
Henry Guo 0c52334480 fix(lock): retry transient distributed lock timeouts (#3101)
* fix(lock): retry transient distributed lock timeouts

* fix(lock): avoid retry during pending lock cleanup

* fix(lock): preserve acquire timeout budget

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-27 17:55:46 +00:00
Henry Guo f8e6fc1f10 fix(ecstore): offload erasure encoding from async workers (#3099)
* fix(ecstore): offload erasure encoding from async workers

* fix(ecstore): reuse encode buffers in blocking tasks

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-27 17:54:05 +00:00
houseme 5dfc1b5f07 fix(readiness): gate on lock quorum health (#3100)
* fix(readiness): gate on lock quorum health

Include distributed lock quorum in runtime readiness decisions and expose the signal in health responses.

Map namespace lock quorum failures to ServiceUnavailable instead of InternalError so clients can safely retry while keeping quorum enforcement unchanged.

* fix(ecstore): restore namespace lock error decoding

Add the missing from_u32 arm for NamespaceLockQuorumUnavailable and cover the numeric roundtrip in error tests.
2026-05-27 17:12:52 +00:00
houseme 4648de9e62 chore(deploy): refine systemd and nixos service docs (#3096)
* fix(deploy): simplify systemd service startup

* docs(deploy): add nixos systemd service example

* docs(deploy): add nixos service guide

* chore(deps): update pulsar and pyroscope

* docs(deploy): refine nixos service guidance

* fix(deploy): use explicit rustfs server entrypoint
2026-05-27 14:47:47 +00:00
Derek Ditch 11e97951fd fix(helm): add LoadBalancer service type support (#3049)
The service template only handled ClusterIP and NodePort via if/else-if
branches. When service.type=LoadBalancer was set, no branch matched and
the type field was omitted from the rendered Service, causing Kubernetes
to silently default to ClusterIP.

Add the missing LoadBalancer branch with support for:
- loadBalancerIP: request a specific IP (e.g. Cilium LB-IPAM, MetalLB)
- loadBalancerClass: select a specific LB implementation
- loadBalancerSourceRanges: restrict client source IPs

This is particularly useful for bare-metal / on-prem clusters using
software load balancers (Cilium LB-IPAM, MetalLB, kube-vip) where
a stable routable IP is preferable to an Ingress for S3 clients.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com>
2026-05-27 14:35:09 +00:00
安正超 f53eb4ad44 fix: reject invalid multipart part numbers (#3091)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-27 08:09:59 +00:00
houseme ceffe21f75 feat(tls): add inspect command for TLS layouts (#3092) 2026-05-27 07:23:58 +00:00
houseme 909f0d57fb feat: improve degraded readiness reporting and shutdown handling (#3089)
* fix(server): unify runtime readiness and shutdown flow

* refactor(server): decouple readiness shared types

* refactor(server): keep readiness types crate-private

* refactor(server): await protocol shutdown handles

* fix(server): bypass cached startup readiness

* feat(health): expose degraded readiness reasons

* fix(health): preserve raw IAM readiness in degraded reports
2026-05-27 03:25:28 +00:00
houseme 658b8dea66 fix: unify runtime readiness publication and graceful shutdown flow (#3087)
* fix(server): unify runtime readiness and shutdown flow

* refactor(server): decouple readiness shared types

* refactor(server): keep readiness types crate-private

* refactor(server): await protocol shutdown handles

* fix(server): bypass cached startup readiness
2026-05-26 18:49:42 +00:00
Henry Guo 0478505839 fix(heal): restore single disk data during deep heal (#3085)
* fix(heal): restore single disk data during deep heal

* fix(heal): clarify erasure heal step logging

* fix(heal): avoid duplicate deep object heals

* fix(ecstore): preserve volume-not-found walk errors

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-26 17:36:17 +00:00
安正超 0875f09a39 fix: rebuild wiped disks during admin heal (#3084)
* fix: rebuild wiped disks during admin heal

* Preserve forceStart heal admission semantics

* Address heal regression test review comments

* fix(heal): address admin heal review comments

* fix(heal): fix force-start dedup and test polling

* fix(heal): address unresolved review comments

* fix(heal): simplify dedup key for clippy

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-26 21:54:29 +08:00
weisd ea74fa7a95 fix(heal): rebuild parity shards during repair (#3086)
Object repair rebuilt missing data shards but left missing parity shards unrecreated. A parity-only repair could therefore complete without restoring the shard contents.

Add a repair-specific reconstruction path that regenerates parity after data shards are available, while keeping normal read decoding data-only. Also shut down repair writers after all blocks are written.

Constraint: Preserve read-path decode behavior and limit parity regeneration to repair.

Confidence: high

Scope-risk: moderate

Directive: Do not route read decoding through parity regeneration without measuring the cost.

Tested: cargo test -p rustfs-ecstore decode_data_and_parity -- --nocapture

Tested: cargo test -p rustfs-ecstore heal_reconstructs_missing_parity_shard -- --nocapture

Tested: cargo test -p rustfs-ecstore erasure_coding -- --nocapture

Tested: cargo test -p rustfs heal_object_marks_missing_shard_disk_dirty_for_capacity_manager -- --nocapture

Tested: cargo test -p rustfs-heal -- --nocapture

Tested: cargo clippy -p rustfs-ecstore --all-targets -- -D warnings

Tested: cargo fmt --all --check

Tested: make pre-commit

Not-tested: Live multi-node disk replacement outside local test harness
2026-05-26 09:49:25 +00:00
Henry Guo 62d1f80dbf fix(data-usage): refresh admin usage after object changes (#3081)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-25 14:24:00 +00:00
安正超 0d0a17bb36 fix: include deployment ID in admin info (#3083)
* fix: include deployment ID in admin info

* test(ecstore): avoid mutating global deployment id
2026-05-25 14:23:35 +00:00
houseme 3d2449872a refactor(credentials): derive RPC secret fallback and remove IAM keygen duplication (#3079)
* refactor(credentials): derive rpc secret and remove iam keygen

* fix(credentials): reject default access key RPC secret

* test(credentials): align RPC fallback and add keygen coverage
2026-05-25 11:05:58 +00:00
Demo Macro 5f5b1207e0 fix(ecstore): correct is_truncated logic in ListObjectsV2 pagination (#2997) 2026-05-25 14:59:19 +08:00
Sergei Z. 64feca3550 fix(user): service account expiration handling with RFC3339 (#3078)
fix(user): enhance service account expiration handling with RFC3339 support

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-25 04:27:25 +00:00
houseme f1207a9e28 docs(skills): quote release bump description (#3077)
* docs(skills): quote release bump description

* chore(deps): bump lapin and rumqttc-next
2026-05-24 19:59:29 +00:00
weisd f9475e10dc fix(replication): preserve multipart pending state (#3058)
* Preserve multipart replication recovery state

Multipart uploads previously only scheduled replication after completion, leaving no persisted pending state for scanner recovery if the initial async work was lost. Persist the same pending replication metadata during multipart initialization and let completion evaluate the object metadata that was actually stored.

The scanner heal path also treated ordinary pending objects as delete-replication candidates. Restrict that path to delete markers and version purge state so pending objects remain eligible for object replication heal.

Constraint: Bucket replication recovery depends on persisted object metadata after the async queue is unavailable.
Rejected: Rely only on immediate completion-time scheduling | it cannot recover after process restart or worker loss.
Confidence: high
Scope-risk: moderate
Directive: Keep multipart upload initialization aligned with single PUT replication metadata semantics.
Tested: cargo fmt --all --check
Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings
Tested: LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 make pre-commit
Tested: Runtime replication outage check confirmed multipart xl.meta stores PENDING status and timestamp.

* fix(replication): preserve version purge scanner state

Role-derived replication configs need target-scoped status strings before scanner heal can build per-target purge status. The duplicated replication-status assignment left version purge status unset, so scanner recovery could lose the target-level purge state.

Constraint: Scanner heal derives per-target purge decisions from version_purge_status_internal.
Rejected: Leave the duplicate as a harmless cleanup | it changes recovery behavior for role-only configs with version purge state.
Confidence: high
Scope-risk: narrow
Directive: Keep role-derived replication and version purge internal status mapping symmetric.
Tested: cargo fmt --all --check
Tested: cargo test -p rustfs-ecstore heal -- --nocapture

---------

Co-authored-by: wly <wlywly0735@126.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-24 14:24:00 +00:00
GatewayJ b0646be756 feat(s3select): improve SelectObjectContent streaming (#3072)
* feat(s3select): improve SelectObjectContent streaming

* fix(s3select): reject empty select expressions

* fix(s3select): address streaming review feedback

---------

Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-24 14:23:47 +00:00
houseme d74e6eb042 refactor(tls): centralize runtime foundation (#3065)
* refactor(targets): move notify net helpers from utils

* refactor(tls): centralize runtime foundation

* refactor(targets): move notify net helpers from utils

* refactor(tls): centralize runtime foundation

* feat(tls-runtime): add TLS debug state and admin handler

* refactor(tls-runtime): unify TLS debug consumer status view

* fix(tls): address PR3065 review feedback

* refactor(tls): align debug status payload types

* refactor(targets): harden TLS hot reload paths

* fix(targets): resolve review-4348251652 findings

* fix(targets): finalize tls runtime review follow-ups

* fix(targets): harden tls reload and review follow-ups

* fix(targets): align tls reload handling across targets

* fix(targets): finalize tls reload state and metrics updates

* chore(deps): trim unused TLS deps

* style(targets): normalize TLS reload formatting

* refactor(targets): introduce tls runtime adapter path

* chore: update workspace manifests for tls refactor

* fix(tls): stabilize material reload and audit workflow

* fix(targets): refresh tls fingerprint flow across sinks

* fix(tls): align runtime coordinator and http reader updates

* fix(sftp): simplify protocol error mapping

* fix(tls): harmonize material loading behavior

* fix(server): finalize tls material wiring in startup flow

* fix(protos): tighten tls generation cache and deps
2026-05-24 06:41:15 +00:00
安正超 8be787387c test(utils): cover bracketed IPv6 zone host parsing (#3073) 2026-05-24 03:18:56 +00:00
houseme c9377161e4 chore(deps): update flake.lock (#3074)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/d233902' (2026-05-15)
  → 'github:NixOS/nixpkgs/3d8f0f3' (2026-05-23)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/61ec6a4' (2026-05-16)
  → 'github:oxalica/rust-overlay/d9973e2' (2026-05-23)
2026-05-24 03:18:41 +00:00
Henry Guo 522605a055 test(internode): cover adapter metrics validation (#3071)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-23 15:23:05 +00:00
安正超 22bcfc474e perf(ecstore): use direct std writes for local disk (#3069)
* perf(ecstore): use direct std writes for local disk

* fix(ecstore): avoid blocking disk writes in local disk path

* fix(ecstore): use tokio async write in local disk path

* chore(ecstore): remove unused AsyncWriteExt import

* fix(ecstore): preserve write semantics and avoid bytes copy

* fix(ecstore): optimize write_all_internal buffer paths

* fix(ecstore): sync write path in local disk

* fix(ecstore): align sync flag docs and avoid ref copy

* chore(rust): format local write path

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-23 15:22:49 +00:00
Henry Guo a28cb34381 test(internode): cover RemoteDisk adapter routing (#3070)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-23 09:41:25 +00:00
LeonWang0735 cbcfe625ef fix(replication): avoid skipping existing-object backfill for new targets (#2992)
* fix(replication): avoid skipping existing-object backfill for new targets

* optimize(replication): delete redundant line

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-23 04:53:08 +00:00
安正超 02a7d3c228 fix: derive run script CORS console port (#3068) 2026-05-23 04:52:46 +00:00
Henry Guo 53b608c089 docs(internode): keep transport adapter OSS scoped (#3067)
docs(internode): keep transport adapter oss scoped

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-23 13:00:36 +08:00
Henry Guo 2786a4734a docs(internode): align transport adapter scope (#3064)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-22 16:27:27 +00:00
GatewayJ c9f0f25f55 fix: bind run script to localhost (#3063) 2026-05-22 14:28:17 +00:00
houseme 20a4db7c9a fix(tls): resolve RUSTFS_TLS_PATH startup regression (#3059)
* fix(tls): resolve RUSTFS_TLS_PATH startup regression

* fix(tls): adopt PR review follow-ups
2026-05-22 09:09:31 +00:00
houseme 6264be437c fix(storage): add scoped timeout policy and startup fs guardrail (#3056)
* fix(storage): add RUSTFS_NETWORK_MOUNT_MODE for CIFS/NFS backends

* style: fix cargo fmt formatting in disk_store.rs

* fix(storage): add RUSTFS_NETWORK_MOUNT_MODE for CIFS/NFS backends

Extend the TimeoutHealthAction introduced in #2996 to read_metadata,
list_dir, and disk_info operations when RUSTFS_NETWORK_MOUNT_MODE=true.
Also raises all drive operation timeouts to 60s (explicit per-operation
overrides still take precedence).

Closes #2790

* feat(startup): add unsupported filesystem policy guardrail

* chore(deps): refresh lockfile and dependency pins

* feat(ecstore): add scoped timeout health-action policy

* docs(config): document drive timeout health-action policy

* refactor(ecstore): cache timeout health policy per disk wrapper

* fix(storage): add RUSTFS_NETWORK_MOUNT_MODE for CIFS/NFS backends (#2838)

* fix(storage): add RUSTFS_NETWORK_MOUNT_MODE for CIFS/NFS backends

* style: fix cargo fmt formatting in disk_store.rs

* fix(storage): add RUSTFS_NETWORK_MOUNT_MODE for CIFS/NFS backends

Extend the TimeoutHealthAction introduced in #2996 to read_metadata,
list_dir, and disk_info operations when RUSTFS_NETWORK_MOUNT_MODE=true.
Also raises all drive operation timeouts to 60s (explicit per-operation
overrides still take precedence).

Closes #2790

* fix(utils): map verified Linux filesystem magic values (#3051)

* fix(utils): cover sha256 checksum validation (#3052)

* fix(utils): cover sha256 checksum validation

* docs: clarify sha256 checksum validation

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>

* refactor(config): replace network mount mode with timeout profile preset

* fix(review): align fallback defaults and extend fs-type detection

* fix(review): cache timeout profile and restore probe timeout semantics

* refactor(ecstore): cache timeout health policy lookup

* perf(ecstore): cache active probe timeout per monitor task

---------

Co-authored-by: mistik <mistiklord4@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-22 06:37:30 +00:00
weisd 69345fe059 fix(scanner): preserve background heal compatibility (#3041) 2026-05-22 14:38:35 +08:00
Henry Guo b42766f1d3 feat(internode): define transport capabilities (#3047)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-22 02:56:37 +00:00
安正超 3e3fcbf44d fix(utils): cover sha256 checksum validation (#3052)
* fix(utils): cover sha256 checksum validation

* docs: clarify sha256 checksum validation
2026-05-22 02:45:53 +00:00
安正超 0d94437788 fix(utils): map verified Linux filesystem magic values (#3051) 2026-05-22 02:37:50 +00:00
houseme ed2078e025 fix(ecstore): allow expired delete markers on locked buckets (#3048)
* fix: allow expired delete markers on locked buckets

* fix: reject zero-day del marker expiration
2026-05-22 01:39:53 +00:00
Henry Guo 2404bc1657 docs(internode): inventory transport data paths (#3040)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-21 15:40:39 +00:00
Henry Guo 0985f0b37b feat(internode): label transport operation metrics (#3045)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-21 15:20:39 +00:00
houseme e69e32285b fix(ecstore): harden multipart part metadata visibility (#3042)
* fix(ecstore): harden multipart part metadata visibility

* fix(ecstore): address PR3042 review follow-ups
2026-05-21 14:47:52 +00:00
Henry Guo 97858baf8e docs(internode): analyze buffer lifecycle (#3046) 2026-05-21 22:52:39 +08:00
Henry Guo 69dcf9e6cb fix(tooling): harden internode transport benchmark setup (#3037)
* refactor(config): centralize internode transport constants

* fix(bench): guard all ripgrep calls behind dry-run check

Move require_cmd rg and metrics collection inside the non-dry-run
path so that --dry-run works on hosts without rg installed.

* feat(tooling): cross-platform protoc setup for Linux and macOS

Make install-protoc.sh support Linux (x86_64, aarch64) alongside
macOS, and bump CI protoc from 29.3 to 33.1 to match the version
required by the gproto build script.

* fix(bench): record internode baseline error counts

* fix(skill): correct YAML frontmatter formatting for release-version-bump

* chore(ci): bump protoc version to 34.1

* fix(tooling): bump protoc 33.1 to 34.1 in install script, restore SKILL.md description

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-21 05:47:32 +00:00
houseme 503f89bf0e chore(deps): bump aws sdk and config (#3035) 2026-05-21 01:47:34 +00:00
houseme dcbffb084f chore(deps): refresh workspace deps and linux fs_type gating (#3030)
* chore(deps): refresh workspace deps and linux fs_type gating

- refresh workspace dependency pins and lockfile updates

- remove now-unused crate dependency entries in multiple Cargo.toml files

- enable profiling export defaults in config and scripts/run.sh

- gate os::fs_type module/function/tests to Linux to avoid non-Linux dead_code warnings

* fix(utils): simplify fs_type linux gating

- keep fs_type module-level linux cfg in os::mod

- remove redundant linux cfg on get_fs_type and test module

* chore(deps): bump s3s git revision

- update workspace s3s dependency to rev 507e1312b211c3ddc214b03875d6fabd15d22ed5

- refresh Cargo.lock source entry for s3s

* chore(dev): allow mysql_async git source and env overrides

- allow mysql_async git source in deny.toml allow-git list

- make scripts/run.sh core env vars overrideable via existing shell env

* fix(utils): import get_fs_type in fs_type tests

- add explicit super::get_fs_type import in fs_type test module

- fix Linux E0425 unresolved function errors in unit tests

* chore(dev): tune run script observability defaults

- make profiling export env overrideable in scripts/run.sh

- set RUSTFS_OBS_SAMPLE_RATIO default from 2.0 to 1.0

- update allow-git review window comments in deny.toml

* test(obs): stabilize profiling env alias tests
2026-05-20 15:03:07 +00:00
houseme b2dfdf85de chore(release): prepare 1.0.0-beta.4 (#3032)
* chore(release): prepare 1.0.0-beta.4

* docs(skill): refine rustfs spec changelog rule

* docs(skill): optimize rustfs release bump workflow
2026-05-20 13:56:43 +00:00
houseme 375482a4b6 refactor: converge storage io hot paths (#3029)
* refactor(issue-633): clarify layered io control policies

* refactor(issue-633): consolidate timeout and deadlock layers

* refactor(issue-633): align storage backpressure metadata

* refactor(issue-633): unify storage backpressure transitions

* refactor(issue-633): simplify watermark transition API

* test(issue-633): add storage backpressure transition test

* refactor(issue-633): align storage pipe meta shape

* refactor(issue-633): enrich storage monitor metadata

* refactor(issue-633): finalize storage backpressure convergence

* refactor(issue-633): complete scheduler layer convergence

* refactor(issue-633): reduce concurrency facade config duplication

* refactor(issue-633): migrate storage callsites to final policy names

* chore(issue-633): apply final pre-commit normalization

* refactor(issue-633): unify timeout wrapper dynamic size path

* refactor(issue-633): make concurrency policies copyable

* refactor(issue-633): converge storage io hot paths

* fix(issue-633): honor storage timeout min bound

* fix(storage): avoid timeout calc panic on huge sizes

* refactor(storage): consolidate timeout checks and test attrs

* fix(storage): harden io scheduler core config mapping

* refactor(storage): eliminate patch-on-patch patterns and dead code

- Remove trivial accessor methods on ConcurrencyConfig that just return pub fields
- Remove dead BackpressureEvent/BackpressureEventType types from concurrency crate
- Fix io_schedule test using wrong constructor (from_core_config -> from_scheduler_config)
- Update manager.rs to use config fields directly instead of removed accessors

* fix: adopt review feedback for config guards

* test: remove needless struct update defaults

* fix: harden timeout policy and preserve api alias
2026-05-20 12:00:23 +00:00
Henry Guo 45b04316b9 refactor(ecstore): decouple transport construction from RemoteDisk (#3027)
Move build_internode_data_transport_from_env() out of RemoteDisk::new()
into new_disk(), so RemoteDisk depends only on the
Arc<dyn InternodeDataTransport> trait object. TCP/HTTP remains the
default backend; all data paths unchanged.

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-20 11:55:08 +00:00
Henry Guo 19b69abe5c feat(internode): harden p0 transport boundary and baseline tooling (#3017)
* feat(internode): p0 transport baseline and ci hardening

* fix(internode): avoid double wrapping transport errors

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-20 11:06:26 +00:00
安正超 c684438625 fix(obs): add proxied PUT replication metrics (#3020)
fix(obs): add proxied put replication metrics
2026-05-20 05:30:57 +00:00
安正超 11054263c1 fix(utils): map common Linux filesystem magic values (#3023) 2026-05-20 04:00:32 +00:00
安正超 b530a9f0b2 fix(ecstore): remove stale bucket metadata parse TODO (#3021)
* fix(ecstore): remove stale bucket metadata parse TODO

* test: cover stored bucket target config parsing
2026-05-20 03:59:55 +00:00
安正超 dd35538e90 fix(ecstore): remove stale disk TODOs (#3022) 2026-05-20 03:59:34 +00:00
安正超 c727589161 fix(obs): remove stale replication metric TODOs (#3024) 2026-05-20 03:59:16 +00:00
安正超 cde53ca6ad fix(utils): handle IPv6 zones and hex ranges (#3019)
* fix(utils): handle low-risk TODO range parsing

* fix(utils): address scoped IPv6 and range review
2026-05-20 03:56:14 +00:00
Henry Guo e929814edc feat(ecstore): add internode transport boundary and TCP baseline runner (#3010)
* docs: add internode data transport RFC

* feat: add internode operation metrics

* fix feedback

* fix(ci): fallback protoc token to github.token

* feat(ecstore): add internode transport boundary and baseline runner

* feat(internode): harden data transport baseline

* Revert "feat(internode): harden data transport baseline"

This reverts commit 5b8d6b8aa4.

* fix(internode): address baseline review comments

* fix(ci): pin setup-protoc to stable release

* fix(ci): install protoc via apt on linux

* fix(ci): restore protoc install for macos and windows

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-19 22:41:34 +00:00
安正超 a8a5f25af3 perf(ecstore): remove owned write sync regression (#3018) 2026-05-19 22:14:35 +00:00
安正超 54be3cab23 fix(heal): ignore missing response subscribers (#3015)
* fix(heal): ignore missing response subscribers

* fix(heal): ignore missing response subscribers
2026-05-19 15:34:22 +00:00
安正超 1c8fdfddf4 chore(ci): pin RustFS setup-protoc release (#3016)
* chore(ci): use RustFS setup-protoc fork

* chore(ci): pin RustFS setup-protoc release
2026-05-19 15:28:51 +00:00
houseme 73bde843d6 refactor(s3): consolidate semantic boundaries and remove s3-common (#3012)
* refactor(common): introduce rustfs-data-usage core crate

* refactor(concurrency): migrate workers crate into concurrency

* refactor(crypto): migrate appauth token APIs into crypto

* fix docs urls

* remove unused crate

* refactor(data-usage): switch consumers to rustfs-data-usage

* chore(fmt): apply cargo fmt and lockfile sync

* refactor(common): remove data_usage compatibility re-export

* refactor(capacity): move capacity_scope to object-capacity

* refactor(io-metrics): relocate internode metrics from common

* refactor(common): decouple scanner report from madmin

* chore(fmt): normalize import ordering after pre-commit

* refactor(s3): split s3 types and ops crates

* refactor(s3): centralize event version and safe parsing

* refactor(s3): add op-event compatibility guardrails

* refactor(s3): add runtime op-event mismatch observability

* refactor(s3): extract delete event mapping helper

* refactor(s3): extract put event mapping helper

* refactor(s3): consolidate remaining event semantic helpers

* refactor(s3): add op-event coverage checks and observability alerts

* refactor(s3-ops): consolidate op-event semantic mapping

* refactor(scanner): remove last_minute wrapper module

* refactor(scanner): consolidate duplicated data usage models
2026-05-19 12:50:25 +00:00
houseme d6fc70eb12 feat(obs): expose key S3 usecase spans at info (#3013) 2026-05-19 11:00:51 +00:00
houseme 25c6bdf490 perf(filemeta): phase-1~3 rename_data metadata optimization (#3011)
* chore(perf): harden amd64 profiling benchmark flow

* fix(profiling): isolate bench buckets and map protobuf conflict

* perf: avoid blocking owned local writes

* style: format profile admin handler

* docs: clarify observability trace validation

* perf: reduce mkdir overhead on local writes

* perf: add rename_data meta microbenchmark

* perf(filemeta): fast-path data_dir decode in version meta

* perf(filemeta): collapse data-dir lookup into one scan

* perf(filemeta): reduce scan allocs and refresh meta bench

* perf(ecstore): skip mkdir path on read-only open

* perf(filemeta): single-pass unshared data-dir scan

* perf(filemeta): add two-key inline remove fast path

* perf(filemeta): compare remove-two keys by bytes first

* bench(ecstore): add remove_two-only micro benchmark

* bench(ecstore): stabilize rename_data meta benchmark timing

* bench(ecstore): align rename_data path with remove_two

* perf(filemeta): avoid uuid string alloc in remove_two

* perf(filemeta): add fast-path for empty inline data

* perf(filemeta): streamline add_version match branch

* perf(filemeta): fast-return remove_key on miss

* perf(filemeta): speed up add_version insertion lookup

* style(ecstore): normalize formatting in perf-tuning files

* refactor(filemeta): unify inline data removal paths
2026-05-19 10:20:24 +00:00
Henry Guo f695870626 feat(internode): add transport observability (#3007)
* docs: add internode data transport RFC

* feat: add internode operation metrics

* fix feedback

* fix(ci): fallback protoc token to github.token

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-19 07:16:05 +00:00
安正超 aa3f13c0d3 fix(ecstore): stop listing after reaching result limit (#3001)
* fix(ecstore): stop listing after reaching result limit

* fix(ecstore): ignore intentional list cancellation

* fix(ecstore): satisfy list cancellation clippy lint

* fix(ecstore): include entry before limit early return

* fix(ecstore): cancel on limit in gather_results

* fix(ecstore): sync timeout branch with reviewed patch

* fix(ecstore): satisfy cancellation clippy lint

---------

Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-19 05:20:21 +00:00
houseme c4248078d6 chore: update dependencies (#2890)
* chore: update dependencies

* build(deps): bump the dependencies group with 5 updates

* build(deps): bump the dependencies group with 6 updates (#2908)

Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>

* fix(ecstore): narrow Windows URL drive path rewrite

* chore(deps): bump starshard to 1.2.0

* revert(ecstore): restore endpoint Windows path behavior

* up

* up

* up

* perf(notify): use cached snapshot mode for scans

* fix

* chore(deps): bump workspace dependency versions

* chore(deps): refresh pinned dependency references

* chore(ci): align profiling and decommission tooling updates

- enable tokio_unstable cfg in performance profiling build flags\n- bump Rust base image from 1.93 to 1.95 in source and decommission Dockerfiles\n- remove obsolete compose version key from docker-compose-simple.yml\n- add standard Apache-2.0 license header to docker-compose.decommission.yml

* chore(deps): bump the dependencies group across 1 directory with 7 updates (#2994)

Bumps the dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [sysinfo](https://github.com/GuillaumeGomez/sysinfo) | `0.39.1` | `0.39.2` |
| [dial9-tokio-telemetry](https://github.com/dial9-rs/dial9-tokio-telemetry) | `0.3.9` | `0.3.10` |
| [opentelemetry](https://github.com/open-telemetry/opentelemetry-rust) | `0.31.0` | `0.32.0` |
| [opentelemetry-otlp](https://github.com/open-telemetry/opentelemetry-rust) | `0.31.1` | `0.32.0` |
| [opentelemetry_sdk](https://github.com/open-telemetry/opentelemetry-rust) | `0.31.0` | `0.32.0` |
| [opentelemetry-semantic-conventions](https://github.com/open-telemetry/opentelemetry-rust) | `0.31.0` | `0.32.0` |
| [opentelemetry-stdout](https://github.com/open-telemetry/opentelemetry-rust) | `0.31.0` | `0.32.0` |



Updates `sysinfo` from 0.39.1 to 0.39.2
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.39.1...v0.39.2)

Updates `dial9-tokio-telemetry` from 0.3.9 to 0.3.10
- [Release notes](https://github.com/dial9-rs/dial9-tokio-telemetry/releases)
- [Changelog](https://github.com/dial9-rs/dial9/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dial9-rs/dial9-tokio-telemetry/compare/dial9-tokio-telemetry-v0.3.9...dial9-tokio-telemetry-v0.3.10)

Updates `opentelemetry` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-rust/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-rust/blob/main/docs/release_0.32.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-rust/compare/opentelemetry-prometheus-0.31.0...opentelemetry-0.32.0)

Updates `opentelemetry-otlp` from 0.31.1 to 0.32.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-rust/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-rust/blob/main/docs/release_0.32.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-rust/compare/opentelemetry-otlp-0.31.1...opentelemetry-otlp-0.32.0)

Updates `opentelemetry_sdk` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-rust/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-rust/blob/main/docs/release_0.32.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.31.0...opentelemetry_sdk-0.32.0)

Updates `opentelemetry-semantic-conventions` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-rust/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-rust/blob/main/docs/release_0.32.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.31.0...opentelemetry-semantic-conventions-0.32.0)

Updates `opentelemetry-stdout` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-rust/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-rust/blob/main/docs/release_0.32.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.31.0...opentelemetry-stdout-0.32.0)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-version: 0.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: dial9-tokio-telemetry
  dependency-version: 0.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: opentelemetry
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: opentelemetry-otlp
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: opentelemetry_sdk
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: opentelemetry-semantic-conventions
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: opentelemetry-stdout
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>

* chore(deps): bump workspace dependency versions

* chore(deps): refresh lockfile windows crate graph

* chore(bench): align snapshot mode labels with coverage

* chore(bench): clarify tested snapshot modes

* fix(review): address PR2890 Copilot comments

---------

Signed-off-by: houseme <housemecn@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 04:58:42 +00:00
Henry Guo 94da6e34cb fix(notify): parse IPv6 hosts without ports (#3000)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
2026-05-19 02:40:21 +00:00
安正超 a9e62dc2c2 perf: avoid blocking hop for owned disk writes (#3004)
* perf: avoid blocking hop for owned disk writes

* fix(ecstore): sync owned write path
2026-05-19 02:27:14 +00:00
yihong ecb6704679 fix: make help color not right (#3005)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2026-05-19 02:26:23 +00:00
安正超 e65aebba53 fix(ecstore): guard transition worker max (#3003)
* fix(ecstore): guard transition worker max

* test: clarify env mutation safety

* test(ecstore): document env mutation concurrency contract
2026-05-19 02:25:51 +00:00
weisd a66337bd28 fix: keep scanner walk timeouts from offlining drives (#2996)
* fix: keep scanner walk timeouts from offlining drives

Scanner walk operations can time out on large or slow directory listings without proving the backing drive is faulty. Keep the timeout error local to the scan while preserving failure marking for ordinary disk operations.

Constraint: Scanner walk_dir can include listing work that exceeds the drive timeout under slow storage.

Rejected: Disable timeout failure marking globally | real stuck disk operations must still affect drive health

Confidence: high

Scope-risk: narrow

Directive: Do not route scanner/listing timeout back into drive offline state without reproducing issue #2651

Tested: cargo test -p rustfs-ecstore timeout

Tested: cargo fmt --all --check

Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings

Tested: make pre-commit with en_US.UTF-8 locale

Related: https://github.com/rustfs/rustfs/issues/2651

* fix: keep remote scanner walks from offlining drives

Remote walk_dir uses a streaming request that can hit total or stall timeouts during large scans without proving the remote drive is faulty. Route that scanner path through an explicit ignore action while preserving failure marking for ordinary remote operations.

Also treat Duration::ZERO as no operation timeout for remote health tracking, matching the existing local disk wrapper contract while still marking network-like errors on default paths.

Constraint: Scanner walk_dir streams can be slow because of directory size or consumer backpressure.

Rejected: Disable remote timeout failure marking globally | normal remote disk operations still need to evict failed connections and update drive health

Confidence: high

Scope-risk: narrow

Directive: Do not reintroduce remote scanner timeout failure marking without reproducing issue #2651 against remote disks.

Tested: cargo test -p rustfs-ecstore execute_with_timeout

Tested: cargo test -p rustfs-ecstore timeout

Tested: cargo fmt --all --check

Tested: cargo clippy --workspace --all-features --all-targets -- -D warnings

Tested: make pre-commit with en_US.UTF-8 locale

Related: https://github.com/rustfs/rustfs/issues/2651

* test: prove scanner walk backpressure keeps drives online

Add a local scanner walk regression test where the output writer never makes progress. The test confirms the walk timeout returns without marking the drive faulty, covering a stall source that is not caused by object count or network transfer speed.

Constraint: Scanner walk can block on downstream writer backpressure as well as disk or network IO.\nConfidence: high\nScope-risk: narrow\nTested: cargo test -p rustfs-ecstore walk_dir_writer_backpressure_timeout_does_not_mark_drive_failure\nTested: cargo test -p rustfs-ecstore timeout\nTested: cargo fmt --all --check\nTested: cargo clippy --workspace --all-features --all-targets -- -D warnings\nTested: make pre-commit

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-05-18 15:04:05 +00:00
houseme bd1e57293f fix: harden lifecycle transition compensation and regression coverage (#2995)
* fix(ecstore): honor transition worker configuration

* fix(ecstore): add transition queue backpressure metrics

* fix(ecstore): schedule transition compensation on enqueue pressure

* fix(ecstore): log transition compensation scheduling

* test(rustfs): add transition compensation fault-injection coverage

* test(rustfs): cover delete after transition compensation

* test(scanner): cover cleanup after transition compensation

* test(rustfs): extend compensation transition coverage

* test(scanner): cover backfill idempotency after compensation

* test(scanner): cover noncurrent expiry after compensation

* test(rustfs): cover versioned delete after compensation

* test(rustfs): cover delete marker lifecycle after compensation

* test(scanner): extend versioned lifecycle compensation coverage

* test(scanner): model versioned delete after compensation

* test(scanner): clarify modeled versioned delete helper

* refactor(ecstore): optimize transition enqueue hot path

* refactor(ecstore): centralize transition runtime constants

* style(ecstore): apply rustfmt for transition timeout helper

* fix(ilm): align queue-full metric semantics

* refactor(ecstore): unify immediate enqueue failure handling

* refactor(ecstore): reuse transition worker env constant

* ci(actions): update setup action inputs
2026-05-18 12:50:43 +00:00
wood 0e888cfef2 fix(ecstore): list_object_v2 error when scanning multipart folder (#2946)
* fix(ecstore): list_object_v2 error when scanning multipart directory with prefix

Signed-off-by: w0od <dingboning02@163.com>

* test(ecstore): cover prefix dir scan with multipart folder support

Signed-off-by: w0od <dingboning02@163.com>

* test(ecstore): harden test shape to improve regression detection

Signed-off-by: w0od <dingboning02@163.com>

* refactor(ecstore): move multipart dir filter into recursion to reduce I/O

Signed-off-by: w0od <dingboning02@163.com>

* test(ecstore): replace scan_dir test with walk_dir integration coverage

Signed-off-by: w0od <dingboning02@163.com>

* refactor(ecstore): remove unnecessary .clone() calls

Signed-off-by: w0od <dingboning02@163.com>

---------

Signed-off-by: w0od <dingboning02@163.com>
2026-05-18 11:01:20 +00:00
安正超 4c9fd789ea docs: update security advisory skill lessons (#2991) 2026-05-18 12:34:27 +08:00
Henry Guo 17ae9f34c7 fix(notify): accept case-insensitive filter rule names (#2990) 2026-05-18 03:41:48 +00:00
houseme cdfe83877b chore(deps): update flake.lock (#2986)
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/b3da656' (2026-05-08)
  → 'github:NixOS/nixpkgs/d233902' (2026-05-15)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/1d634a9' (2026-05-09)
  → 'github:oxalica/rust-overlay/61ec6a4' (2026-05-16)
2026-05-17 03:09:08 +00:00
安正超 be37fd285e ci(nix): avoid requesting review from PR author (#2987) 2026-05-17 11:13:45 +08:00
安正超 095337100e test(policy): validate default policies (#2985) 2026-05-17 01:41:03 +00:00
GatewayJ e0729f5f4d fix(policy): align action-family validation and defaults (#2984)
* fix(policy): align action-family validation and defaults

* test(e2e): add accountinfo service-account roundtrip

* test(policy): add mixed action family cases
2026-05-16 11:19:04 +00:00
houseme 6e12289339 fix(runtime): finalize issue 2941 profiling cleanup (#2983)
* perf(runtime): narrow profiling support and upgrade starshard

* style(notify): normalize starshard imports

* perf(ecstore): reduce list_path_raw coordination overhead

* docs(scripts): add issue 2941 perf capture workflow

* fix(runtime): finalize issue 2941 profiling cleanup

* build(deps): bump quick-xml to 0.40.0

* chore(scripts): untrack local perf capture guide

* fix(scripts): honor label in perf capture output
2026-05-16 11:09:04 +00:00
weisd 9dcf8cb7ea fix: stabilize rebalance start and listing (#2961)
* fix: unblock empty chunked admin rebalance start

Some admin clients send empty POST requests with chunked framing and no explicit content length. The admin rebalance route carries no request body, so normalize that known route before downstream validation sees a missing length.

Constraint: Keep normalization scoped to known empty-body admin routes

Rejected: Relax transfer-encoding handling for S3 routes | broader protocol risk

Confidence: high

Scope-risk: narrow

Tested: cargo test -p rustfs --lib server::layer::tests::

Tested: cargo fmt --all --check

Tested: git diff --check

* fix: keep rebalance listing from stalling

Large object migration was awaited inside the listing callback, so metacache readers could stop being drained while drive walk operations were still writing listing data. Move entry migration into bounded background tasks and wait for them after each set listing completes.

Constraint: Preserve existing rebalance cancellation and first-error propagation

Rejected: Only increase drive walk timeouts | masks callback backpressure and still fails for slower migrations

Confidence: medium

Scope-risk: moderate

Tested: cargo test -p rustfs-ecstore rebalance_unit_tests

Tested: cargo test -p rustfs-ecstore cache_value::metacache_set::tests

Tested: cargo test -p rustfs --lib server::layer::tests::

Tested: cargo clippy -p rustfs-ecstore --lib --all-features -- -D warnings

Tested: cargo clippy -p rustfs-ecstore --tests --all-features -- -D warnings

Tested: cargo fmt --all --check

Tested: git diff --check

---------

Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-16 05:17:01 +00:00
安正超 6898e720dd fix(security): harden proxy auth and default credentials (#2981)
* fix(security): harden proxy auth and default credentials

* fix(security): address proxy and credential feedback
2026-05-16 04:01:50 +00:00
yihong 824c4f7673 docs: fix some dead links (#2975)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-16 10:06:53 +08:00
安正超 c0c92cb048 ci(build): honor console asset download fallback (#2980) 2026-05-16 10:06:23 +08:00
Henry Guo bca8b08c2b fix: handle Windows paths in pre-commit tests (#2974)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-15 14:04:51 +00:00
Henry Guo 738fb86611 fix(admin): surface access key policy errors (#2970)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-15 08:26:38 +00:00
安正超 092c6bc135 ci(build): pin macOS x86 release runner (#2971) 2026-05-15 11:56:13 +08:00
escapecode 5cda460451 fix(sftp): preserve OPEN-time client attrs as object metadata (#2913)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-15 00:32:06 +00:00
GatewayJ fc8322ed64 bucket policy notify & pba (#2968) 2026-05-14 14:31:40 +00:00
houseme cd2cd74314 ci: force Node24 in Nix workflows with pinned actions (#2966) 2026-05-14 11:13:42 +00:00
Henry Guo ced390b02b test(ecstore): cover managed sse range reads (#2962)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-14 06:56:21 +00:00
Henry Guo 11be278c40 test(notify): cover prefix suffix event dispatch (#2960) 2026-05-14 05:38:38 +00:00
Henry Guo 0893c05540 fix(ecstore): use hex sha256 for delete objects (#2958)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-14 05:34:26 +00:00
houseme 81754d80b3 refactor(targets): unify queue/connectivity handling and coverage (#2953)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: marshawcoco <marshawcoco@gmail.com>
2026-05-14 04:31:23 +00:00
houseme bdb98598d2 chore(release): prepare 1.0.0-beta.3 (#2957) 2026-05-14 04:27:26 +00:00
安正超 5fccfceb91 test(ecstore): cover walk listing error success paths (#2954) 2026-05-14 01:03:31 +00:00
Henry Guo c1bcee327c fix(notify): keep live listen events active when disabled (#2952)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-13 17:10:14 +00:00
Henry Guo d4d07095f8 fix(ecstore): propagate walk listing errors (#2949)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-13 16:59:49 +00:00
Sergei Z. 8c49671161 Fix #2775 recursive list handling in LocalDisk::scan_dir() (#2923)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-13 14:36:23 +00:00
cxymds c65d4071a3 fix(ecstore): map missing metadata to not found (#2944)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-13 13:03:15 +00:00
weisd 4ea805cbc0 fix: preserve pagination when max keys exceed limit (#2943)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-13 12:48:38 +00:00
Henry Guo 26341742e0 fix(ecstore): fail listing on walk_dir producer errors (#2937) 2026-05-13 11:08:05 +00:00
安正超 fd3fb77ed5 fix(sftp): avoid metadata on multipart copy (#2935)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-13 07:44:09 +00:00
weisd c5125b20bd fix(storage): keep storage info RPC map encoded (#2942)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-13 06:19:43 +00:00
weisd 9b83de53e1 fix(ecstore): surface prefix listing storage errors (#2940) 2026-05-13 04:24:54 +00:00
houseme d72b8cb88e fix(server): fail fast when configured TLS parsing fails (#2939) 2026-05-13 04:16:39 +00:00
Henry Guo 17eff5a97b fix(tls): ignore Kubernetes secret projection dirs (#2938) 2026-05-13 03:45:42 +00:00
houseme 129cb0f920 fix: make HeadObject consistent after write completion (#2936) 2026-05-13 03:27:38 +00:00
安正超 28c9358482 test(notify): cover encoded key target union (#2934)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-12 15:20:06 +00:00
Henry Guo fe5690ca70 fix(sftp): preserve open attrs metadata (#2929)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-12 13:34:52 +00:00
安正超 25cf164461 test(protocols): cover SFTP host key reload failure (#2928)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-12 10:40:23 +00:00
安正超 8a501846f4 test(iam): cover mixed STS claim policy names (#2932) 2026-05-12 10:39:53 +00:00
GatewayJ b2ba2e5bb3 iam: handle sts claim policy names (#2902)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-12 07:10:42 +00:00
安正超 8169bd63e9 test(protocols): cover TLS reload fingerprint ordering (#2927) 2026-05-12 02:50:49 +00:00
houseme 6185d38911 fix(protocols): add hot reload for WebDAV FTPS and SFTP (#2922)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 16:25:20 +00:00
cxymds 8ef2c0a5e1 fix(storage): sync transition tier config across peers (#2918)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-11 15:23:29 +00:00
Henry Guo d6813b53a2 fix(ecstore): preserve list marker set index (#2919)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-11 15:20:43 +00:00
Henry Guo 5ca99a8e32 fix(ecstore): fail listing on stalled reader (#2920)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-11 15:19:46 +00:00
Henry Guo 07a01a1123 fix(notify): match filters against decoded event keys (#2921)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-11 15:01:37 +00:00
Henry Guo 941986b331 test(e2e): gate protocol runner by requested features (#2912) 2026-05-11 13:59:24 +00:00
Henry Guo aba65a448c fix(protocols): encode storage client request URIs (#2911) 2026-05-11 10:47:30 +00:00
Henry Guo 1c94f7a066 fix(sftp): classify backend errors by type (#2909) 2026-05-11 02:56:08 +00:00
安正超 77c21dc5fd test(obs): cover replication bandwidth tombstones (#2906) 2026-05-11 01:55:54 +00:00
安正超 404fc816e9 test(targets): cover MySQL probe validation (#2907) 2026-05-11 01:55:48 +00:00
LeonWang0735 61a7820651 optimize(obs):zero and expire removed replication bandwidth series (#2901)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-10 14:26:18 +00:00
安正超 0375dd39bb fix(targets): handle MySQL DSN scheme case (#2903)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-10 14:25:22 +00:00
安正超 f84d8983ed test(s3): promote passing SSE multipart cases (#2900) 2026-05-10 13:16:23 +00:00
JaySon bafff6d207 feat(targets): add check_mysql_server_available probe function (#2884)
Signed-off-by: JaySon-Huang <tshent@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-10 11:53:45 +00:00
houseme 53ec1b95d8 keep sftp e2e tests buildable (#2897) 2026-05-10 11:52:15 +00:00
安正超 65c62ab2ea test(sftp): cover init session activity stamp (#2898) 2026-05-10 09:57:04 +00:00
安正超 c419cff348 test(sftp): cover init negotiation and platform gating (#2896) 2026-05-10 05:59:10 +00:00
houseme 5a5ffcfac0 chore(deps): update flake.lock (#2894) 2026-05-10 03:48:57 +00:00
escapecode 96b293bf8a feat(sftp): add SFTPv3 protocol support (#2875)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-10 03:48:42 +00:00
houseme 8892cbbdd7 feat: enhance WebDAV support with features and directory operations (#2856) (#2892)
Signed-off-by: giter <giter@users.noreply.github.com>
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: giter <giter@users.noreply.github.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Lijiajie <lijiajie@ffcode.net>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-05-09 16:45:09 +00:00
唐小鸭 09c2d15057 fix(sse): Temporarily refactored the SSE design for ECStore (#2813)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-09 14:06:35 +00:00
Sergei Z. 6275918d92 fix: empty-body requests without content length (#2888)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 14:05:53 +00:00
Henry Guo 5879d0b59d fix(server): handle public health before s3 host parsing (#2866)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-09 13:15:37 +00:00
JaySon cb54cecf8c docs(targets): sync AGENTS.md and test doc comments with code (#2881)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-09 03:06:11 +00:00
安正超 9288f1ed3a fix(targets): handle postgres dsn redaction scheme case (#2886) 2026-05-09 01:58:23 +00:00
安正超 8182e2dd38 test(ecstore): cover empty runtime listing candidates (#2889) 2026-05-09 01:57:54 +00:00
houseme 81ad48dac2 feat(targets): add AMQP support for notify and audit (#2879)
Co-authored-by: Hyesook Yun <74169420+suk13574@users.noreply.github.com>
2026-05-09 01:56:26 +00:00
安正超 1582f216fe test(ecstore): cover offline capacity snapshots (#2880) 2026-05-08 16:33:34 +00:00
安正超 5081b8396d test(ecstore): cover system path failure classifier (#2874) 2026-05-08 14:51:11 +00:00
houseme c90bfe2b23 fix(ecstore): harden runtime read-path quorum handling (#2872) 2026-05-08 09:56:39 +00:00
Henry Guo 03045ff2e6 fix(iam): keep error state on initial load failure (#2846)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-08 08:26:01 +00:00
安正超 61bd5698bb test(admin): cover pools list response serialization (#2862)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-08 07:32:49 +00:00
安正超 44d2b6a284 [codex] docs: ban rust-refactor-helper skill (#2869) 2026-05-08 15:33:45 +08:00
安正超 fe978488a0 test(admin): cover pool used-size saturation (#2863) 2026-05-08 06:06:54 +00:00
安正超 e209fc6eef test(ecstore): cover store init health reset delegation (#2865) 2026-05-08 05:42:05 +00:00
weisd 97a2775434 fix(ecstore): reset drive health between store init format retries (#2848)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-08 03:16:58 +00:00
cxymds 9d85b7d23a feat: enrich admin pools list response (#2853)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-07 13:53:25 +00:00
安正超 ba7ee5fbdd docs(security): make advisory skill lesson first (#2860)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-07 21:53:26 +08:00
安正超 6896b38dc2 test(s3): promote lifecycle expiration header tests (#2858) 2026-05-07 13:08:42 +00:00
安正超 9df57b8cd8 test(targets): cover Redis env config loading (#2857) 2026-05-07 13:08:11 +00:00
安正超 6ec3a4c4d5 docs(security): refresh advisory lesson states (#2859) 2026-05-07 21:21:15 +08:00
houseme fd37a7d01e fix(targets): probe webhook health by host port (#2854) 2026-05-07 11:52:45 +00:00
Henry Guo 9c0141fbdf docs(io-metrics): fix misleading metrics links (#2849) 2026-05-07 20:00:19 +08:00
cxymds 2a0fbb8d77 fix(ecstore): repair decommission pool quorum (#2847)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-07 11:14:11 +00:00
houseme 5431b9273d feat(targets): complete redis mysql postgres target wiring (#2842)
Signed-off-by: jaehanbyun <awbrg789@naver.com>
Signed-off-by: houseme <housemecn@gmail.com>
Signed-off-by: Gunther Xing <jiengup@gmail.com>
Signed-off-by: JaySon-Huang <tshent@qq.com>
Co-authored-by: jaehanbyun <awbrg789@naver.com>
Co-authored-by: Gunther Xing <jiengup@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: JaySon <tshent@qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-07 10:00:59 +00:00
安正超 b159d656cc test(admin): cover POST content length compat layer (#2844)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-07 09:51:09 +00:00
cxymds b7815b0694 fix(ecstore): remove startup order sensitivity (#2850) 2026-05-07 09:07:25 +00:00
houseme eaa5ff3053 bump workspace versions and replace cfg-if in crypto (#2851) 2026-05-07 09:04:27 +00:00
houseme f1cd7c1345 feat(rustfs): add ftps/webdav defaults to info output (#2845) 2026-05-07 04:39:56 +00:00
cxymds 06097a3c33 fix(admin): normalize empty admin POST content length (#2843) 2026-05-07 02:52:21 +00:00
安正超 5e0ca006f0 test(replication): cover ETag comparison edge cases (#2840) 2026-05-07 02:48:12 +00:00
安正超 abc07a9dc4 fix(build): quote build script features argument (#2841) 2026-05-07 02:47:53 +00:00
Nikita Bakun 4d6171e996 fix(replication): handle version ID format mismatch with AWS S3 (#2829)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-06 21:30:07 +00:00
GatewayJ 3130670157 test(object-lock): cover default retention delete marker (#2836) 2026-05-06 21:14:00 +00:00
安正超 96d41b6349 test(helm): cover standalone scale-to-zero rendering (#2831) 2026-05-06 21:13:40 +00:00
安正超 9b9e0db9f6 test(lifecycle): cover ILM process time aliases (#2839) 2026-05-06 21:12:22 +00:00
安正超 68fcbffcb6 test(build): cover build script feature flags (#2837) 2026-05-07 05:18:51 +08:00
giter 04712fb6c6 feat: add features option to build script (#2834)
Signed-off-by: giter <giter@users.noreply.github.com>
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-06 15:09:44 +00:00
安正超 4728abcff1 fix(security): document unsafe and TLS overrides (#2835) 2026-05-06 15:09:02 +00:00
cxymds 70be0804ee fix: 2827 lifecycle days next midnight (#2833)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-06 15:00:53 +00:00
安正超 b10db403b6 test(s3): promote passing copy metadata case (#2832) 2026-05-06 13:58:32 +00:00
GatewayJ 9e93d3f47a fix(object-lock): materialize default retention metadata (#2824)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-06 13:48:12 +00:00
cxymds 9f07029373 fix: reload bucket metadata after lifecycle updates (#2822)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-06 13:01:44 +00:00
Henry Guo 4b36667ba1 fix(policy): avoid logging generated access keys (#2826)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-06 12:56:27 +00:00
houseme 41ba34a145 fix(rpc): add issue 2815 regression and docker validation (#2828) 2026-05-06 12:22:13 +00:00
Michael Graff 3898d524fe security: same-origin console CORS, fail-closed helm creds, deny.toml, sample-config hardening (#2769)
Signed-off-by: Michael Graff <explorer@flame.org>
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-06 07:34:44 +00:00
Duru Can Celasun 718bec7722 feat(helm-chart): support scale to 0 in standalone mode (#2797)
Signed-off-by: Duru Can Celasun <can@dcc.im>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-06 06:42:56 +00:00
安正超 50ddec3ffc fix: preserve data on self metadata copy (#2819)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-06 06:28:36 +00:00
Henry Guo 7692c0c3bd test(credentials): avoid printing default secret (#2820)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-06 05:19:42 +00:00
houseme 3dd0692917 refactor: unify credential env constants and deploy env usage (#2821)
Co-authored-by: Henry Guo <marshawcoco@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-06 04:30:29 +00:00
GatewayJ 090d60e00a fix(auth): authorize DeleteObjects per key (#2814) 2026-05-06 00:47:36 +00:00
安正超 c228fabfdb test(admin): cover orphan resync status cleanup (#2816) 2026-05-06 00:47:11 +00:00
安正超 bde569d055 test(getobject): cover buffer threshold edge cases (#2817) 2026-05-06 00:47:02 +00:00
houseme e19b0dd997 fix(notify): improve webhook target diagnostics for env setup (#2810) 2026-05-06 00:28:46 +00:00
houseme 26b3aad069 build(deps): bump the dependencies group with 3 updates (#2812) 2026-05-05 14:05:13 +00:00
安正超 bf893bcb55 fix(admin): clear removed site resync status (#2803) 2026-05-05 13:42:12 +00:00
安正超 60d4598562 test(lock): cover shared waiter abort cleanup (#2811) 2026-05-05 13:41:15 +00:00
houseme 565cbdffed fix(getobject): prevent large-download memory buffering (#2809) 2026-05-05 12:58:26 +00:00
houseme 49b2782d51 fix(lock): make slow-path waiter accounting cancellation-safe (#2805) 2026-05-05 11:19:23 +00:00
houseme 743b87014b fix(notify): validate bucket notification filter rules (#2806) 2026-05-05 10:35:36 +00:00
安正超 3208f930c5 fix(s3): advertise byte ranges on head object (#2802) 2026-05-05 04:29:25 +00:00
安正超 36b3d21c44 test: cover RPC secret trimming fallback (#2796)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 04:26:36 +00:00
cxymds 935bb8a8e2 fix(admin): allow site replication removal with offline peers (#2739)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-05 03:54:32 +00:00
安正超 9364ecba67 docs: add security advisory lessons skill (#2801)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 11:59:13 +08:00
Henry Guo 0153710791 fix(server): avoid logging default credential values (#2800)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-05 03:44:07 +00:00
Alexander Kharkevich 995e26f5ee fix(rpc): use map-encoded msgpack for all internode RPC responses (#2771)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-05 03:42:29 +00:00
JaySon e6fdcd1ad6 fix: Fix formatting issue in ARCHITECTURE.md (#2787)
Signed-off-by: JaySon-Huang <tshent@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-05 11:33:55 +08:00
安正超 f016673416 test: cover license verifier success path (#2798) 2026-05-05 02:55:56 +00:00
安正超 42ff6b4d80 test(admin): cover legacy profile auth guards (#2799) 2026-05-05 02:55:45 +00:00
安正超 3ced40f221 test(license): cover public key validation (#2793) 2026-05-04 15:12:59 +00:00
dependabot[bot] a830ab2a5d build(deps): bump the dependencies group with 4 updates (#2785)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 10:26:51 +00:00
Joey Shi 52f121ab2a Ensure CreationDate is always present in ListBuckets response (#2783) 2026-05-04 08:20:30 +00:00
安正超 eb8868397e fix: address security review follow-ups (#2781) 2026-05-03 13:53:36 +00:00
安正超 66c38b629d Harden admin and RPC security checks (#2773)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-03 11:55:09 +00:00
安正超 eb23710d2e fix(security): harden CORS and license handling (#2774) 2026-05-03 11:39:27 +00:00
houseme 4b66155f26 chore(deps): update flake.lock (#2777) 2026-05-03 02:44:53 +00:00
Ramakrishna Chilaka 4978f60254 test(ecstore): cover ranged decode and harden offset bounds (#2758)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-02 09:02:50 +00:00
安正超 fe59eb4952 test(admin): cover site identity merge gaps (#2767) 2026-05-02 08:43:47 +00:00
安正超 a455b4377c fix: handle empty multipart list-parts (#2765)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-02 02:47:52 +00:00
安正超 08fab34804 test(admin): cover peer identity scheme dedupe (#2764) 2026-05-01 04:11:46 +00:00
安正超 f07fed0c49 test(filemeta): cover no-wait refresh coalescing (#2755) 2026-05-01 02:48:30 +00:00
安正超 dcfbc2612a test(admin): cover site replication scheme normalization (#2757) 2026-05-01 02:48:20 +00:00
安正超 87e1c7aeb6 test(filemeta): cover future cache timestamp refresh (#2762) 2026-05-01 02:48:08 +00:00
安正超 0d7e0a814f test(ci): cover Helm Recreate strategy rendering (#2752) 2026-04-30 12:14:38 +00:00
安正超 ef5ccc232a test(filemeta): cover shared cache reuse (#2754) 2026-04-30 12:14:15 +00:00
GatewayJ c29c8a5a1e fix(filemeta): harden and optimize metacache path (#2724)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-30 07:47:31 +00:00
安正超 403997f2f8 test(ci): cover Helm chart version mapping (#2751) 2026-04-30 06:11:14 +00:00
安正超 e34d75dfdf test(admin): cover site replication absolute URI endpoint (#2745)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-30 06:11:00 +00:00
安正超 132e72ef39 fix(ci): keep Helm chart versions valid for non-beta releases (#2749) 2026-04-30 01:51:12 +00:00
安正超 4f4c759b67 docs: simplify pull request template (#2744) 2026-04-29 20:29:23 +08:00
majinghe e331a26262 feat: helm chart version update (#2738) 2026-04-29 11:14:35 +00:00
安正超 61d2e9fbc3 test(webdav): cover decoded path parsing (#2729)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-29 07:30:40 +00:00
majinghe 946755aa89 feat: helm publish manual trigger support (#2732) 2026-04-29 06:01:22 +00:00
houseme c9622611ed fix(admin): harden site-replication identity handling and local verification for issue 2723 (#2730) 2026-04-29 03:27:55 +00:00
majinghe 7041e628b7 fix: docker image build and helm chart publish error caused by versio… (#2731) 2026-04-29 03:23:47 +00:00
Ramakrishna Chilaka 8e1bd560d8 test(get): reject range with part number (#2725) 2026-04-28 23:15:59 +00:00
majinghe d447da75c1 chore: update version from alpha to beta (#2720)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-28 23:08:10 +00:00
Rafael Peroco 2c9524e2c9 fix(helm): only render rollingUpdate when strategy type is RollingUpdate (#2728) 2026-04-28 23:06:31 +00:00
唐小鸭 e16f1ae639 fix(window): Compatible with Windows Path (#2691)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-28 14:36:59 +00:00
安正超 d79720da1d test(signer): cover header fallback helpers (#2711) 2026-04-28 14:09:56 +00:00
安正超 b747e9817e fix(policy): preserve gateway ListBucket resources (#2710) 2026-04-28 14:09:42 +00:00
giter 90ce72122b fix(webdav): decode URL-encoded filenames in path parsing (#2722)
Signed-off-by: giter <giter@users.noreply.github.com>
2026-04-28 14:06:05 +00:00
houseme c4d5c5c5ec fix(obs): disable profiling export by default and fix Helm env name (#2719) 2026-04-28 11:57:15 +00:00
cxymds c698a0f2b6 fix(policy): allow AssumeRole in system policies (#2718) 2026-04-28 11:01:46 +00:00
houseme 2953558f41 fix(lifecycle): prevent eager date-expiry deletion on config update (#2708) 2026-04-28 10:26:14 +00:00
weisd e0b8c4fd42 fix(storage): avoid faulting local drives on transient timeouts (#2714) 2026-04-28 06:38:31 +00:00
weisd a995ec0315 fix(iam): preserve portable IAM storage and derived auth (#2713) 2026-04-28 05:57:10 +00:00
houseme 946b502527 build(deps): bump the dependencies group with 2 updates (#2709) 2026-04-27 23:53:29 +00:00
GatewayJ 09be06a4d2 fix(ecstore): log walk failures in IAM listing path (#2705)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 14:13:40 +00:00
安正超 159ddd5bac fix: honor bucket-scoped ListBucket policies with s3:prefix (#2707)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 14:13:22 +00:00
houseme a68fe1601f fix(ilm): harden signer failures and guard remote tier delete storms (#2706) 2026-04-27 12:39:02 +00:00
weisd 334184b005 fix(replication): prevent target state loss across buckets (#2704) 2026-04-27 09:27:33 +00:00
GatewayJ cfbd094bc4 fix(iam): propagate cache miss load failures (#2692)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 09:21:22 +00:00
weisd 468dc3aebd fix(replication): fan out single-bucket rules to all targets (#2701) 2026-04-27 06:51:19 +00:00
dependabot[bot] fd258e877c build(deps): bump the dependencies group with 2 updates (#2694)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 02:42:49 +00:00
安正超 4dafb64d58 test(server): cover default module switch source (#2697) 2026-04-27 02:41:38 +00:00
houseme 50d03ef021 perf(memory): add reclaim signals and cache controls (#2689) 2026-04-26 16:42:35 +00:00
houseme 438913b84a feat: add console-managed audit and notify module switches (#2690) 2026-04-26 16:10:41 +00:00
GatewayJ 37a3cbc497 fix(admin): map IAM not found errors to 404 (#2685)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-26 14:13:14 +00:00
安正超 a05687b900 test(utils): cover disk-check env alias precedence (#2684) 2026-04-26 07:41:50 +00:00
houseme 561d695237 chore(deps): update flake.lock (#2683) 2026-04-26 00:58:02 +00:00
houseme 59f41eb86a feat(obs): improve metrics coverage and dashboard performance (#2682) 2026-04-25 18:51:29 +00:00
houseme 81854762d4 fix(ecstore): log missing local paths during physical disk independence checks (#2680) 2026-04-25 08:01:01 +00:00
houseme 1e9c75a201 feat(ecstore): enforce local disk topology guardrails and expose device ids (#2679) 2026-04-25 06:02:02 +00:00
安正超 80413e0f8e test(ecstore): cover non-zero offset shard span reads (#2677)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-25 02:31:28 +00:00
houseme b96ccfd110 build(deps): bump the dependencies group with 7 updates (#2676) 2026-04-25 02:15:14 +00:00
Henry Guo c717195de2 fix(admin): harden STS and KMS authorization checks (#2653)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-25 01:34:18 +00:00
cxymds 94f64acc87 fix(site-replication): sync IAM and bucket replication (#2671) 2026-04-25 01:33:43 +00:00
唐小鸭 d949d4e794 fix: avoid sending HEAD bodies over TLS HTTP/2 (#2648)
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-25 01:33:08 +00:00
houseme 7215761784 feat(trusted-proxies): add switchable simple and legacy modes (#2674)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-25 01:25:32 +00:00
安正超 f833cd9cbe fix(metrics): keep S3 op counts when chains are disabled (#2675)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-25 01:24:53 +00:00
houseme 13b4500212 feat(obs): improve telemetry stack, replication metrics, and Grafana alignment (#2672)
Co-authored-by: Filipe Monteiro <a22407332@alunos.ulht.pt>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: weisd <im@weisd.in>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-24 13:50:17 +00:00
Henry Guo 2705e3f53b fix(audit): remove unwrap from target shutdown path (#2668)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 09:25:11 +00:00
houseme 92f812fc80 feat(rustfs): gate audit/notify by global env switches (#2669) 2026-04-24 09:14:19 +00:00
Ramakrishna Chilaka fefb308b35 feat(policy): implement BinaryEquals condition evaluation (#2626)
Signed-off-by: Ramakrishna Chilaka <49393831+RamakrishnaChilaka@users.noreply.github.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-24 07:08:11 +00:00
cxymds 8d4caeacad fix(oidc): add federated logout flow (#2667)
Co-authored-by: GatewayJ <835269233@qq.com>
2026-04-24 06:51:31 +00:00
安正超 2860c82e3c fix(console): block disabled health probes from SPA fallback (#2665) 2026-04-24 03:34:57 +00:00
houseme 572dd1264e fix(admin): harden health readiness and add health response controls (#2662) 2026-04-23 17:37:42 +00:00
houseme 47247789ad feat(obs): support OTLP headers and timeout overrides for HTTP exporters (#2661) 2026-04-23 17:02:26 +00:00
安正超 39f7de4450 test(admin): cover list service account authorization (#2650)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-23 13:40:59 +00:00
houseme de6fe816c2 build(deps): bump the dependencies group with 6 updates (#2659) 2026-04-23 12:41:37 +00:00
houseme 368ef0f16c refactor(targets): unify endpoint source/merge logic and bump rustfs-kafka-async to v1.2.0 (#2654)
Co-authored-by: Filipe Monteiro <a22407332@alunos.ulht.pt>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: weisd <im@weisd.in>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-23 09:14:36 +00:00
houseme bc37cc4001 refactor(logging): unify request-id propagation and fallback metrics (#2652) 2026-04-23 07:11:27 +00:00
GatewayJ ecf0db9bb7 fix(admin): enforce owner check for service account update (#2646)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-23 01:54:43 +00:00
1683 changed files with 593500 additions and 81122 deletions
@@ -0,0 +1,274 @@
---
name: adversarial-validation
description: Execute the Adversarial Validation policy from the root AGENTS.md — run the seven reviewer roles (correctness, simplicity, security, concurrency/durability, compatibility, performance, test coverage) with RustFS-specific attack probes. Use on every behavior-affecting code change, bug fix, or design proposal before declaring it done.
---
# Adversarial Validation Playbooks
The policy — risk tiers, role list, protocol, exit criteria — lives in the
root `AGENTS.md` under "Adversarial Validation (Default On)". Read it first;
this skill does not restate it. This file adds the RustFS-specific probe
playbook for each role: concrete attacks, where they apply, and the real
shipped bug or rule that earns each probe its place.
## How to run a role
1. Pick the tier and the applicable roles per the root `AGENTS.md`.
2. Run each role as an independent pass over the final diff — a parallel
reviewer agent where the tooling supports it, otherwise a fresh
sequential pass that starts from the diff and the nearest scoped
`AGENTS.md`, discarding the writing session's assumptions.
3. Within a role, execute the probes whose domain the diff touches, plus any
attack the diff obviously invites that no probe lists — the playbook is a
floor, not a ceiling.
4. Report findings (concrete failure scenario or named missing test, with
file:line) or the role's null report: "attacked X, Y, Z — no break
found". A bare pass is not a result.
## Role playbooks
### Correctness adversary
- For any change touching error aggregation or quorum decisions, build the exact disk-error slice at the quorum boundary: N disks where successes == quorum, then flip one success to an error (quorum-1) and separately inject None/nil placeholder entries into the slice. Trace whether reduce_errs (or the new equivalent) picks the placeholder as the dominant error or lets quorum-1 pass as success. Also check heal/write paths: does a per-target failure at quorum-1 return an explicit error, or silently degrade to success?
- Where: crates/ecstore/src/disk/error_reduce.rs; crates/ecstore/src/set_disk/{core,ops}; crates/heal
- Evidence: Commit 20d61c73b 'stop reduce_errs leaking nil placeholder as dominant error' (#4551) and 47c1e730c 'make erasure heal write quorum best-effort per target' (#4545); crates/ecstore/AGENTS.md: 'Do not weaken quorum checks... Prefer explicit failure over silent data corruption or implicit success.'
- For any change in EC read/reconstruct/streaming code, trace the mid-stream error path: the first K shards read fine, then a shard turns out bitrot-corrupt or inconsistent after N bytes have already been sent to the client. Verify the error propagates as a stream error (client sees failure), not a clean end-of-body — a silently truncated GET body is data corruption. Also re-check byte accounting: sum of per-part bytes vs object size, and partNumber-to-offset routing for the first and last part.
- Where: crates/ecstore/src/set_disk/read.rs (reconstruct-read, inconsistent_source_indexes handling ~line 3827); codec streaming paths in crates/ecstore/src/erasure_coding
- Evidence: Known live bug: EC reconstruct-read failing on inconsistent shards mid-stream silently truncates GET body → client 'unexpected EOF'; commit 15808254d 'correct codec-streaming byte accounting and partNumber routing' (#4535).
- For any listing/pagination change, construct the exact-boundary inputs: (a) exactly max_keys/max_uploads matching entries — assert the response contains max and is_truncated=false, then max+1 entries — assert exactly max returned with is_truncated=true and a correct continuation marker; (b) a delimiter listing where folding into CommonPrefixes re-fills a full page; (c) an object 'a' coexisting with prefix dir 'a/'. Off-by-one and dropped-truncation bugs live exactly at these boundaries.
- Where: crates/ecstore listing/merge paths (metacache, list_objects, list_multipart_uploads); rustfs/src/storage
- Evidence: Three recent real bugs: fefa70b31 'stop ListMultipartUploads from returning one upload past max-uploads' (#4447), d91f4d455 'report truncation when delimiter list re-folds a full page' (#4538), 7e1f7f242 'preserve CommonPrefixes when an object and same-named prefix dir coexist' (#4563).
- Run the diff's logic with a directory object key (trailing slash, e.g. 'pre/dir/') as input. Check which layer encodes/decodes __XLDIR__ — set_disk never sees the trailing slash, so any trailing-slash branch added below the store layer is dead code and a wrong-layer bug. For delete paths, check whether options force a nil versionId onto directory keys: the resulting miss surfaces as version-not-found, not object-not-found, so callers matching only ObjectNotFound leak ghost directory entries.
- Where: crates/ecstore/src/store*.rs (store layer) vs crates/ecstore/src/set_disk/*; delete option construction (del_opts) and its callers
- Evidence: trailing-slash branches must live at store layer; del_opts injects nil version for dir keys, PR#4220 ghost-directory cleanup never fired on the real path (rustfs#4307, backlog#798 still OPEN).
- Feed every new binary-UUID metadata read the three degenerate values: key absent, zero-length bytes, and 16 zero bytes (nil UUID). All three must mean 'no value' — any path that produces Uuid::nil() and then acts on it (e.g. sends it as a versionId) is a finding. For tier code specifically: with remote-tier version None or "", assert the tier GET/DELETE request carries no versionId parameter at all — sending versionId="" or nil yields NoSuchVersion against unversioned tier buckets.
- Where: crates/filemeta/src/filemeta/version.rs; crates/ecstore/src/bucket/lifecycle/; crates/ecstore/src/services/tier/; any new consumer of crates/utils/src/http/metadata_compat.rs
- Evidence: AGENTS.md Cross-Cutting Domain Invariants (defensive Uuid read pattern + unversioned-tier rule); docs/operations/tier-ilm-debugging.md ('nil transition_ver_id = corrupt legacy write-back, readers must filter'); commit 726f3dc18 'accept empty remote version_id in tier recovery paths' (#4552).
- For each match/if-let on an error or algorithm enum touched by the diff, enumerate what the wildcard/else arm swallows. Inject the variants the author didn't think of — DiskNotFound during listing, an unsupported checksum algorithm, an Err from a cleanup rename/delete — and trace whether they degrade into 'not found', a wrong-but-plausible value, or silent success. Any error path that converges with the success path without logging and propagating is a finding.
- Where: crates/ecstore (listing, delete/rename cleanup); crates/checksums; error-mapping layers in rustfs/src/storage
- Evidence: Three recent real bugs of this exact shape: e0619e355 'stop treating DiskNotFound as object not-found in listing' (#4536), afaf8c681 'reject md5 instead of silently returning crc32' (#4513), f7d2b2563 'propagate disk delete/rename failures instead of swallowing them' (#4546).
- For any change to version ordering, index lookup, or shard/part indexing: (a) call the accessor with index == len() and len()-1 — a get_idx-style bound must reject, not panic or wrap; (b) construct two versions with identical mod-times and check the sort tie-break is total and deterministic (equal keys must not compare as both before each other); (c) for EC shard math, compute shard size for object sizes 0, 1, blockSize-1, blockSize, blockSize+1 and cross-check total reconstructed length against the object size.
- Where: crates/filemeta/src/filemeta/*.rs (version sort, get_idx); crates/ecstore/src/erasure_coding shard-size math
- Evidence: Commit 8bfb00bc0 'guard get_idx bound and fix sorts_before tie-break' (#4509) — both bug classes shipped before; ecstore AGENTS.md high-risk designation for read/write/repair correctness.
- Exercise the zero/empty end of every new size or count parameter: zero-length object PUT then GET (body must be empty, not error), part count 0, empty Vec of disks/entries into aggregation functions, and env/config values of 0 (must clamp or reject, never divide-by-zero or 'scan nothing and report zero usage'). Anywhere the diff computes a ratio, capacity, or progress percentage, plug in 0 and the max value.
- Where: crates/ecstore aggregation and scanner paths; crates/object-capacity; config/env parsing in touched crates
- Evidence: Commits 787cc77a7 'clamp zero capacity env values to safe defaults' (#4559) and 32b1094ec 'resolve a symlinked scan root instead of silently counting zero' (#4564) — zero-as-silent-wrong-answer is a recurring repo bug class.
- For any diff touching multipart or object commit paths, order the operations on paper and attack the failure point between them: kill the process (or return Err) after the commit rename but before cleanup, and after cleanup but before commit. Verify the earlier-failure case leaves the object readable and the later-failure case leaves no half-visible object; part meta files must never be deleted before the commit is durable.
- Where: crates/ecstore multipart commit/cleanup (set_disk/ops); rustfs/src/storage multipart handlers
- Evidence: Commit c77c5f047 'defer multipart part.N.meta cleanup until after commit' (#4548) — cleanup-before-commit ordering already caused a real data-loss window; the #4221 durability work shows fsync/ordering bugs are endemic here.
Null report example: "Attacked quorum-1 error reduction, exact max-keys listing boundary, trailing-slash dir keys, nil-UUID tier versionId, and mid-stream reconstruct error propagation — no break found."
### Simplicity adversary
- Smaller-diff attack: rewrite the diff's change mentally (or actually, in scratch) as the minimal in-place edit and compare. Flag as findings: a helper function with exactly one caller introduced by this diff; a file rewrite where a 3-line edit inside the existing control flow suffices; reshaped control flow in init/locking/metadata/quorum paths beyond what the fix requires; new string literals duplicating existing constants (grep the token first); #[path] module inclusion. If the smaller diff achieves identical behavior, report it with the concrete replacement.
- Where: Any diff; extra scrutiny for crates/ecstore, crates/lock, rustfs/src/storage where 'preserve the existing control-flow shape' is an explicit rule
- Evidence: AGENTS.md 'Change Style for Existing Logic' (one-off helper ban, preserve control-flow shape in distributed/locking/metadata paths, no #[path]) and 'Reuse Before You Write' (constants clause); the Adversarial Validation roles list charters the simplicity adversary with exactly this attack.
- Reuse-and-necessity attack: for each new helper the diff introduces, run `ls crates/utils/src crates/common/src` and `rg -i 'fn \w*<term>'` over those dirs plus the touched crate (snake_case signatures — a full-text single-word grep drowns, a multi-word phrase returns nothing). A reimplementation of an existing workspace utility, or of plain std/tokio behavior no wrapper refines, is a finding — but so is forced reuse with mismatched semantics (normalization such as `clean` resolving `.`/`..` against raw S3 keys, error type, backoff, durability gating). For each new defensive branch, demand the nameable trigger and flag re-validation of what a validated upstream layer on the SAME path already guarantees — excluding the Cross-Cutting Domain Invariant patterns (nil/empty/absent UUID, dual metadata keys, unversioned-tier versionId) and re-checks before destructive actions, which are load-bearing even when redundant on the happy path. For each new test, flag near-duplicates pinning the same code path AND poison-value class as an existing test — boundary companions (n==max vs max+1, absent vs empty vs nil UUID, MetaObject vs MetaDeleteMarker) are never near-duplicates; the test-coverage skeptic playbook below mandates them.
- Where: Any diff adding helpers, branches on decoded/peer data, or tests; helper checks against crates/utils, crates/common, and the touched crate
- Evidence: AGENTS.md 'Reuse Before You Write' and 'Necessary Code Only'; GHSA-f4vq-9ffr-m8m3 (normalization-asymmetry traversal — why forced reuse of normalizing helpers on raw keys is itself an attack); docs/operations/tier-ilm-debugging.md nil-versionId incident (why boundary re-checks are load-bearing).
Null report example: "Rewrote the diff as an in-place edit (no smaller equivalent exists), grepped both new helpers against crates/utils, crates/common, and the touched crate (no existing equivalent; call-site semantics checked), verified the two new defensive branches name concrete corrupt-input triggers, and checked the added tests against the existing suite (each pins a distinct poison-value class) — no break found."
### Security reviewer
- For every admin handler in the diff, grep the exact AdminAction constant it passes to validate_admin_request and confirm it names the operation the handler actually performs. Construct the escalation: a low-privileged user whose policy grants the wrong-but-adjacent action (e.g. Export while the handler Imports, or Update while it Lists) — if the mismatched constant lets them through, that is the bug. Also confirm read-only endpoints (metrics, list, server-info, diagnostics) still call an operation-specific admin authz path and not a mere 'credentials exist' check.
- Where: rustfs/src/admin/handlers/**, rustfs/src/admin/router registration; check_permissions / validate_admin_request / AdminAction::* call sites
- Evidence: GHSA-vcwh-pff9-64cc (ImportIam checked ExportIAMAction), GHSA-mm2q-qcmx-gw4w (ListServiceAccount used UpdateServiceAccountAdminAction), GHSA-f5cv-v44x-2xgf (/admin/v3/metrics accepted any authenticated IAM user). advisory-patterns.md 'Admin authorization and route exposure'; rustfs/src/admin/AGENTS.md 'route registration, whitelist, handler authz must agree'.
- If the diff touches service-account or IAM import/update, treat parent, claims, accessKey, secretKey, status, policy names, and groups as attacker-controlled. Construct an ImportIam/create payload where parent points at root (or another user) and prove the code writes credentials without proving caller ownership or root authority. Separately, set deny_only=true (or 'no explicit deny') on a restricted account and check it does not skip the required allow check, letting it mint an unrestricted child.
- Where: crates/iam/, rustfs/src/admin/handlers (service account / import IAM), rustfs/src/auth.rs
- Evidence: GHSA-566f-q62r-wcr8 (attacker-controlled parent/claims/accessKey/secretKey → persistent backdoor under root), GHSA-xgr5-qc6w-vcg9 (deny_only=true skipped allow checks → privilege creation). crates/iam/AGENTS.md security boundaries; advisory-patterns.md 'IAM import, service accounts'.
- For a changed protocol-frontend handler (FTP/FTPS/SFTP/WebDAV/gateway), enumerate ALL sibling command handlers in the same driver — not just the changed one. For each, confirm it calls the per-operation IAM authorize hook mapped to the correct S3 action (RETR→GetObject, SIZE/MDTM→HeadObject, MKD→CreateBucket, bucket probe→ListBucket/HeadBucket) BEFORE touching storage. Construct a denied-authz case and prove the storage backend is never reached.
- Where: crates/protocols/ (FtpsDriver, SftpDriver, WebDAV), authorize_operation call sites
- Evidence: GHSA-3g29-xff2-92vp (FTP RETR/SIZE/MDTM authenticated but skipped IAM), GHSA-g3vq-vv42-f647 (FTPS MKD called create_bucket without s3:CreateBucket). advisory-patterns.md: 'RustFS advisories show mixed guarded and unguarded siblings in the same driver.'
- For any secret/token/signature/password comparison in the diff, check it uses a constant-time compare (e.g. subtle/constant_time_eq), not == or early-return byte loops. Then check the failure-response paths: construct an invalid-user request and an invalid-secret request and confirm they are indistinguishable (same error, no early length short-circuit) so an attacker cannot enumerate valid users or time-side-channel the secret.
- Where: crates/protocols/ (FTPS/WebDAV/FormPost auth), crates/credentials/, rustfs/src/auth.rs, RPC signature verification
- Evidence: GHSA-3p3x-734c-h5vx (FTPS/WebDAV early-return string equality + distinguishable invalid-user vs invalid-password). Fix commits 3c3113619 (constant-time FTPS/WebDAV) and c41062f27 (constant-time FormPost signature). 3p3x was fixed by PR #4403.
- If the diff touches internode/RPC auth secret handling, trace whether the RPC HMAC secret can fall back to a public default (e.g. 'rustfsadmin', 'rustfs rpc') or be derived deterministically from the S3 root credentials. Construct the case where RUSTFS_RPC_SECRET is unset and confirm the code fails closed rather than silently using a default or a root-derived key. Verify RPC signing keys are independent random secrets, not reused across S3-root/RPC-HMAC/STS-JWT roles.
- Where: crates/credentials/, crates/ecstore/src/rpc/, internode auth setup
- Evidence: GHSA-r5qv-rc46-hv8q (fell back to 'rustfsadmin'), GHSA-75fx/68cw (RPC secret derivable from root creds → forgeable signatures), GHSA-h956 (hard-coded 'rustfs rpc'), GHSA-m77q (STS JWT reused root secret). Fix commit 7b2055405 (fail closed when deriving RPC secret from default credentials, PR#4402).
- If the diff touches RPC/NodeService authentication, verify the HMAC payload binds the EXACT concrete gRPC method path (not a service prefix), the HTTP method surrogate, and a fresh timestamp. Construct captured valid metadata for method A and replay it to method B within the timestamp window — if it authorizes, the signature is under-bound. Also test stale timestamp, wrong path, wrong method, wrong secret.
- Where: crates/ecstore/src/rpc/, verify_rpc_signature / NodeServiceServer, x-rustfs-signature handling
- Evidence: GHSA-c667-rgrv-99vj (signed service prefix instead of concrete method path → cross-method replay in timestamp window). advisory-patterns.md 'RPC input validation and panic safety'; Minimum Regression Test Expectations lists replay across two methods.
- For any RPC/gRPC handler or deserialization touched, feed empty bytes, truncated MessagePack/protobuf, invalid enum discriminants, and stale timestamps. Grep the deserialization path for unwrap()/expect()/panic-prone decode and prove malformed attacker payloads return a typed error, not a panic (remote DoS). Weak internode auth makes reachability worse, so combine with the RPC-secret probe.
- Where: crates/ecstore/src/rpc/, any #[derive(Deserialize)] decoded from wire bytes, RPC handler bodies
- Evidence: GHSA-gw2x-q739-qhcr (malformed GetMetrics reached unwrap() → remote DoS). advisory-patterns.md 'Treat all RPC payload bytes as attacker-controlled.' rust-code-quality skill: unwrap abuse.
- Take any object key, RPC disk path, or archive/tar/zip entry name introduced or handled in the diff and construct traversal payloads: '../', URL-encoded '%2e%2e%2f', absolute paths, platform separators, empty components. Trace the value through parse → authz check → final storage path and prove (a) authz and storage normalize the SAME way, and (b) the canonicalized path cannot escape the bucket/prefix root. Attack the case where authz sees the raw attacker bucket but storage cleaning crosses into a victim bucket.
- Where: crates/ecstore (path join/canonicalize), rustfs/src/storage/, Snowball auto-extract / normalize_extract_entry_key, rpc read_file_stream
- Evidence: GHSA-pq29-69jg-9mxc (read_file_stream joined untrusted paths, no canonical boundary check), GHSA-8r6f-hmq2-28rg (traversal object keys bypassed authz), GHSA-f4vq-9ffr-m8m3 (Snowball '../victim-bucket/object' authorized raw path then storage crossed boundary). Note __XLDIR__ trailing-slash encoding is store-layer only.
- If the diff touches multipart/copy or presigned POST, verify UploadPartCopy enforces source GetObject AND destination PutObject semantics equivalent to CopyObject, including copy-source policy conditions (not just independent source-read + dest-write). Construct a cross-bucket UploadPartCopy from a bucket the caller cannot read. For presigned POST, submit an upload that violates content-length-range, key prefix, or exact content-type and prove the server rejects it.
- Where: rustfs/src/storage / S3 API handlers: upload_part_copy, CompleteMultipartUpload, PostObject policy enforcement
- Evidence: GHSA-mx42-j6wv-px98 (UploadPartCopy missed source authz → cross-bucket exfil), GHSA-wfxj-ph3v-7mjf (missed destination copy-source policy constraint), GHSA-w5fh-f8xh-5x3p (presigned POST didn't enforce signed policy conditions). advisory-patterns.md 'S3 copy, multipart, and upload policy validation'.
- Grep the diff for debug!/trace!/info!/error! and any ?value / {:?} on structs or response bodies that can carry secret_key, session_token, JWT claims, HMAC secrets, expected signatures, access keys beyond safe identifiers, or raw credential-bearing responses. Check custom Debug impls and merged-config dumps too. Construct the error path (invalid signature, failed auth) and confirm it does not log the secret or the derived authenticator. Verify audit/notify entries redact credential request headers.
- Where: rustfs/src/**, crates/iam/, crates/audit/, crates/notify/, crates/targets/, RPC signature error paths
- Evidence: GHSA-r54g (STS creds logged at info), GHSA-8cm2 (debug logs leaked tokens/secrets/JWT claims/raw STS bodies), GHSA-333v (invalid RPC signature log included HMAC secret + expected signature). Fix commit ee6f79110 (redact credential request headers from audit/notify, backlog#963). rustfs-logging-governance skill.
- For any struct in the diff deserialized from untrusted input (S3 XML/JSON, lifecycle rules, bucket policy, replication config, RPC payload), check for #[serde(deny_unknown_fields)]. Construct a payload with a typo'd field (e.g. 'NoncurentDays') or an extra field and prove it is rejected, not silently ignored. Flag #[serde(default)] on security-critical fields (retention days, limits, permissions) lacking explicit post-deserialize validation, and any user-controlled integer cast with `as` (i32 as u32) — feed a negative value and check it doesn't wrap to a huge positive.
- Where: crates/policy/ (bucket policy), crates/ecstore lifecycle/ILM config, replication config structs, crates/protocols XML parsing
- Evidence: AGENTS.md 'Serde Safety'; advisory-patterns.md 'Serde deserialization' (no deny_unknown_fields found repo-wide; 'NoncurentDays' typo silently accepted; i32 as u32 wrap). Fix commit 1acd47f15 (SSE crash-loop DoS + credential reserved-char bypass, backlog#806).
- If the diff touches SSE / encryption reader-writer composition, do not trust API metadata claiming encryption. Trace the reader wrapper order (HashReader / EncryptReader / compression / warp) and confirm EncryptReader is actually in the chain that writes to disk — construct the case where a helper unwraps a nested reader and bypasses encryption, storing plaintext. Require a regression test that inspects the ACTUAL stored bytes on disk, not just read-back. Also check encrypted-object checksums are not exposed.
- Where: rustfs/src/storage/ecfs.rs, crates/rio/ (reader wrappers), crates/kms/, SSE-C replication
- Evidence: GHSA-xrrf-67jm-3c2r (SSE metadata reported encryption while composition bypassed EncryptReader, stored plaintext). Fix commits a7b9659e7 (hide encrypted object checksums, #4529), 80cc3b1fc (preserve SSE-C checksum state, #4410). advisory-patterns.md 'SSE and on-disk storage invariants'.
- If the diff touches CORS or the console/browser/object-preview surface: confirm default CORS does not reflect an arbitrary Origin while also sending Access-Control-Allow-Credentials: true — construct a request with a spoofed Origin and check the response. For preview, confirm attacker-controlled object content is origin-isolated (not rendered in a same-origin iframe with console creds), served with nosniff/CSP, and that preview trust derives from validated content-type + sandboxing, NOT from object name/extension (.pdf, .html). Separately, if aws:SourceIp is evaluated, spoof X-Forwarded-For / X-Real-IP as a direct (non-trusted-proxy) client and confirm the socket peer IP is used instead.
- Where: rustfs/src/server/layer.rs (CORS), console preview/auth code, aws:SourceIp / policy condition evaluation, X-Forwarded-For handling
- Evidence: GHSA-x5xv-223c-8vm7 (default CORS reflected arbitrary origins with credentials), GHSA-v9fg-3cr2-277j (preview rendered attacker HTML same-origin, exposed localStorage creds), GHSA-7gcx-wg4x-q9x6 (extension-based PDF detection bypassed sandbox), GHSA-fc6g-2gcp-2qrq (aws:SourceIp trusted client XFF). advisory-patterns.md 'Browser, CORS' + 'Trusted proxy'.
Null report example: "Attacked admin action-constant matching in the two changed handlers (both call validate_admin_request with the exact AdminAction), the FTPS RETR/SIZE authz parity, and the new lifecycle struct's serde surface (has deny_unknown_fields) — no break found."
### Concurrency/durability reviewer
- For every new or moved lock acquisition, enumerate all other code paths that take any overlapping subset of those locks and construct the concrete ABBA interleaving (thread 1 holds A wants B, thread 2 holds B wants A). If the diff acquires 2+ locks without a comment documenting acquisition order, that alone is a finding.
- Where: crates/ecstore/** (namespace locks, set_disk, disk registry), crates/audit/**, crates/lock/**, any Mutex/RwLock pair in a diff
- Evidence: crates/ecstore/AGENTS.md 'Lock Ordering' (document order; same set in different orders = deadlock); real ABBA deadlock fixed in c0d5f938f (#4421, audit registry vs stream_cancellers)
- If the diff touches the object write/commit path or a lock guard's lifetime, construct the timeline where the distributed lock is lost (heartbeat refresh fails / expiry) after shard writes but before the xl.meta rename commit — verify the commit is fenced on guard.is_lock_lost() (set_disk/ops/object.rs:874-880) and the diff does not move the commit outside the fenced region or drop the guard early.
- Where: crates/ecstore/src/set_disk/ops/object.rs, ops/multipart.rs, crates/lock/**
- Evidence: 1e6207c08 (#4406) fence write commit on lock loss; ddf197ba5 (#4388) heartbeat lock refresh; backlog#899 fencing comment in object.rs
- For any change to file creation or write-then-rename: write out the exact syscall order (write tmp -> fdatasync tmp -> rename -> fsync parent dir -> fsync ancestor dirs on first object under a prefix) and simulate a power cut after each step. Flag any dropped/reordered sync, and check the change honors the durability gate (RUSTFS_DRIVE_SYNC_ENABLE, strict/relaxed/none modes, per-bucket overrides) instead of hardcoding one mode. The 'skip tmp parent fsync' optimization is only sound when the file is renamed out of tmp — verify that precondition still holds.
- Where: crates/ecstore/src/disk/local.rs, disk/os.rs, disk/fs.rs, crates/ecstore/src/bucket/durability.rs, set_disk/core/io_primitives.rs
- Evidence: PR #4221 (the repo previously had no fsync anywhere); 2df315baf/c081586e7 (#4493) fsync ancestor dirs; 062a68d15 (#4387) tmp-parent-fsync skip is rename-conditional; eaff17cad (#4397) durability modes; 54872d52d (#4478) rename_data crash harness exists — extend it for the diff
- If the diff touches quorum counting or per-disk error aggregation, construct adversarial error vectors for reduce_errs: nil/placeholder entries, DiskNotFound mixed with FileNotFound, exactly quorum-1 agreeing errors — and show which dominant error wins. Specifically attack the case where offline-disk errors get counted as 'object does not exist', flipping a read/heal decision into data loss. Also check quorum monotonicity: a retry or heal pass must never conclude with a LOWER quorum than the original write.
- Where: crates/ecstore/src/disk/error_reduce.rs, crates/ecstore/src/api/mod.rs, set_disk read/heal paths
- Evidence: 20d61c73b (#4551) reduce_errs leaked nil placeholder as dominant error; e0619e355 (#4536) DiskNotFound treated as object-not-found in listing; quorum monotonicity flagged as open follow-up to #4221 (#4221 follow-up list); crates/ecstore/AGENTS.md 'Do not weaken quorum checks'
- For any multi-disk fan-out (delete, rename, heal write, cleanup), trace each per-disk Result: find any `let _ =`, `.ok()`, or best-effort collapse that keeps a failed disk out of the quorum math. Construct the run where exactly write_quorum-1 disks succeed and prove the op still returns success — that is the bug. Conversely, for heal writes, check one bad target cannot fail the whole heal (best-effort per target).
- Where: crates/ecstore/src/set_disk/ops/*.rs, disk/disk_store.rs, crates/heal/**
- Evidence: f7d2b2563 (#4546) disk delete/rename failures were swallowed; 47c1e730c (#4545) heal write quorum made best-effort per target; 2b063b0c4 (#4400) disk-replacement heal missed versions
- For every new .await placed between a state mutation and its cleanup/commit (or inside select!/timeout/spawned task that can be aborted), construct the cancellation point: client disconnects and the future is dropped exactly there. Enumerate what is left behind — tmp files, incremented counters never decremented, half-written xl.meta, a held permit/waiter — and verify cleanup runs in Drop or the state is re-entrant. Background loops the diff adds must have a hard outer timeout so a wedged awaitee cannot pin them forever.
- Where: crates/ecstore/** write paths, crates/object-capacity/** scanners, crates/audit/**, anything using tokio::select! or spawn+abort
- Evidence: d608e320f io_uring cancel-safety spike (cancellation known-hard here); 5a372557e (#4533) wedged scans needed hard outer timeout; 7b87d4d13 (#4520) waiter-count leak; e44bece00 (#4497) audit start race + paused drops
- If the diff touches metacache/list producers or cursor resume, construct the interleaving where the producer task completes (or errors) while a reader with a saved cursor comes back for the next page — verify a completed producer is tolerated (no error, no hang) and that a re-folded/full page reports truncation instead of silently ending the listing early. Also feed a corrupt/oversized length prefix into any metacache decode the diff touches.
- Where: crates/ecstore/src/cache_value/metacache_set.rs, crates/ecstore/src/store/list_objects.rs, crates/filemeta/**
- Evidence: 91a23361e (#4531) tolerate completed metacache producers; d91f4d455 (#4538) delimiter re-fold dropped truncation flag; d2c100fd3 (#4226) corrupt length-prefix guard
- For multipart changes, construct concurrent operations on the SAME uploadId: put_object_part racing put_object_part (same part number), abort racing complete between the parts listing and the commit rename, and list-parts racing cleanup. Verify every metadata read/list/abort holds the per-uploadId lock, and that part.N.meta cleanup is deferred until AFTER the commit rename — cleanup before commit loses parts on a crash between the two.
- Where: crates/ecstore/src/set_disk/ops/multipart.rs, rustfs/src/storage/
- Evidence: 3bc8d79fe (#4329) serialize put_object_part per uploadId; 7fb95d4fc (#4428) unlocked upload metadata reads/aborts; 93ffbdb9b (#4437) unlocked part listings; c77c5f047 (#4548) part.N.meta cleanup moved after commit
- Any cleanup/rollback logic added near a commit: verify it runs strictly AFTER the commit is durable, is best-effort (its failure must not fail an already-committed write), and is safe under retry — i.e., re-running it after a partial first attempt must never delete the newly-committed data dir or the last surviving copy.
- Where: crates/ecstore/src/set_disk/ops/object.rs (rename_data tail), ops/multipart.rs, disk cleanup helpers
- Evidence: afc7f1d6f/d908243e6 (#4386, backlog#898) post-commit old-data-dir cleanup had to be made best-effort; e7cc719c1 (#4389) speculative tmp cleanup moved off hot path
- If the diff does read-modify-write on any persisted shared state (bucket metadata, notify/target config, queue store), construct two concurrent writers: show whether the second write silently discards the first (lost update) — RMW must be serialized or CAS-guarded. For persisted queues/replay, construct crash-mid-replay and prove entries are neither lost nor delivered twice without an idempotency key.
- Where: crates/notify/**, crates/targets/** (queue store, SQL backends), crates/ecstore/src/bucket/metadata_sys.rs
- Evidence: 2490d4ee2 (#4425) persisted config RMW lost updates; 08e44b95f (#4505) queue store crash-safety + replay lifecycle; e008cc5da (#4500) SQL backend idempotency
- If the diff touches erasure decode/reconstruct or streaming GET, construct the failure mid-stream: shards become inconsistent (or a disk read fails) after N bytes of the body have already been sent — verify the stream surfaces an error to the client instead of ending cleanly at a truncated length. Silent truncation on a 200 response is the known failure mode.
- Where: crates/ecstore/src/set_disk/read.rs (reconstruct-read validation, historically ~line 3117), crates/ecstore/src/erasure/coding/decode.rs
- Evidence: Known open bug: EC reconstruct-read 'inconsistent shards' mid-GET silently truncates body -> client unexpected EOF; crates/ecstore/AGENTS.md 'explicit failure over silent corruption'
Null report example: "Attacked lock ordering on the new disk-registry mutex pair, lock-loss fencing across the moved commit, power-cut points around the added rename, and cancellation at the two new awaits — no break found; quorum math and metacache paths untouched by this diff."
### Compatibility reviewer
- Grep the diff for raw 'x-rustfs-internal-' or 'x-minio-internal-' string literals used with map.insert/remove/get instead of the metadata_compat helpers. If found, construct the MinIO-written object case: a metadata map containing ONLY 'X-Minio-Internal-<suffix>' (mixed case, no RustFS key) and trace the diff's read path — does it miss the value? Then construct the removal case: does remove leave the twin key behind so a stale MinIO-key value resurrects on next read? Also check the value-type trap: get_bytes has NO case-insensitive fallback (unlike get_str), so a diff that moves a suffix from FileInfo.metadata (String) to meta_sys (Vec<u8>) silently loses mixed-case MinIO keys.
- Where: Any code touching FileInfo.metadata / user_defined / meta_sys: crates/ecstore/, crates/filemeta/, rustfs/src/storage/, crates/utils/src/http/metadata_compat.rs
- Evidence: Repo-wide invariant in AGENTS.md 'Cross-Cutting Domain Invariants' and CLAUDE.md; helpers and the asymmetry are pinned by tests test_str_lookup_accepts_minio_metadata_case and test_get_bytes_no_case_insensitive_fallback in crates/utils/src/http/metadata_compat.rs
- For any diff reading a binary UUID from internal metadata (transitioned-versionID, tier-free-versionID, data_dir), trace the three degenerate inputs — key absent, value empty (b""), value nil UUID — through to the outgoing tier/S3 request. The bug shape to hunt: unwrap_or_default() or Uuid::from_slice(..).unwrap_or(Uuid::nil()) turning 'no value' into Uuid::nil(), which then gets serialized as ?versionId=00000000-... and the remote tier returns NoSuchVersion. The required pattern is .and_then(|v| Uuid::from_slice(&v).ok()).filter(|u| !u.is_nil()).
- Where: crates/ecstore/src/bucket/lifecycle/ (bucket_lifecycle_ops.rs, tier_sweeper.rs), crates/ecstore/src/services/tier/warm_backend_*.rs, crates/filemeta/src/filemeta/version.rs
- Evidence: Historical production bug documented in docs/operations/tier-ilm-debugging.md ('Nil-UUID versionId sent to tier'); regression tests live in crates/filemeta/src/filemeta/version.rs; follow-up fix 726f3dc18 'accept empty remote version_id in tier recovery paths' (#4552)
- For any diff touching tier or replication GET/DELETE against a remote S3 target, enumerate BOTH directions of the versionId contract and trace each: (a) remote version None/"" means the tier bucket is unversioned — the request must carry NO versionId parameter at all (not an empty one, not nil); (b) remote version Some(v) on a versioned target — the versionId MUST be sent, especially on version-purge deletes, or the delete lands on the wrong version / creates a delete marker instead of purging. Check whether the diff collapses these cases through a single Option/String conversion that loses the distinction.
- Where: crates/ecstore/src/services/tier/warm_backend*.rs, crates/ecstore/src/bucket/lifecycle/tier_sweeper.rs, replication code under crates/ecstore/src/bucket/
- Evidence: Invariant in AGENTS.md and docs/operations/tier-ilm-debugging.md; real bug fixed by 0fad35645 'send versionId on version-purge deletes to generic S3 targets' (#4401) — the versioned direction, and #4552 — the unversioned direction
- If the diff changes xl.meta encoding (adds/reorders msgpack header fields, touches FileMeta::marshal_msg or codec.rs encode paths), attack downgrade and cross-vendor parse: encode an object with the new code and decode it with (a) the meta_ver<=3 read path and (b) the real-MinIO fixture tests from #4377. Then check the header signature: is it recomputed over the new bytes, or copied/hardcoded? MinIO validates it; a stale or zero signature makes MinIO reject the file. Finally verify XL_META_VERSION was not silently bumped — old RustFS/MinIO nodes reject meta_ver > 3 during a rolling upgrade.
- Where: crates/filemeta/src/filemeta.rs (XL_HEADER_VERSION/XL_META_VERSION, lines ~46-54), crates/filemeta/src/filemeta/codec.rs (check_xl2_v1, decode_xl_headers)
- Evidence: Real bug 073bc9675 'compute header signature instead of hardcoding zero' (#4343); format contract pinned in docs/architecture/minio-file-format-compat.md (write meta_ver 3, read <=3, XL2 magic); parity fixtures from a91d9cefc (#4377)
- If the diff touches xl.meta / FileInfo decode (into_fileinfo, version parsing, part arrays), construct hostile foreign input: a MinIO- or corruption-shaped msgpack with a parts-count that disagrees with the etags/sizes array lengths, missing optional fields, and a meta_ver 2 object with legacy checksum. Trace whether the new code indexes past an array, panics, or fabricates default values instead of returning a decode error. Run the pinned legacy fixtures (test_issue_2265_legacy_meta_v2_object_compatibility, test_issue_2288) plus the #4377 real-MinIO xl.meta parse tests against the diff.
- Where: crates/filemeta/src/ (fileinfo.rs, filemeta.rs, filemeta/codec.rs, filemeta/version.rs)
- Evidence: Real bug 7efacbdf9 'validate part array lengths in into_fileinfo' (#4382); legacy meta_ver 2 regression fixtures at crates/filemeta/src/filemeta.rs (~:1130-:1174) cited by docs/architecture/minio-file-format-compat.md
- If the diff 'corrects' a formula, constant, or layout that is a byte-for-byte MinIO port (shard-size math, bitrot hash interleaving, erasure distribution, inline-data prefix), treat the correction itself as the bug: verify against legacy on-disk data before accepting. Concretely: run crates/ecstore/tests/legacy_bitrot_read_test.rs and the ECA-18 pinning tests; check whether existing objects written by old RustFS or MinIO still verify byte-for-byte. Known trap examples: bitrot_shard_file_size's bare return for non-streaming algorithms is CORRECT MinIO whole-file behavior, and the 32-byte prefix on inline data is the HighwayHash256 bitrot hash, not corruption.
- Where: crates/ecstore/src/erasure/coding/bitrot.rs, crates/ecstore/src/io_support/bitrot.rs, crates/filemeta/src/fileinfo.rs
- Evidence: f96314a1d 'pin streaming-only bitrot layout invariant (ECA-18)' (#4553) — audit explicitly decided NOT to change the formula because it breaks legacy interop; #4377 proved the inline-data prefix is the bitrot hash
- For any diff that copies object metadata into an S3-client-visible surface (GET/HEAD response headers, notification event userMetadata, ListObjects/replication payloads, copy-object metadata directives), construct an object carrying internal keys under BOTH prefixes and in non-canonical casing ('X-Minio-Internal-Compression') and confirm every one is stripped via is_internal_key (which is case-insensitive) — not by an exact-match filter on one prefix. Leaked internal keys are an API-semantics break and an information leak.
- Where: rustfs/src/storage/, crates/notify/, replication and copy_object paths in crates/ecstore/
- Evidence: Real bug cf8929189 'strip rustfs/minio internal metadata from event userMetadata' (#4419); is_internal_key contract in crates/utils/src/http/metadata_compat.rs
- If the diff touches crates/protos (node.proto, models.fbs) or internode RPC request/response structs, attack the rolling-upgrade interleaving: an old node sends a message without the new field to a new node, and a new node sends the extended message to an old node. Verify proto field numbers are only appended (never reused/renumbered), FlatBuffers tables are only extended at the end, and that an absent new field decodes to a safe default on the receiving side — 'safe' meaning it must not be interpreted as success/authorization (RPC errors fail closed) and must not flip a quorum decision.
- Where: crates/protos/ (node.proto, models.fbs, generated/), gRPC transport and dispatch in the internode layer
- Evidence: 6f613317f 'optimize gRPC transport' (#4337) shows the wire layer churns; security advisory 68cw fixed by PR#4402 established RPC fail-closed as a repo rule (see .agents/skills/security-advisory-lessons)
- For S3 handler diffs, replay the request shapes real clients actually send, not just the canonical one: mc and aws-sdk differ on path normalization (root '//' ListBuckets), virtual-host vs path style, and header casing. Then attack every pagination boundary the diff touches: request exactly max-keys/max-uploads/max-parts items and verify the response returns exactly N (not N+1), sets IsTruncated correctly, and yields a NextMarker/KeyMarker that resumes without skipping or duplicating — construct the N+1st-item case explicitly.
- Where: rustfs/src/storage/ S3 handlers, listing paths in crates/ecstore/src/store/ and set_disk/
- Evidence: Real bugs 511ad31ba 'normalize root double-slash ListBuckets requests' (#4336) and fefa70b31 'stop ListMultipartUploads from returning one upload past max-uploads' (#4447); docs/architecture/s3-compatibility-matrix.md is the compat source of truth
- If the diff changes bucket-metadata (.metadata.bin) or IAM/config parsing structs, run it against the real MinIO RELEASE.2025-07-23 fixtures: the msgpack blob uses PascalCase field names, so any serde rename, field-type change, or derive tweak silently drops MinIO-written fields instead of erroring. Verify parse_all_configs still loads all ten config types from the fixture without loss, and that drop-in migration still decrypts MinIO-encrypted IAM/server config rather than treating ciphertext as corrupt.
- Where: crates/ecstore/src/bucket/metadata*, crates/ecstore/src/bucket/migration.rs, IAM/config load paths in crates/iam/ and crates/config/
- Evidence: Fixture parity test parses_real_minio_bucket_metadata_blob_without_loss from a91d9cefc (#4377); real bug 717cdd2ab 'decrypt MinIO IAM & server config on drop-in migration' (#4358); format matrix in docs/architecture/minio-file-format-compat.md
- If the diff adds a compatibility shim, legacy fallback, wrapper, or old-endpoint alias (grep the diff for 'legacy', 'fallback', 'compat', 'deprecated'), verify two things: (1) it carries a RUSTFS_COMPAT_TODO(<task-id>) marker with an exact removal condition and a matching entry in the register — an unmarked shim becomes permanent dead weight; (2) the fallback's default direction is safe for old data: e.g. a new decode path must fall back to the legacy decode for old objects by default, not gate legacy reads behind an opt-in flag that makes existing data unreadable after upgrade.
- Where: Anywhere in the diff; register at docs/architecture/compat-cleanup-register.md; recent example: allow_inplace_legacy_fallback flag in the ecstore erasure codec streaming path
- Evidence: docs/architecture/compat-cleanup-register.md review checklist; d232a46b4 wired legacy decode prefetch behind a default-OFF gate while keeping legacy reads working (#4542), with arity fallout fixed in 05890d6e2 (#4573)
Null report example: "Attacked dual-key metadata writes/removals against MinIO-only-key objects, nil/empty transitioned-versionID paths to the tier, xl.meta encode against meta_ver<=3 decoders and the #4377 real-MinIO fixtures, and proto field-number evolution for old-node/new-node RPC — no compatibility break found."
### Performance reviewer
- For every `.clone()` the diff adds or moves onto a per-request/per-object path, open the cloned type and count heap fields (String, Vec, HashMap, Bytes). If >5 heap fields or it contains an EC block buffer, construct the cost: N concurrent PUTs x M objects -> N*M deep copies per second. Demand Arc-wrapping of heavy fields or pass-by-reference; also flag new `String` allocations in header/path/signature parsing where `&str`/`Cow<str>` suffices.
- Where: crates/ecstore/src/set_disk/**, crates/ecstore/src/store*.rs, rustfs/src/storage/, crates/filemeta/, request handlers in rustfs/src/
- Evidence: crates/ecstore/AGENTS.md 'Allocation Discipline in Hot Paths' (no Clone on >5-heap-field structs, Arc for large buffers, &str/Cow for temporary computations); .agents/skills/rust-code-quality/SKILL.md ranks 'unnecessary clone in hot path' as P1 must-fix
- For every new sync_all/sync_data/fdatasync/flush/File::sync call in the diff, trace the call chain to DurabilityMode / RUSTFS_DRIVE_SYNC_ENABLE resolution (crates/ecstore/src/disk/local.rs:291 DurabilityMode, :347 resolve_durability_mode) and to per-bucket durability overrides. Construct the run where the operator sets mode=none (or legacy RUSTFS_DRIVE_SYNC_ENABLE=false) and the new fsync still fires — that is an ungated durability cost and a regression on 4KiB writes.
- Where: crates/ecstore/src/disk/local.rs, crates/ecstore/src/bucket/durability.rs, crates/ecstore/src/set_disk/** (rename_data/commit paths), any crate doing tokio::fs or std::fs writes
- Evidence: #4221 fsync work caused a measured -10% 4KiB write regression (#814 investigation), later gated; durability modes added in eaff17cad (#4397), per-bucket tier overrides in 13e48d93a (#4407); 2df315baf (#4493) shows even ancestor-dir fsyncs are routed through the gate
- Attack blocking-work placement from both directions: (a) find new synchronous fs calls, hashing, or EC encode/decode executed directly on an async runtime thread without spawn_blocking/block_in_place — construct the stall (a slow disk blocks a worker thread and every task queued on it); (b) find new code that splits one logical disk operation into multiple spawn_blocking hops per object — each hop is a threadpool round-trip, so K hops x N objects multiplies latency. Demand the author justify the placement with the size of the work, not habit.
- Where: crates/ecstore/src/disk/local.rs, crates/ecstore/src/erasure_coding/, crates/ecstore/src/bitrot/, crates/rio/
- Evidence: 608ab14d7 (#4554, HP-12) folded metadata open+fstat+read into a single spawn_blocking because per-op hops were measurably slow; 8fc637fb1 (#4484) moved the short EC encode inline because block_in_place cost exceeded the work — direction depends on measured work size
- For each lock acquisition the diff adds or relocates, mark the guard's live range and list every .await and disk/RPC call inside it. Construct the contention interleaving: N concurrent requests serialize on the guard while the holder waits on IO; for namespace/multipart commit locks, compute worst-case hold time (fsync + rename per disk) against the lock's timeout. Also diff the acquisition order against other paths taking the same locks (ABBA).
- Where: crates/ecstore/src/set_disk/** (commit/rename paths), crates/lock/, crates/audit/ registry, any RwLock/Mutex in per-request paths
- Evidence: crates/ecstore/AGENTS.md 'Lock Ordering'; c0d5f938f (#4421) fixed a real ABBA deadlock between registry and stream_cancellers; #4370 history: fsync-heavy serial cross-disk commits held a lock long enough to blow test timeouts (#4370)
- Trace exactly what executes inside the PUT commit critical section (under the object write lock, between tmp write and rename_data completion) before vs after the diff. Any newly added work there — cleanup, extra stat, additional rename, O_DIRECT write, logging — is an attack target: construct the per-PUT latency delta and demand it be moved off the critical section or parallelized across disks.
- Where: crates/ecstore/src/set_disk/ops/*, crates/ecstore/src/disk/local.rs rename_data path
- Evidence: Three real optimizations removed exactly this class of regression: e7cc719c1 (#4389) moved speculative PUT-tail tmp cleanup off the hot path, 92c8c6db7 (#4411) moved O_DIRECT shard-writes off the commit critical section, 651ccac13 (#4487) parallelized tmp xl.meta write and shard fdatasync on commit
- Find any new loop in a batch API that performs a per-item stat/read/RPC sequentially. Construct the concrete blowup: a 1000-key DeleteObjects or a full listing page -> 1000 serial round-trips added by the diff. Demand either a gate (skip when not needed) or bounded parallelism; for startup/load paths, check for accidental O(n^2) (re-scanning the full list per item).
- Where: crates/ecstore/src/store_delete*.rs / batch object APIs, listing/metacache paths, crates/iam/ store loading, crates/heal/
- Evidence: a413729b1 (#4398) had to gate and parallelize the DeleteObjects per-object stat fanout after it shipped serial; 16a91c35e (#4537) fixed O(n^2) IAM startup load by chunking — both were diff-introduced fanouts of this exact shape
- For every buffer the diff allocates on the encode/decode/shard path, check: is it sized with with_capacity to the EC-expanded block (not the logical size, not default-grown)? Does it copy into a fresh Vec where Bytes::slice/clone (refcount) or the io-core buffer pool would avoid the copy? Does the diff read hash and data in separate passes where one pass suffices? Construct the per-block byte-copy count before vs after. If the diff touches the io-core pool, verify gauge accounting still balances.
- Where: crates/ecstore/src/erasure_coding/, crates/ecstore/src/bitrot/, crates/io-core/src/pool.rs, crates/rio/
- Evidence: 92bf55ce6 (#4396) fixed a real regression by right-sizing BytesMut encode ingest capacity to the EC-expanded block; 47bee8b31 (#4475) merged bitrot hash+data into one read pass; 7fa3d0d4b (#4534) shows pool gauge accounting is easy to drift when touching buffer reuse
- For every logging or instrumentation statement the diff adds, classify the call site frequency: per-request, per-object, per-shard, or per-block. Anything info!/warn!/error! at per-object frequency or higher is a finding — construct the flood (one listing under client cancellation, one 10k-object heal) and count emitted lines. New metrics/timers on the data path must be feature-gated, not always-on. Run scripts/check_logging_guardrails.sh on the diff.
- Where: any per-request/per-object code, especially crates/ecstore listing and heal loops, rustfs/src/storage/ handlers; scripts/check_logging_guardrails.sh
- Evidence: .agents/skills/rustfs-logging-governance/SKILL.md (trace level for hot-path/repetitive success events); d25ddb0e1 (#4372) fixed real listing-cancellation error-log noise; hotpath instrumentation is deliberately feature-gated (3f13d098b #4394, f262fcfce #4541 HP-14)
- Count how many times the diff's request path parses or fetches the same metadata: xl.meta/FileMeta decoded more than once per object, bucket metadata (metadata_sys) re-fetched inside a per-object loop, or the dual x-rustfs-internal/x-minio-internal key lookup re-run repeatedly on the same map. Construct the per-request parse count before vs after; a second full FileMeta decode per GET is a finding.
- Where: crates/filemeta/, crates/ecstore/src/set_disk/** read paths, crates/ecstore/src/bucket/metadata_sys.rs, crates/utils/src/http/metadata_compat.rs
- Evidence: 608ab14d7 (#4554) exists because redundant metadata-read syscall sequences per object were measurable; CLAUDE.md dual-key metadata convention makes repeated get_bytes lookups an easy hidden double-parse
- If the diff touches PUT/GET/commit/erasure paths and claims 'no perf impact', demand numbers, not assertion: run the criterion benches (cargo bench -p ecstore — comparison_benchmark, erasure_benchmark, rename_data_meta_benchmark, single_block_non_inline_benchmark per crates/ecstore/benches/) against origin/main, and for end-to-end paths the warp A/B relative-budget gate (scripts/run_hotpath_warp_ab.sh --baseline-ref origin/main, as .github/workflows/performance-ab.yml runs it). Probe specifically at 4KiB object size — that is where the last real regression hid.
- Where: crates/ecstore/benches/, .github/workflows/performance-ab.yml, scripts/run_hotpath_warp_ab.sh
- Evidence: crates/ecstore/AGENTS.md: 'Benchmark-sensitive changes should include measurable rationale'; performance-ab.yml (215747022 #4480) is the repo's own relative-budget gate; the #4221 regression was only visible at 4KiB writes (#814 bisect)
Null report example: "Attacked the new rename_data commit-section work, durability-gate routing of the added fdatasync, guard live-range across the shard-write awaits, and per-object clone count in the PUT path; ran comparison_benchmark + rename_data_meta_benchmark vs origin/main (4KiB delta within noise) — no break found."
### Test-coverage skeptic
- For every behavior claim in the PR description, revert that hunk (git stash / manual undo of the changed lines) and name the exact test (`cargo test -p <crate> <test_name>`) that fails. If no test fails on revert, the behavior is untested — file a finding, not a note. Especially verify the test exercises the REAL production call path, not a lookalike helper.
- Where: All crates; highest value in crates/ecstore, rustfs/src/storage, crates/heal
- Evidence: AGENTS.md exit criterion 'Every behavior change has a test that fails without it'. Real bug: PR #4220 (ghost-directory cleanup) merged with green tests but its fix never executed on the real delete path — required follow-up rustfs#4307, backlog#798 stayed OPEN. The tests exercised a path the production flow never took.
- Read each added/modified test and confirm it asserts the real outcome (returned value, stored bytes, error variant), not merely 'call succeeded' or 'no panic'. Flag any test whose only observable is that the function returned, and any `assert!(result.is_err())` that never checks WHICH error. Then check: does the test prove the exploit/failure form is denied, or only that the intended form still works?
- Where: crates/e2e_test (security_boundary_test.rs pattern), and every #[cfg(test)] module in the diff
- Evidence: Commit dee8e4e63 (#4466) had to rewrite 277 lines of crates/e2e_test/src/security_boundary_test.rs because 'security boundary tests' passed without asserting real outcomes. .agents/skills/rust-code-quality/SKILL.md checklist: 'Every test function has at least one assert!'; .agents/skills/security-advisory-lessons/SKILL.md: 'Does the test prove the exploit form is denied, or only that the intended form still works?'
- When the diff adds a boolean/mode parameter or config flag, find the test that fails if the flag's effect is INVERTED inside the changed function. Tests that were mechanically updated to pass `false`/default at every call site assert nothing about the new behavior. Execute the check: flip the flag's branch in the source and confirm at least one test goes red for each branch.
- Where: crates/ecstore/src/set_disk/ (e.g. build_codec_streaming_part_reader), any function gaining a parameter
- Evidence: Commit 05890d6e2 (#4573): PR #4560 added a 15th param allow_inplace_legacy_fallback; the arity tests were fixed by passing `false` everywhere — they assert Err outcomes independent of the flag, so the fallback behavior itself has no revert-detecting test at those sites.
- Mutation spot-check on error propagation: for each newly added `?`, `return Err`, or error-mapping line, mentally replace it with `Ok(default)`/ignore and ask which test fails. The swallowed-error bug class recurs in this repo and always ships with green tests — a fix that propagates errors needs a test that injects the failure (faulty disk, failed rename, dispatch error) and asserts the caller sees Err.
- Where: crates/ecstore (disk delete/rename, reduce_errs), crates/audit, crates/notify, crates/targets
- Evidence: Three recent fixes for the same class: f7d2b2563 (#4546, disk delete/rename failures swallowed), dbc628f16 (#4424, audit dispatch failures swallowed), 20d61c73b (#4551, reduce_errs leaking nil placeholder as dominant error). All existed while tests were green.
- Any test touching GET/read/reconstruct/stream paths must assert the FULL body content and exact length against a known value, not status-ok or first-bytes. Construct the degraded-read case (missing/inconsistent shards forcing EC reconstruction) and assert byte-for-byte equality; a mid-stream failure that truncates the body passes every test that only checks headers or the first chunk.
- Where: crates/ecstore/src/set_disk/read.rs and ops/, crates/rio, crates/e2e_test GET scenarios
- Evidence: Known live bug: EC reconstruct-read verification failure mid-GET at set_disk read path silently truncates the body → client 'unexpected EOF'; version-independent, undetected by existing suites because none assert full-body integrity under shard inconsistency.
- For on-disk / on-wire format changes (xl.meta, .metadata.bin, bitrot framing), reject round-trip-only tests: a struct serialized and deserialized by the same code under test cannot catch format drift. Demand the test parse a REAL captured fixture from crates/filemeta/tests/fixtures, crates/ecstore/tests/fixtures, or crates/rio-v2/tests/minio_fixture_lab — or capture a new one from a single-disk MinIO instance (RELEASE.2025-07-23 procedure from #4377).
- Where: crates/filemeta, crates/ecstore (headers, msgpack bucket metadata), crates/rio-v2, migration code
- Evidence: Commit 073bc9675 (#4343): filemeta header signature was hardcoded to zero — round-trip tests passed for months. Commit a91d9cefc (#4377) established the real-MinIO fixture convention (inline/versioned/multipart xl.meta, HighwayHash256-prefixed inline bodies) precisely because synthetic fixtures proved nothing about interop.
- Attack new concurrency tests for flakiness-by-construction: grep the added tests for `sleep(`, fixed timeouts under ~30s on lock acquisition, and use of shared global state (disk registry, lock client, GLOBAL_*). Serialized cross-disk commits exceed small lock timeouts under full-suite CI disk load. If the test shares global state or saturates IO, it must join the `ecstore-serial-flaky` nextest test-group in .config/nextest.toml (note: serial_test's #[serial] does NOT work — nextest runs each test in its own process). Require readiness polling, never fixed sleeps.
- Where: crates/ecstore tests, crates/e2e_test, .config/nextest.toml
- Evidence: Commit 2dfa3d3c3 (#4370): concurrent_resend test flaked with Lock(Timeout 5s) on CI — six legitimate serialized cross-disk commits under IO pressure needed 30s. Commit 7c701d9f2 (#4558) created the nextest test-group after bucket_delete_* raced make_bucket into InsufficientWriteQuorum. 65849740f (#4213) deflaked global-state contamination. crates/e2e_test/AGENTS.md: 'readiness checks and explicit polling over fixed sleep-based timing'.
- If the diff writes internal object metadata, run the dual-key mutation: delete the `x-minio-internal-<suffix>` write (keeping only `x-rustfs-internal-`) and check whether any test fails. Because `get_bytes` prefers the RustFS key, every read-back test stays green while MinIO interop is silently broken — coverage must include an assertion that BOTH keys are present in the stored metadata map.
- Where: crates/utils/src/http/metadata_compat.rs and all its callers in crates/ecstore and rustfs/src/storage
- Evidence: CLAUDE.md domain convention: metadata must be written under both x-rustfs-internal- and x-minio-internal- keys for MinIO interop; get_bytes prefers the RustFS key, making the MinIO-key half of the invariant invisible to read-back tests.
- For changed quorum/version/UUID logic, name the tests covering the specific poison values: quorum1 disks, nil UUID, absent vs empty vs nil-serialized UUID bytes, remote-tier version_id of None/"" (unversioned tier bucket → no versionId sent), and the same metadata read on both MetaObject and MetaDeleteMarker version types. Mutation check: remove a `.filter(|u| !u.is_nil())` guard from the diff and confirm a test fails; if none does, the nil-UUID class is uncovered.
- Where: crates/ecstore (tier recovery, heal, quorum paths), crates/filemeta, code reading UUIDs from xl.meta metadata
- Evidence: Commit 726f3dc18 (#4552) fixed rejection of empty remote version_id in tier recovery. CLAUDE.md invariant: absent/empty/nil UUID all mean 'no value', not Uuid::nil(). docs/operations/tier-ilm-debugging.md: None/"" tier version means unversioned bucket. df9cbc4ed (#4427): unvalidated distribution values caused shuffle index panic — edge values reached production untested.
- For any pagination/limit/truncation change, construct the exact-boundary test: result count == max (page exactly full), max+1, and a delimiter re-fold that lands precisely on the page boundary — assert both the item count AND the is_truncated/continuation marker. Off-by-one at the page boundary is a recurring shipped bug here.
- Where: crates/ecstore listing paths (list_objects, ListMultipartUploads, metacache), S3 handlers in rustfs/src/storage
- Evidence: Two shipped boundary bugs: fefa70b31 (#4447) ListMultipartUploads returned one upload past max-uploads; d91f4d455 (#4538) delimiter re-fold of a full page lost the truncation flag. Both survived existing tests because no test pinned n == max exactly.
- Green `cargo test -p <crate>` on the touched crate is not a coverage verdict for the diff's test code itself: run `cargo clippy --all-targets -p <crate>` and a workspace-wide test BUILD (`cargo check --workspace --all-targets` at minimum) before accepting the tests as evidence. Test-only code that doesn't compile workspace-wide or fails clippy has repeatedly broken main and masked whether tests ran at all.
- Where: All crates; especially concurrent-branch merges into crates/ecstore
- Evidence: #4322 broke main because only cargo test ran (field_reassign_with_default is clippy-only). b06f3df6b (#4441) and 05890d6e2 (#4573): test code broke the workspace test build (E0061) on main after textually-clean merges, failing CI for every open PR.
Null report example: "Attacked revert-detection for all 3 claimed behaviors (each has a named test that fails on revert), flag-inversion on the new fallback parameter (both branches covered in codec_streaming tests), full-body assertions on the changed GET path, and n==max pagination boundary — no coverage gap found."
## Sources and maintenance
Probes are distilled from shipped bugs in git history (commit/PR references
above), GitHub security advisories (see the security-advisory-lessons
skill), scoped `AGENTS.md` rules, and invariants under `docs/architecture/`
and `docs/operations/`. Line numbers drift; when a cited location no longer
matches, trust the invariant and re-locate the code. When a new bug class
ships, add a probe with its evidence here rather than growing the policy
section in `AGENTS.md`.
+58
View File
@@ -0,0 +1,58 @@
---
name: arch-checks
description: Resolve failures from the repository's architecture guard scripts — check_layer_dependencies.sh, check_architecture_migration_rules.sh, check_unsafe_code_allowances.sh, check_logging_guardrails.sh, check_doc_paths.sh. Use when make pre-commit / pre-pr or CI fails on one of these checks.
---
# Architecture Guard Checks
All five run in `make pre-commit` / `make pre-pr` and in CI. Fix the cause;
never weaken a check to get green.
## `check_layer_dependencies.sh` — layer DAG in `rustfs/src`
Enforces `interface (admin, storage/ecfs, storage/s3_api) → app → infra`; no
upward imports. Known legacy violations live in
`scripts/layer-dependency-baseline.txt`.
- **New violation**: restructure your change so the dependency points
downward (move the shared type/function to the lower layer).
- **You legitimately removed a baseline entry**: run
`./scripts/check_layer_dependencies.sh --update-baseline` and commit the
shrunken baseline. Never add new entries to the baseline to make a new
violation pass.
## `check_architecture_migration_rules.sh` — required doc sections
Asserts that the core docs under `docs/architecture/` (overview,
crate-boundaries, runtime-lifecycle, readiness-matrix,
storage-control-data-plane, global-state-crate-split-plan,
ecstore-module-split-plan, …) still contain specific headings and exact
source lines. If it fails after a doc edit, you reworded or removed a
guarded line — restore the wording or update the script deliberately in the
same PR, with rationale.
## `check_unsafe_code_allowances.sh`
Every `#[allow(unsafe_code)]` needs a `SAFETY:` comment within a few lines.
Write the actual safety argument; don't add a placeholder.
## `check_logging_guardrails.sh`
A fixed list of security-sensitive files (auth, IAM, KMS, admin handlers…)
is scanned for logging violations. If you created a new sensitive file,
consider adding it to the script's `checked_files` list.
## `check_doc_paths.sh`
Instruction/architecture docs (`AGENTS.md`, `CLAUDE.md`, `ARCHITECTURE.md`,
`docs/architecture/*.md`) must not reference repo file paths that no longer
exist. If your refactor moved code, update the docs that point at it — the
error message lists `doc -> stale-path` pairs.
## `check_no_planning_docs.sh`
Planning-type documents must not be committed (see AGENTS.md "Sources of
Truth"). The guard fails if anything is tracked under `docs/superpowers/`
`.gitignore` already ignores it, but `git add -f` bypasses that, so this closes
the hole. Fix by removing the listed file(s) with `git rm`; keep the plan or
spec in the issue tracker or a local worktree instead.
@@ -41,6 +41,10 @@ Use this skill to review code changes consistently before merge, before release,
- hidden coupling to shared helpers/constants/features
- If a point is uncertain, mark it as an open question instead of guessing.
#### Rust-specific checks (apply to all Rust changes)
Run the full checklist in [rust-code-quality](../rust-code-quality/SKILL.md) — the canonical Rust review checklist for the unwrap/casting/cloning/locking/recursion/error-type/serde/test rules and the reuse-and-necessity checks (duplicated helpers, defensive branches without a nameable trigger, redundant error wrapping). Do not restate those rules here; carry its P0P3 ratings over unchanged and use this skill's output format.
### 4) Findings-first output
- Order findings by severity:
- P0: critical failure, security breach, or data loss risk
@@ -0,0 +1,83 @@
---
name: plugin-contract-guard
description: Invariants and change procedure for the target-plugin / extension system — plugin manifests, admin plugin/extension catalog and instance APIs, secret redaction, external-plugin install policy. Use when editing crates/targets (manifest, plugin, control_plane, catalog, runtime), crates/extension-schema, or rustfs/src/admin plugin_contract.rs / plugins_*.rs / extensions.rs / target_descriptor.rs.
---
# Plugin & Extension Contract Guard
The "plugin system" spans four surfaces that must stay consistent:
| Surface | Location |
|---|---|
| Manifests & registry | `crates/targets/src/{manifest,plugin}.rs` |
| Install/enable planning (control plane) | `crates/targets/src/control_plane.rs` |
| Extension schemas | `crates/extension-schema/src/lib.rs`, `crates/targets/src/catalog/extension.rs` |
| Admin API contract | `rustfs/src/admin/plugin_contract.rs`, `handlers/{plugins_catalog,plugins_instances,extensions,target_descriptor}.rs` |
## Hard invariants (verify before merging)
1. **Secrets have one source of truth.** Secret config keys are declared only
in the plugin manifest (`TargetPluginManifest.secret_fields`,
`crates/targets/src/manifest.rs`) and flow to admin via
`AdminTargetSpec.secret_fields`. Never add a hand-maintained per-service
secret table in a handler; if redaction misses a field, fix the manifest.
2. **Redaction must round-trip.** Instance GET responses replace secret values
with `***redacted***` (`REDACTED_SECRET_VALUE` in `plugins_instances.rs`).
Instance PUT restores the stored secret when it receives that placeholder
back (`restore_redacted_secret_values`). Any new read or write path for
target config must keep both halves: redact on the way out, restore the
placeholder on the way in. The placeholder literal must never be persisted.
3. **Fixtures never reach production responses.**
`example_external_webhook_plugin()` (`crates/targets/src/catalog/mod.rs`)
is a test/demo fixture for control-plane planning tests. Production
catalog/extension handlers must not include it; regression tests
(`plugin_catalog_never_exposes_example_or_external_fixtures`,
`extension_catalog_never_exposes_example_or_external_fixtures`) enforce it.
4. **External plugin flow is planning-only and deny-by-default.**
`plan_external_target_plugin_action` returns decisions, it executes
nothing. `TargetPluginExternalFlowGate::default()` is fully closed and
`TargetPluginInstallPolicy::default().allowed_download_hosts` is empty —
keep it that way; tests opt in via explicit policies. Install validation
requires https, an allowlisted host, a full 64-hex-char sha256 digest,
signature and provenance URIs, and an artifact matching the host
`target_triple`.
5. **Custom target types must not collide.** Unknown target types get an
interned unique `custom:<type>` plugin id (`custom_plugin_id` in
`manifest.rs`). Custom plugins with secrets must register via
`TargetPluginDescriptor::with_manifest` and declare `secret_fields`;
`::new` derives a manifest with no secrets.
## Changing the admin JSON contract
- Shapes are locked twice in `plugin_contract.rs` tests: insta snapshots
(`rustfs/src/admin/snapshots/`) plus literal `json!` assertions. Update
both deliberately; a shape change is a console-facing API change.
- Field naming is `snake_case`, except discovery blocks
(`runtimeCapabilities`, `clusterSnapshot`, `extensionsCatalog`) which are
camelCase **by cross-endpoint convention** (same shape in `system.rs`,
`console.rs`, `pools.rs`). Do not "fix" that inconsistency locally.
- Contract types deliberately duplicate `rustfs_targets` types
(anti-corruption layer). Add a `From` impl; do not serialize internal
types directly.
## Handler conventions
- Every new admin plugin/extension route needs authorization at the top of
`call` and an `include_str!` guard test asserting it (repo-wide pattern —
see `plugin_instance_handlers_require_admin_authorization_contract`).
- Reads use `GetBucketTargetAction` (instances) or `ServerInfoAdminAction`
(catalogs); writes use `SetBucketTargetAction`.
- Refresh persisted module switches once per request
(`refresh_persisted_module_switches`), then evaluate the sync
`module_disabled_block_reason` per domain — do not re-read the store per
domain or per instance.
## Generic bounds
Event-payload generics use the `PluginEvent` blanket trait
(`crates/targets/src/plugin.rs`). Do not respell
`Send + Sync + 'static + Clone + Serialize + DeserializeOwned`.
+5 -2
View File
@@ -30,7 +30,9 @@ Use this skill before `gh pr create`, before `gh pr edit`, or when reviewing whe
- Treat introducing a new hardcoded literal where a project constant already exists as a likely regression risk; require either a refactor to reuse the constant or an explicit exception explanation in the PR body.
3. Verify readiness requirements
- Require `make pre-commit` before marking the PR ready.
- Require `make pre-commit` before marking PRs ready when the diff changes Rust code, product behavior, CI behavior, runtime configuration, security-sensitive logic, migrations, storage, auth, networking, or other high-risk paths.
- For documentation-only, agent-instruction-only, or local developer-tooling-only changes, allow focused verification instead of `make pre-commit` when it directly validates the changed surface.
- For focused verification, explain why the full gate was not run and list the scope-specific commands in the PR body.
- If `make` is unavailable, use the equivalent commands from `.config/make/`.
- Add scope-specific verification commands when the changed area needs more than the baseline.
- If required checks fail, stop and return `BLOCKED`.
@@ -79,7 +81,8 @@ Use this skill before `gh pr create`, before `gh pr edit`, or when reviewing whe
## Blocker rules
- Return `BLOCKED` if `make pre-commit` has not passed.
- Return `BLOCKED` if a code, behavior, CI, runtime configuration, security-sensitive, migration, storage, auth, networking, or other high-risk change has not passed `make pre-commit`.
- Return `BLOCKED` if a documentation-only, agent-instruction-only, or local developer-tooling-only change lacks focused verification for the changed surface.
- Return `BLOCKED` if the diff contains unrelated changes that are not acknowledged.
- Return `BLOCKED` if required template sections are missing.
- Return `BLOCKED` if the title/body is not in English.
@@ -3,7 +3,8 @@
- Confirm the branch is based on current `main`.
- Confirm the diff matches the stated scope.
- Confirm no secrets, logs, temp files, or unrelated refactors are included.
- Confirm `make pre-commit` passed, or document why it could not run.
- Confirm `make pre-commit` passed for code, behavior, CI, runtime configuration, security-sensitive, migration, storage, auth, networking, or other high-risk changes.
- For documentation-only, agent-instruction-only, or local developer-tooling-only changes, confirm focused verification covered the changed surface and the PR body explains why the full gate was not run.
- Confirm extra verification commands are listed for risky changes.
- Confirm the PR title uses Conventional Commits and stays within 72 characters.
- Confirm the PR title does not use tool-specific prefixes such as `[codex]`.
+122
View File
@@ -0,0 +1,122 @@
---
name: rust-code-quality
description: Enforce Rust-specific code quality rules on every code change. Use before merge to catch unwrap abuse, silent truncation, unnecessary cloning, lock ordering violations, recursion risks, and error type anti-patterns.
---
# Rust Code Quality Gate
Use this skill on every Rust code change to enforce quality rules that `cargo clippy` does not catch.
## Quick Start
1. Identify changed `.rs` files.
2. Run automated checks on changed files.
3. Run manual review checklist on the diff.
4. Report findings; block merge if P0/P1 issues exist.
## Automated Checks
Run these on every changed `.rs` file (excluding test modules):
```bash
# 1. unwrap/expect in production code
rg -n '\.unwrap\(\)|\.expect\(' <changed-files> | grep -v '#\[cfg(test)\]' | grep -v 'test' | grep -v 'bench'
# 2. Silent type truncation via `as` cast
rg -n ' as (u8|u16|u32|u64|usize|i8|i16|i32|i64|isize)\b' <changed-files>
# 3. String as error type
rg -n 'Result<.*String>' <changed-files> | grep -v test
# 4. Box<dyn Error> in public APIs
rg -n 'Box<dyn.*Error' <changed-files> | grep -v test
# 5. println/eprintln in production
rg -n 'println!\|eprintln!' <changed-files> | grep -v test
# 6. Ordering::Relaxed usage (verify each is intentional)
rg -n 'Ordering::Relaxed' <changed-files>
# 7. Default substituted for a possibly-required value (judge each: is the value optional by domain?)
rg -n 'unwrap_or_default\(\)|unwrap_or\(' <changed-files>
```
## Manual Review Checklist
For every Rust code change, verify:
### Error Handling
- [ ] No `unwrap()` or `expect()` in production code without justification comment
- [ ] No `Result<_, String>` in public API signatures
- [ ] No `Box<dyn Error>` in public trait/struct methods
- [ ] `Error::source()` is overridden when inner error is stored
- [ ] Error messages are actionable (what failed, with what input)
### Type Safety
- [ ] No silent `as` truncation (negative→unsigned, large→small)
- [ ] `try_into()` or explicit clamping used for numeric conversions
- [ ] No `f64 as usize` without prior clamping
### Concurrency
- [ ] Lock acquisition order is documented when multiple locks are used, and matches every other call site taking any overlapping subset (ABBA check)
- [ ] No `tokio::sync` lock guard (read or write) held across `.await` without bounded hold time — long-lived read guards wedge writers (#4195)
- [ ] Concurrent counters use `compare_exchange` loops, not load-then-store
- [ ] `std::sync::Mutex` in async context is held only briefly, never across `.await`
### Memory and Performance
- [ ] No `.clone()` on structs with >5 heap-allocated fields in hot paths
- [ ] `HashMap::with_capacity()` / `Vec::with_capacity()` used when size is known
- [ ] Large buffers wrapped in `Arc` rather than cloned
- [ ] Temporary string computations use `&str` or `Cow<str>` instead of `String`
### Recursion Safety
- [ ] Recursive functions have a depth limit or use iterative traversal
- [ ] Tree/cache traversals handle corrupted/cyclic input safely
### Testing
- [ ] Every test function has at least one `assert!`
- [ ] Tests use `.expect("context")` not bare `.unwrap()`
- [ ] No `println!`/`eprintln!` in production code (use `tracing`)
### Serde
- [ ] Structs from untrusted input have `#[serde(deny_unknown_fields)]`
- [ ] `#[serde(default)]` not used on security-critical fields without validation
### Code Hygiene
- [ ] No `#![allow(dead_code)]` at crate root
- [ ] No camelCase statics or Hungarian notation
- [ ] New string literals don't duplicate existing constants
### Reuse and Necessity
- [ ] No new helper duplicating an existing workspace utility (`crates/utils`, `crates/common`, the touched crate) or plain std/tokio behavior no wrapper refines; reused helpers match the call site's semantics (normalization, error type, backoff, durability gating)
- [ ] No branch without a nameable concrete trigger; no re-validation of what a validated upstream layer on the same path already guarantees (Cross-Cutting Domain Invariant patterns and pre-destructive-action re-checks are load-bearing — keep them)
- [ ] Error context attached once where actionable, not re-wrapped at every hop; no typed→generic error conversion below aggregation/quorum layers
- [ ] No comments narrating the next line, restating a signature, or describing the change itself (invariant comments — lock ordering, `SAFETY`, unwrap justification — are not narration)
- [ ] No near-duplicate test pinning the same code path and poison-value class as an existing test (boundary companions — n==max vs max+1, absent/empty/nil UUID — are never near-duplicates)
## Severity Classification
- **P0 (Block merge)**: `unwrap()` in request hot path, silent truncation on user input, lock ordering violation, recursion without depth limit
- **P1 (Must fix)**: `Result<_, String>` in public API, unnecessary clone in hot path, `Box<dyn Error>` in trait method, `unwrap_or_default()` on a domain-required value (metadata, quorum, version id)
- **P2 (Should fix)**: Missing `assert!` in test, `println!` in production, missing `with_capacity`, new helper duplicating an existing workspace utility, defensive branch with no nameable trigger (corrupt or stale persisted/peer data is always a nameable trigger for boundary-crossing values), near-duplicate test, redundant error re-wrapping
- **P3 (Nice to fix)**: Naming convention violation, missing doc comment, `as_ptr()` vs `Arc::ptr_eq`, narrating comment
## Output Template
```
## Rust Code Quality Report
### Automated Scan
- unwrap/expect in production: N found
- as casts: N found
- String errors: N found
- println/eprintln: N found
### Findings
- [P1] `path:line` — description
- Fix: ...
- Validation: ...
### Verdict
PASS / BLOCKED (list blocking findings)
```
@@ -0,0 +1,52 @@
# Rust Code Quality Checklist
Use this as a quick pre-merge checklist for every Rust code change.
## Critical (P0 — block merge)
| Check | Command |
|-------|---------|
| No `unwrap()` in request/storage hot path | `rg '\.unwrap\(\)' <files> \| grep -v test` |
| No `as` truncation on user input | `rg ' as (u32\|usize\|i32)' <files>` |
| Lock order consistent across call sites | Manual: trace all lock acquisitions |
| Recursive functions have depth limit | Manual: check for `max_depth` or iterative pattern |
| No `panic!`/`unwrap_or_else(panic!)` in production | `rg 'panic!\|unwrap_or_else.*panic' <files> \| grep -v test` |
## High (P1 — must fix)
| Check | Command |
|-------|---------|
| No `Result<_, String>` in public API | `rg 'Result<.*String>' <files> \| grep -v test` |
| No `Box<dyn Error>` in public trait | `rg 'Box<dyn.*Error' <files> \| grep -v test` |
| No unnecessary `.clone()` in hot path | Manual: check loops and per-request paths |
| `Error::source()` implemented when inner error stored | Manual: check `impl Error` |
| No `eprintln!`/`println!` in production | `rg 'println!\|eprintln!' <files> \| grep -v test` |
## Medium (P2 — should fix)
| Check | Command |
|-------|---------|
| Tests have assertions | Manual: check for `assert` in test functions |
| `HashMap`/`Vec` use `with_capacity` when size known | Manual: check `::new()` in loops |
| No `#![allow(dead_code)]` at crate root | `rg 'allow.dead_code' <files> \| grep 'lib.rs'` |
| Serde structs from untrusted input have `deny_unknown_fields` | Manual: check `#[derive(Deserialize)]` |
## Low (P3 — nice to fix)
| Check | Command |
|-------|---------|
| No camelCase statics | `rg 'static ref [a-z]' <files>` |
| `Arc::ptr_eq` instead of `as_ptr + ptr::eq` | `rg 'as_ptr\|ptr::eq' <files>` |
| Public functions have doc comments | `rg 'pub fn' <files> \| grep -v '///'` |
## Quick One-Liner
```bash
# Run all automated checks on changed files
CHANGED=$(git diff --name-only HEAD~1 -- '*.rs' | grep -v test | grep -v bench)
echo "=== unwrap/expect ===" && rg -c '\.unwrap\(\)|\.expect\(' $CHANGED 2>/dev/null
echo "=== as casts ===" && rg -c ' as (u8|u16|u32|u64|usize|i8|i16|i32|i64|isize)\b' $CHANGED 2>/dev/null
echo "=== String errors ===" && rg -c 'Result<.*String>' $CHANGED 2>/dev/null
echo "=== println ===" && rg -c 'println!|eprintln!' $CHANGED 2>/dev/null
echo "=== Ordering::Relaxed ===" && rg -c 'Ordering::Relaxed' $CHANGED 2>/dev/null
```
@@ -0,0 +1,107 @@
---
name: rustfs-logging-governance
description: Standardize and review RustFS logging with structured `tracing` events, lower noise on hot paths, preserve security-sensitive diagnostics, and extend guardrails to prevent legacy logging patterns from returning. Use when editing or reviewing RustFS logs, startup/config diagnostics, cloud metadata logs, request validation logs, or `scripts/check_logging_guardrails.sh`.
---
# RustFS Logging Governance
Use this skill when RustFS logging needs to be added, cleaned up, reviewed, or protected against regressions.
## Quick Start
1. Identify the files whose logs are changing.
2. Scan current `tracing` or `log` macros before editing.
3. Convert sentence-style logs to short event-style logs.
4. Demote hot-path success logs unless operators truly need them at `info`.
5. Preserve failure, fallback, and security-relevant diagnostics.
6. Update `scripts/check_logging_guardrails.sh` when a broad cleanup removes a legacy pattern class.
7. Validate with formatting, targeted checks/tests, and the logging guardrail script.
## Core Workflow
### 1. Scope the logging surface
- Read the changed module in full before touching log lines.
- Classify the log site:
- lifecycle/startup
- request or validation path
- background loop or hot path
- fallback/degraded behavior
- cloud metadata or external fetch path
- metrics/config summary
- Do not rewrite business logic to make logging easier.
### 2. Use the RustFS event shape
- Prefer fields first, message second.
- Use short labels, not prose paragraphs.
- Default field shape:
- `event`
- `component`
- `subsystem`
- `state` or `result`
- key context fields
- Reuse stable field names and avoid inventing near-duplicates.
See `references/logging-governance.md` for the event model, level policy, and anti-pattern list.
### 3. Choose the right level
- `error`: operation failure that affects behavior or security guarantees.
- `warn`: degraded path, fallback, suspicious input, or operator-actionable misconfiguration.
- `info`: low-frequency lifecycle or mode selection.
- `debug`: targeted diagnostics and low-volume detail.
- `trace`: hot-path and repetitive success-path events.
When in doubt, lower the verbosity of normal success paths and keep structured detail in fields.
### 4. Preserve security and privacy boundaries
- Do not log secrets, tokens, auth headers, raw credential payloads, or merged config dumps.
- Avoid logging raw forwarded headers or full trusted network inventories above `debug`.
- Keep warning/error logs useful without echoing attacker-controlled payloads unnecessarily.
### 5. Keep summaries aggregated
- Replace multi-line startup banners or checklist logs with one structured event.
- If metrics already express a concept, avoid duplicating it with many `info!` lines.
- Prefer counts, modes, and sources over inventories unless debug detail is truly needed.
### 6. Update guardrails when needed
- Broad logging cleanup should usually extend `scripts/check_logging_guardrails.sh`.
- Add forbidden patterns only for styles the repo has intentionally retired:
- sentence-style lifecycle logs
- noisy hot-path `info!`
- checklist-style summary logs
- legacy fallback wording that has been replaced by structured fields
- Keep guardrails concrete and grep-friendly.
### 7. Validate manually
Use the smallest relevant set:
```bash
cargo fmt --all --check
./scripts/check_logging_guardrails.sh
cargo check -p <affected-crate>
cargo test -p <affected-crate>
```
For broader Rust changes, add:
```bash
./scripts/check_unsafe_code_allowances.sh
./scripts/check_architecture_migration_rules.sh
cargo clippy -p <affected-crates> --all-targets -- -D warnings
```
## RustFS-Specific Notes
- The durable RustFS logging direction is `event + component + subsystem + state/result + key context fields`.
- `crates/concurrency` and `crates/trusted-proxies` are examples of this style for lifecycle, fallback, and cloud metadata logs.
- `scripts/check_logging_guardrails.sh` is the enforcement point for preventing removed log styles from returning.
## References
- Read `references/logging-governance.md` when you need the detailed field set, anti-pattern examples, or guardrail update checklist.
@@ -0,0 +1,4 @@
interface:
display_name: "RustFS Logging Governance"
short_description: "Standardize RustFS logs with structured events and guardrails."
default_prompt: "Use $rustfs-logging-governance to standardize or review RustFS logging, reduce noise, and update guardrails."
@@ -0,0 +1,285 @@
# RustFS Logging Governance Reference
## Workspace Scope Map
Use `Cargo.toml` `[workspace].members` as the source of truth for crate membership. When doing a broad logging sweep, classify crates by operational role so logs stay consistent within each role.
### Core Server And Request Handling
- `rustfs`
- Role: top-level server, startup, auth, admin wiring, S3 request handling.
- Logging focus: startup lifecycle, config summaries, authn/authz failures, protocol entrypoints, degraded subsystems.
- `crates/protocols`
- Role: protocol integrations such as FTP, SFTP, WebDAV, and related server-side protocol layers.
- Logging focus: listener lifecycle, per-protocol enablement/disablement, request bridge failures.
- `crates/madmin`
- Role: admin API contracts and management interfaces.
- Logging focus: admin action boundaries, validation failures, compatibility warnings.
- `crates/trusted-proxies`
- Role: forwarded IP trust, proxy chain validation, cloud metadata sources.
- Logging focus: direct/trusted/fallback decisions, degraded metadata fetches, aggregated config summaries.
- `crates/keystone`
- Role: Keystone auth integration.
- Logging focus: integration enablement, upstream auth failures, config safety without credential leakage.
### Storage, Healing, And Data Plane
- `crates/ecstore`
- Role: erasure-coded storage implementation and peer/store initialization.
- Logging focus: disk/peer lifecycle, storage fallback, object I/O failures, avoid per-object noise.
- `crates/heal`
- Role: healing orchestration and repair workflows.
- Logging focus: scheduler lifecycle, repair decisions, backlog or skipped work summaries, avoid repetitive task spam at `info`.
- `crates/scanner`
- Role: data integrity scanning and health monitoring.
- Logging focus: scan lifecycle, compaction/deep-heal transitions, lag/backlog, noisy folder iteration should stay at `debug/trace`.
- `crates/object-capacity`
- Role: capacity scan and refresh core.
- Logging focus: refresh lifecycle, degraded capacity sources, aggregate stats rather than per-object chatter.
- `crates/filemeta`
- Role: file metadata parsing and helpers.
- Logging focus: parse failures, schema/format mismatch, avoid dumping raw metadata payloads.
- `crates/storage-api`
- Role: storage contracts and shared data plane interfaces.
- Logging focus: contract mismatch and boundary diagnostics, usually low-volume.
- `crates/checksums`
- Role: checksum helpers and validation.
- Logging focus: integrity failures and compatibility mismatches, not per-chunk success logs.
- `crates/zip`
- Role: ZIP handling and compression helpers.
- Logging focus: parse/extract failures, archive path safety issues, avoid verbose file-by-file success logs.
### Security, Identity, And Policy
- `crates/iam`
- Role: identity and access management.
- Logging focus: authz decision boundaries, imported payload safety, do not leak principals, secrets, or claims.
- `crates/policy`
- Role: policy modeling and evaluation.
- Logging focus: deny/allow decision context, parser/validation failures, no raw secret-bearing request dumps.
- `crates/credentials`
- Role: credential handling.
- Logging focus: never log secrets or tokens; only safe identifiers and redacted states.
- `crates/kms`
- Role: key management service integration.
- Logging focus: init/health/fallback, key-source availability, never log key material.
- `crates/crypto`
- Role: cryptographic helpers and security primitives.
- Logging focus: only algorithm or mode state, not plaintext, ciphertext, or secret-derived material.
- `crates/security-governance`
- Role: security governance contracts.
- Logging focus: policy/state transitions and enforcement diagnostics.
- `crates/signer`
- Role: request signing helpers.
- Logging focus: signature validation failures without expected-signature leakage.
### Notifications, Audit, And Targets
- `crates/notify`
- Role: notification dispatch, runtime facade, notifier implementations.
- Logging focus: target lifecycle, dispatch summaries, stream lag/backpressure, avoid per-event success spam.
- `crates/audit`
- Role: audit target fan-out and audit pipeline management.
- Logging focus: pipeline lifecycle, target availability, batch dispatch summaries, avoid noisy "started successfully" prose.
- `crates/targets`
- Role: target-specific configuration and utilities used by fan-out style systems.
- Logging focus: target selection, config validation, per-target degraded state.
- `crates/s3-types`
- Role: S3 event and type definitions.
- Logging focus: usually minimal; keep logging at integration boundaries rather than low-level type crates.
- `crates/s3-ops`
- Role: S3 operation definitions and mapping.
- Logging focus: mapping/contract failures, unsupported combinations, not normal-path request spam.
### Concurrency, Locking, And Runtime Foundations
- `crates/concurrency`
- Role: timeout, locking, backpressure, and I/O scheduling facade.
- Logging focus: lifecycle transitions and degraded states, not high-frequency worker/permit churn at `info`.
- `crates/lock`
- Role: distributed locking implementation.
- Logging focus: lock lifecycle, contention anomalies, lock ordering or timeout diagnostics.
- `crates/tls-runtime`
- Role: shared TLS runtime foundation.
- Logging focus: certificate lifecycle, reload/fallback, validation failures without sensitive dumps.
- `crates/obs`
- Role: observability helpers.
- Logging focus: this crate shapes other crates' telemetry conventions; avoid recursive or redundant summaries.
- `crates/io-core`
- Role: zero-copy I/O core primitives.
- Logging focus: keep very sparse; prefer metrics unless failures are actionable.
- `crates/io-metrics`
- Role: I/O metrics collection.
- Logging focus: typically minimal; metrics should carry the hot-path signal.
- `crates/rio`
- Role: Rust I/O utility layer.
- Logging focus: compatibility or runtime boundary failures, not fast-path internals.
- `crates/rio-v2`
- Role: next-generation I/O compatibility layer.
- Logging focus: migration/feature-mode differences and degraded fallback between I/O paths.
- `crates/utils`
- Role: shared helpers.
- Logging focus: usually avoid direct logging in generic helpers unless the helper is itself an operational boundary.
- `crates/common`
- Role: shared data structures and helpers.
- Logging focus: same principle as `utils`; prefer callers to log context-rich events.
- `crates/config`
- Role: configuration management.
- Logging focus: config source, fallback, validation, and summary aggregation; avoid dumping merged configs.
- `crates/data-usage`
- Role: shared data usage models and algorithms.
- Logging focus: refresh lifecycle, summary stats, and degraded reads.
### Schema, Contracts, And API Support
- `crates/protos`
- Role: protobuf definitions.
- Logging focus: usually none inside the crate; emit logs at decode/use boundaries.
- `crates/extension-schema`
- Role: extension schema contracts.
- Logging focus: schema validation and compatibility mismatches.
- `crates/s3select-api`
- Role: S3 Select API interfaces.
- Logging focus: request validation and unsupported feature boundaries.
- `crates/s3select-query`
- Role: S3 Select query engine.
- Logging focus: query parse/planning/execution failures, avoid row-level spam.
- `crates/protocols`
- Role: non-S3 protocol support.
- Logging focus: see core server section; keep per-request verbosity below `info`.
### Testing And Non-Production Crates
- `crates/e2e_test`
- Role: end-to-end tests.
- Logging focus: test clarity matters more than production governance, but avoid copying test-only logging style into production crates.
## Current Guardrail Coverage Map
`scripts/check_logging_guardrails.sh` currently enforces retired patterns in these high-signal areas:
- `rustfs/src/main.rs`
- `rustfs/src/startup_iam.rs`
- `rustfs/src/auth.rs`
- `rustfs/src/protocols/client.rs`
- `crates/audit/src/pipeline.rs`
- `crates/audit/src/system.rs`
- `crates/audit/src/global.rs`
- `crates/notify/src/config_manager.rs`
- `crates/notify/src/runtime_facade.rs`
- `crates/notify/src/notifier.rs`
- `crates/ecstore/src/store/peer.rs`
- `crates/ecstore/src/store/init.rs`
- `crates/ecstore/src/tier/tier.rs`
- `crates/concurrency/src/workers.rs`
- `crates/concurrency/src/manager.rs`
- `crates/concurrency/src/lock.rs`
- `crates/concurrency/src/deadlock.rs`
- `crates/trusted-proxies/src/global.rs`
- `crates/trusted-proxies/src/config/loader.rs`
- `crates/trusted-proxies/src/proxy/metrics.rs`
- `crates/trusted-proxies/src/proxy/validator.rs`
- `crates/trusted-proxies/src/proxy/chain.rs`
- `crates/trusted-proxies/src/middleware/service.rs`
- `crates/trusted-proxies/src/cloud/detector.rs`
- `crates/trusted-proxies/src/cloud/ranges.rs`
- `crates/trusted-proxies/src/cloud/metadata/aws.rs`
- `crates/trusted-proxies/src/cloud/metadata/azure.rs`
- `crates/trusted-proxies/src/cloud/metadata/gcp.rs`
When expanding coverage, prefer crates with:
- repeated sentence-style lifecycle logs
- high-frequency success-path `info!`
- startup/config checklist banners
- security-sensitive fallback wording
- external fetch/retry/fallback flows
That typically means the next broad candidates are `rustfs`, `crates/notify`, `crates/audit`, `crates/targets`, `crates/heal`, and `crates/scanner`.
## Event Model
Prefer this structure when the fields are available:
- `event`
- `component`
- `subsystem`
- `state` or `result`
- stable context fields such as:
- `enabled`
- `implementation`
- `validation_mode`
- `peer_ip`
- `client_ip`
- `proxy_hops`
- `duration_ms`
- `fallback`
- `reason`
- `range_count`
- `hold_time_ms`
- `available_slots`
- `total_slots`
- `permits_in_use`
## Level Policy
- `error`: the operation fails and callers or security guarantees are affected.
- `warn`: a degraded path, fallback, suspicious request, or operator-actionable config issue occurs.
- `info`: a low-frequency lifecycle or mode transition occurs.
- `debug`: useful diagnostics exist but normal operators do not need them all the time.
- `trace`: hot-path and repetitive success-path details occur.
## Preferred Patterns
- Use a short message label:
- `"trusted proxy validation failed"`
- `"concurrency manager state changed"`
- `"trusted proxy cloud metadata loaded"`
- Put key meaning into fields, not only the message text.
- Aggregate config or metrics summaries into one log event.
## Retired Patterns
These should usually be removed or replaced:
- Sentence-style lifecycle logs:
- `info!("Concurrency manager stopped")`
- `info!("Trusted Proxies module initialized")`
- Checklist or banner logs:
- `info!("=== Application Configuration ===")`
- `info!("Available metrics:")`
- Hot-path noise:
- `info!("worker take, {}", *available)`
- `debug!("Proxy validation successful in {:?}", duration)`
- Legacy fallback prose:
- `"Request from private network but not trusted: ..."`
- `"Cloud metadata fetching is disabled"`
## Guardrail Update Checklist
When extending `scripts/check_logging_guardrails.sh`:
1. Add the touched files to `checked_files`.
2. Add only legacy patterns that have been intentionally retired.
3. Keep patterns literal and grep-friendly.
4. Run the guardrail script after changes.
5. Avoid adding patterns for logs that are still valid elsewhere in the repo.
## Validation Checklist
For logging-only changes:
```bash
cargo fmt --all --check
./scripts/check_logging_guardrails.sh
cargo check -p <affected-crate>
cargo test -p <affected-crate>
```
For broader Rust changes:
```bash
./scripts/check_unsafe_code_allowances.sh
./scripts/check_architecture_migration_rules.sh
cargo clippy -p <affected-crates> --all-targets -- -D warnings
```
@@ -0,0 +1,169 @@
---
name: rustfs-release-publish
description: "End-to-end RustFS release pipeline: bump version files on main directly to the final target version, cut a preview tag on that commit, verify the CI build and release artifacts, run the downloaded binary locally and exercise the console, validate the server with the latest rc client, then publish the final tag on the SAME validated commit — never a new bump commit, never latest main. Use whenever the user wants to release/publish a RustFS version (发版/发布)."
---
# RustFS Release Publish (preview-validated pipeline)
This skill orchestrates a full release. It wraps `rustfs-release-version-bump` (which only edits version files and opens the PR) with a mandatory preview-tag validation loop before the final tag is published.
Core design: **version files never carry a `-preview.N` suffix**. The preview suffix exists only in tag names. This works because the binary self-reports the git tag it was built from (`build::TAG` via shadow_rs, see `rustfs/src/config/cli.rs` `SHORT_VERSION`), and `build.yml` derives artifact names and prerelease classification from the tag name — Cargo.toml's version is only a no-tag fallback. Therefore the preview tag and the final tag can (and MUST) point at the exact same commit: what you validated is byte-for-byte the source that ships.
Pipeline shape:
```
bump version files to <target> (final version, ONE commit) -> merge
-> tag <preview-tag> at that commit -> CI green
-> verify release artifacts -> run binary locally + console checks
-> validate with latest rc client
-> tag <target> at the SAME commit (zero delta) -> re-verify CI/release
```
On validation failure: fix lands on main via normal PR (version files are already at `<target>`, no new bump PR), then tag `<preview-tag N+1>` at the new main commit and restart from Phase 2.
## Required inputs
- Final target version, for example `1.0.0-beta.10`.
- Preview iteration `N` (default: next unused preview tag for that target; check with `git tag -l '<target>-preview.*'` — and for stable targets `git tag -l '<target>-rc.*'` — after `git fetch --tags`).
If the target version is missing or ambiguous, stop and ask before doing anything (see the semver gate below).
## Semver gate — confirm the target version before touching anything
Versions follow [SemVer 2.0.0](https://semver.org/). Precedence reminder:
```
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 < 1.0.1 < 1.1.0 < 2.0.0
```
Numeric prerelease identifiers compare numerically (`beta.9 < beta.10`), not lexically — see [semver.org spec item 11](https://semver.org/#spec-item-11). Preview tags are internal validation tags layered on top of the target's prerelease channel — they are never themselves a deliverable version and never appear in version files.
Rules:
- A request like "发个版" / "release the next version" without an exact version string is ALWAYS ambiguous. Derive the current latest tag (`git tag --sort=-v:refname | head`), then ask the user to choose via AskUserQuestion with concrete candidates, e.g. from `1.0.0-beta.10`: next prerelease `1.0.0-beta.11`, promote to `1.0.0-rc.1`, promote to stable `1.0.0`. Never guess between these — they have very different meanings (channel promotion vs. iteration) and different CI classification consequences.
- After a stable `X.Y.Z` exists, the next version must state which component bumps: patch `X.Y.(Z+1)` for fixes only, minor `X.(Y+1).0` for backward-compatible features, major `(X+1).0.0` for breaking changes. If the user names a bump type but not a number, compute it from the latest stable tag and echo the exact resulting version back for confirmation.
- Echo the final confirmed version string verbatim in your first status report; every later phase must use exactly that string. If at any point the user's wording and the confirmed version diverge, stop and re-confirm.
## Preview tag naming
- Prerelease target (contains `alpha`/`beta`/`rc`): preview tag is `<target>-preview.N`, e.g. `1.0.0-beta.10-preview.3`. It contains `beta`, so `build.yml`'s substring-based classification marks it prerelease — safe.
- **Stable** target (e.g. `1.1.0`): NEVER tag `1.1.0-preview.N``build.yml` marks a tag prerelease only if its name contains `alpha`, `beta`, or `rc`, so `1.1.0-preview.N` would be treated as a stable release and overwrite `latest.json` as stable. Use `1.1.0-rc.N` as the preview tag instead.
## Hard rules
- Version files (Cargo.toml, Cargo.lock, README, flake.nix, Chart.yaml, rustfs.spec) are bumped ONCE, directly to `<target>`. Never write a `-preview.N` suffix into any version file. If `rustfs-release-version-bump` is ever asked for a `-preview` version, that is a pipeline bug — stop.
- Tags have no `v` prefix. Always annotated: `git tag -a <tag> -m "Release <tag>"`.
- The final tag MUST point at exactly `PREVIEW_HASH` — the commit the validated preview tag points at. Never tag current `main` HEAD (commits merged after validation are unvalidated), and never create an extra version-bump commit between preview and final.
- Phases run in order; a failure in any phase blocks everything after it. After the fix lands on main, restart from Phase 2 with the next preview iteration against the new `origin/main` hash — do not resume mid-pipeline against a stale hash.
- If the release is abandoned after Phase 1 merged, main's version files claim a version that was never tagged. Either revert the bump PR or leave it to be overwritten by the next release — but tell the user explicitly and record the decision.
- User-facing status updates in Chinese; commits, PR titles/bodies, and tag messages in English. No hard-wrapping in commit messages, PR bodies, or documentation prose — one logical line per sentence/paragraph, let soft wrap handle display.
## Phase 0 — Preflight
- `git status --short` clean; `git fetch origin main --tags`.
- `gh auth status` works; confirm you can view `gh release list -L 3`.
- Confirm the exact final target version with the user if not explicit.
## Phase 1 — Version bump to the final target (once)
- If main's version files already read `<target>` (e.g. this is a restart after a failed preview), verify with `rg -n "<target>" Cargo.toml rustfs.spec helm/rustfs/Chart.yaml` and skip to Phase 2.
- Otherwise invoke the `rustfs-release-version-bump` skill with the final `<target>` (NOT a preview version), full GitHub flow (commit/push/PR).
- Get the PR merged into main. Record the resulting main commit:
```bash
git fetch origin main
PREVIEW_HASH=$(git rev-parse origin/main) # must contain the bump PR
```
`PREVIEW_HASH` is the single source of truth for the rest of the pipeline — report it to the user and reuse it verbatim in Phases 2 and 6. Both the preview tag and the final tag will point at it.
## Phase 2 — Publish the preview tag
```bash
git tag -a "<preview-tag>" -m "Release <preview-tag>" "$PREVIEW_HASH"
git push origin "<preview-tag>"
```
Pushing the tag triggers `.github/workflows/build.yml` ("Build and Release"); `docker.yml` chains off it via `workflow_run`.
On a restart (N+1), refresh `PREVIEW_HASH=$(git rev-parse origin/main)` first — it must contain the fix — and re-report it.
## Phase 3 — CI and artifact verification
- Watch the tag build: `gh run list --workflow build.yml --limit 5` then `gh run watch <run-id>`. Every matrix target must succeed (linux x86_64/aarch64 × musl/gnu, macos-aarch64, windows-x86_64) plus the release and latest.json jobs.
- Verify the GitHub release: `gh release view "<preview-tag>" --json isPrerelease,assets`
- `isPrerelease` must be `true`.
- Assets must include all 6 platform zips in both versioned (`rustfs-<platform>-v<tag>.zip`) and `-latest` forms, plus `SHA256SUMS`, `SHA512SUMS`, `rustfs-<tag>.sbom.cdx.json`, `rustfs-<tag>.provenance.json`.
- Verify the chained Docker run succeeded: `gh run list --workflow docker.yml --limit 3`.
- Checksum spot-check for the platform you will run locally: download the zip and `SHA256SUMS`, verify with `shasum -a 256 -c` (grep to one line).
## Phase 4 — Run the artifact locally, verify the console
Work inside the session scratchpad directory; never leave stray data dirs.
```bash
gh release download "<preview-tag>" -p "rustfs-macos-aarch64-v<preview-tag>.zip" -D "$SCRATCH"
cd "$SCRATCH" && unzip -o rustfs-*.zip
./rustfs --version # must report the PREVIEW TAG (build::TAG), not the Cargo.toml version, plus expected short SHA
mkdir -p data
RUSTFS_ACCESS_KEY=rustfsadmin RUSTFS_SECRET_KEY=rustfsadmin ./rustfs ./data
```
Defaults: S3 endpoint `:9000`, embedded console `:9001`.
Checks (all must pass):
- `./rustfs --version` reports the preview tag name and the short SHA of `PREVIEW_HASH`. Reporting `<target>` without the `-preview.N` suffix means the build did not embed the tag — treat as FAIL and investigate before proceeding.
- `curl -fsS http://localhost:9000/health/ready` returns ready.
- Startup log shows the embedded console being served (this was the regression that `fix(release): require embedded console assets` guards).
- Open `http://localhost:9001` in the browser: login with `rustfsadmin`/`rustfsadmin`; dashboard renders without JS console errors; create a bucket, upload a file, download it back (byte-identical), delete the object and bucket. Keep the server running for Phase 5.
## Phase 5 — Validate with the latest rc client
`rc` is the RustFS CLI client from <https://github.com/rustfs/cli>.
- Ensure the latest release is installed: compare `rc --version` against `gh api repos/rustfs/cli/releases/latest --jq .tag_name`; update via `brew upgrade rustfs/tap/rc` (or download the release binary).
- Point it at the preview server and run the command matrix, recording PASS/FAIL per command:
```bash
rc alias set preview http://localhost:9000 rustfsadmin rustfsadmin
rc ls preview/
rc mb preview/rel-check
rc cp <local-file> preview/rel-check/
rc stat preview/rel-check/<file>
rc cat preview/rel-check/<file> # matches source
rc cp preview/rel-check/<file> ./out && cmp <local-file> ./out
rc cp -r <local-dir>/ preview/rel-check/dir/
rc find preview/rel-check --name "*"
rc share download preview/rel-check/<file> --expire 1h # presigned URL fetchable via curl
rc rm preview/rel-check/<file> && rc rm -r --force preview/rel-check/dir
rc rb preview/rel-check
rc admin user list preview/
rc admin user add preview/ relcheckuser relchecksecret12
rc admin user remove preview/ relcheckuser
rc alias remove preview
```
- Any FAIL blocks the release. Afterwards stop the server and delete the scratch data directory.
## Phase 6 — Publish the final tag on the validated commit
No second version bump, no release branch. The final tag goes on the exact commit the preview validated:
```bash
git fetch origin --tags
git rev-parse "<preview-tag>^{commit}" # must equal PREVIEW_HASH — abort if not
git tag -a "<target>" -m "Release <target>" "$PREVIEW_HASH"
git push origin "<target>"
```
- CI rebuilds from the same source; the only changed input is the tag name, so the binary now self-reports `<target>`.
- Re-run the Phase 3 verification against the final tag: all matrix jobs green; `gh release view "<target>"` shows the full asset set; for a prerelease target `isPrerelease` is `true`, for a stable target it must be `false` and `latest.json` must be updated.
- Optionally spot-check `./rustfs --version` from a final-tag artifact — it must report `<target>`.
## Output contract
Always report:
- Target version, preview tag(s) used, `PREVIEW_HASH` (which both tags point at).
- Per-phase result (PASS/FAIL/BLOCKED) with key evidence: CI run URLs, release URLs, console check results, the rc command matrix.
- Any deviation from this pipeline and why the user approved it.
@@ -0,0 +1,126 @@
---
name: rustfs-release-version-bump
description: "Publish a RustFS alpha/beta/stable release with an auditable flow: confirm target version and scope, update workspace and release assets (including strict rustfs.spec changelog identity/date/version format), run required verification, and finish with commit, push, and GitHub PR creation."
---
# RustFS Release Version Bump
Use this skill to publish a RustFS release (alpha, beta, or stable) with a minimal, auditable diff and a complete ship flow (`edit -> verify -> commit -> push -> PR`).
Validated baseline: release pattern used in PR `#2957`.
## Required inputs
- Exact target version, for example `1.0.0-beta.4`.
- Delivery scope:
- Local only (`edit/verify`).
- Local + git (`commit/push`).
- Full GitHub flow (`commit/push/PR`).
If target version is missing or ambiguous, stop and ask before editing.
Reject any target version containing a `-preview.` suffix: preview identifiers are tag-only (see `rustfs-release-publish`) and must never be written into version files. If asked for one, stop and point to the release pipeline instead of editing.
## Read before editing
- `AGENTS.md` (root and nearest path-specific files).
- `.github/pull_request_template.md`.
- Current branch status and diff against `origin/main`.
## Default release file scope
Treat the following file list as the default checklist for each release bump:
- `Cargo.toml`
- `Cargo.lock`
- `README.md`
- `README_ZH.md`
- `flake.nix`
- `helm/rustfs/Chart.yaml`
- `rustfs.spec`
Only drop a file when the current repository release process clearly no longer requires it.
## Hard release policy
- Docker doc tags use `<version>` (for example `rustfs/rustfs:1.0.0-beta.4`), not `v<version>`.
- Helm chart version mapping follows `beta.N -> 0.N.0`.
- `rustfs.spec` `Release` uses prerelease suffix only (for example `beta.4`).
- Do not change these rules without explicit confirmation.
## Step-by-step workflow
1. Confirm intent and isolate scope
- Confirm target version string exactly.
- Confirm whether user requested local-only or full GitHub flow.
- Inspect current branch and ensure only release-related files are touched for this task.
2. Update workspace versions
- Bump `[workspace.package].version` in `Cargo.toml`.
- Bump internal workspace crate dependency versions in `Cargo.toml`.
- Update `Cargo.lock` so workspace package versions match target version.
- Re-scan for partial leftovers.
3. Update release assets
- `README.md` and `README_ZH.md`: update versioned Docker examples to target version.
- `flake.nix`: update package version to target version.
- `helm/rustfs/Chart.yaml`:
- `appVersion` = target version.
- `version` follows chart mapping rule, for example:
- `1.0.0-beta.3` -> `0.3.0`
- `1.0.0-beta.4` -> `0.4.0`
- `rustfs.spec`:
- Set `Release` to prerelease suffix (example `beta.4`).
- Add/update top changelog entry with exact format:
- `* Thu May 20 2026 houseme <housemecn@gmail.com>`
- `- Update RPM package to RustFS 1.0.0-beta.4`
- Changelog identity and time must come from current environment:
- `git config --get user.name`
- `git config --get user.email`
- `date '+%a %b %d %Y'`
- Changelog version text must match target release version exactly.
4. Verify before shipping
- Run:
- `cargo fmt --all`
- `cargo fmt --all --check`
- `make pre-commit`
- If verification passes, run `cargo clean`.
- If `make pre-commit` fails, return `BLOCKED` with root cause and do not silently widen scope to fix unrelated issues unless user asks.
5. Commit strategy
- Preferred split when both parts changed:
- `chore(release): prepare <version>` for `Cargo.toml` and `Cargo.lock`.
- `chore(release): align release assets for <version>` for docs and packaging files.
- If user asks for one commit, use one commit.
- Stage only intended release files; do not include unrelated working tree changes.
6. Push and PR
- Push branch:
- `git push -u origin <branch>` (first push), or `git push` (tracking already exists).
- Create PR with template headings unchanged:
- `gh pr create --base main --head <branch> --title ... --body-file ...`
- PR title/body must be English.
- Use `N/A` for non-applicable template sections.
- Include verification commands and any `BLOCKED` reason clearly.
## Recommended check commands
- `git status --short --branch`
- `git diff --name-only origin/main...HEAD`
- `git diff --stat origin/main...HEAD`
- `rg -n "<old_version>|<new_version>" Cargo.toml Cargo.lock README.md README_ZH.md flake.nix helm/rustfs/Chart.yaml rustfs.spec`
- `cargo fmt --all`
- `cargo fmt --all --check`
- `make pre-commit`
- `cargo clean`
## Output contract
When using this skill, always report:
- Target version.
- Files changed.
- Any assumptions or uncertainties requiring confirmation.
- Verification result (`PASSED` or `BLOCKED`) with key evidence.
- Commit message(s) used.
- Push status and PR URL when GitHub flow is requested.
@@ -0,0 +1,4 @@
interface:
display_name: "RustFS Release Bump"
short_description: "Prepare RustFS release branches like PR #2957."
default_prompt: "Use $rustfs-release-version-bump to prepare a RustFS release version, ask about any unclear version policy, and finish the commit/push/PR flow."
@@ -0,0 +1,149 @@
---
name: security-advisory-lessons
description: Apply RustFS security lessons distilled from repository GitHub Security Advisories. Use when making or reviewing RustFS code changes, doing security checks, handling PR review for auth/authz, IAM, storage, RPC, logging, CORS, console/browser, encryption, policy, or endpoint changes, and when deciding which security regression tests are required.
---
# RustFS Security Advisory Lessons
Use this skill as a RustFS-specific security lens before changing or approving code. For the distilled advisory lessons and review patterns, read [advisory-patterns.md](references/advisory-patterns.md).
When currentness matters, fetch the live advisory inventory instead of relying on this skill as a status mirror:
```bash
gh api repos/rustfs/rustfs/security-advisories --paginate \
--jq '.[] | {ghsa_id,state,severity,summary,updated_at}'
```
Fetch full advisory details only when the live summary suggests a new or changed lesson:
```bash
gh api repos/rustfs/rustfs/security-advisories/<GHSA_ID>
```
For the full pattern map, read [advisory-patterns.md](references/advisory-patterns.md).
## Workflow
### 1. Scope the change
- Identify touched routes, protocol frontends, handlers, storage paths, credentials, logs, browser surfaces, CI/release code, and policy checks.
- Treat these paths as security-sensitive by default: `rustfs/src/admin/`, `rustfs/src/storage/`, `rustfs/src/auth.rs`, `rustfs/src/server/layer.rs`, `crates/iam/`, `crates/policy/`, `crates/credentials/`, `crates/ecstore/src/rpc/`, `crates/protocols/`, `crates/rio/`, OIDC/STS federation code, and console preview/auth code.
### 2. Map to advisory classes
- Read [advisory-patterns.md](references/advisory-patterns.md) for matching GHSA lessons.
- Do not rely on advisory titles alone. Confirm whether the issue is authentication, authorization, input validation, storage invariant, browser isolation, logging, or operational hardening.
### 3. Verify fail-closed behavior
- Check that unauthenticated, wrong-permission, cross-user, cross-bucket, malformed-input, and default-config cases fail explicitly.
- Prefer exact action/permission checks over broad helper calls or inferred ownership.
- Confirm lower storage/RPC layers do not bypass checks done in upper layers.
### 4. Require regression evidence
- For behavior changes, add focused negative tests that reproduce the advisory class.
- For sensitive fixes, include tests for the bypass form, not only the happy path.
- If a test is impractical, explain the residual risk and provide a manual verification command.
### 5. Report clearly
- Lead with concrete findings and file/line evidence.
- Separate proven vulnerabilities from hardening risks.
- Avoid exaggerating unauthenticated impact when the code actually rejects unauthenticated requests but allows a low-privileged authenticated bypass.
## Advisory-Derived Guardrails
### Auth and admin authorization
- Every admin or diagnostic route needs an explicit authn and authz story. Route registration, router whitelist, and handler-level authorization must agree.
- Match the admin action to the operation exactly. Copy-paste action constants are a known RustFS vulnerability class.
- Avoid authentication-only helpers for state-changing admin APIs; use `validate_admin_request` or the established equivalent with the right `AdminAction`.
- Read-only admin APIs such as metrics, server info, and diagnostics still require admin authorization; checking only that credentials exist is not enough.
- Replication admin reads can expose remote target credentials; list/get target endpoints require replication/admin authorization and must not return secrets to low-privilege callers.
- Do not assume admin-action `Resource` scoping constrains blast radius unless the policy engine actually enforces resources for that action.
### IAM and service accounts
- Treat imported IAM payload fields as attacker-controlled: `parent`, `claims`, `accessKey`, `secretKey`, status, policy names, and groups.
- For service account create/update/import, prove parent ownership or root/admin authority before writing credentials or claims; an action permission alone must not allow choosing root or another user as `target_user`.
- Treat IAM export packages as credential disclosure surfaces; never include plaintext user or service-account secret keys unless the caller is allowed to recover those secrets and the export format is intentionally sealed.
- Do not let `deny_only` or "no explicit deny" become an allow decision that skips required allow checks.
- Test cross-user list/update/import flows with wrong, correct, self, parent, and root identities.
### STS, OIDC, and federation flows
- Every STS endpoint must have an explicit authentication story: SigV4 where required, OIDC token verification for web identity, and role/session policy validation before issuing credentials.
- JWT session tokens must be signed and verified by a trusted issuer/key path, not by service-account-controlled material or a reused root secret.
- JWT verification must enforce required claims and expiration for every bearer token path; "allow missing exp" is never acceptable for user-presented credentials.
- Public OIDC bootstrap and callback routes must treat `Host`, `X-Forwarded-Proto`, redirect targets, `state`, and callback parameters as untrusted; credential-bearing redirects require a configured, allowlisted origin.
- OIDC discovery and validation URLs are SSRF sinks. Resolve and classify hostnames at connection time, reject rebinding to loopback/private/link-local ranges, and do not rely on literal string checks.
### S3 copy, multipart, and presigned POST
- Multipart copy must enforce source `GetObject` and destination `PutObject` semantics equivalent to `CopyObject`, including copy-source and policy conditions.
- Do not let `CreateMultipartUpload`, `UploadPartCopy`, `CompleteMultipartUpload`, or `AbortMultipartUpload` return success without authorization.
- Presigned POST policies are server-side contracts. Enforce `content-length-range`, key prefix, exact metadata/content-type, and all signed policy conditions.
### Protocol frontends and IAM parity
- FTP/FTPS, SFTP, gateway, and other protocol drivers must enforce IAM per operation before calling storage backends; authentication to a protocol listener is not authorization.
- Match protocol commands to the same S3 actions as HTTP, such as `RETR` to `GetObject`, `SIZE`/`MDTM` to `HeadObject`, `MKD` to `CreateBucket`, and bucket probes to `ListBucket` or `HeadBucket`.
- Review every handler in a protocol driver, not only the changed handler, because RustFS advisories show mixed guarded and unguarded siblings in the same driver.
- Regression tests for protocol frontends should deny the shared authorization hook and prove the backend is not reached for the denied command.
- Compare protocol secrets in constant time, normalize invalid-user and invalid-secret failures where practical, and add rate limiting before exposing password-style protocol endpoints.
### Paths, object keys, and filesystem access
- Never join untrusted bucket/object/RPC path strings onto filesystem roots without normalization and boundary checks.
- Reject or safely handle `..`, absolute paths, URL-encoded traversal, platform separators, empty components, and paths that canonicalize outside the intended root.
- Validate both S3 object-key paths and internode/RPC disk paths; storage helpers can bypass S3 authorization if they trust already-parsed paths.
- Archive auto-extract paths are object keys too. Validate tar/zip entry names before IAM checks and before storage writes, and prove cleaned paths cannot cross bucket or prefix boundaries.
### Secrets, default credentials, and crypto
- Do not ship hard-coded shared tokens, HMAC secrets, private keys, or production test keys.
- Defaults for root credentials and internode/RPC auth must fail closed for network-reachable deployments or generate per-install random secrets; warnings alone are not a security boundary.
- Keep cryptographic roles separated: root S3 credentials, RPC HMAC keys, and STS/JWT signing keys must not be reused or deterministically derived from each other.
- License or token validation must use signatures with embedded public/verifying keys only; do not use private-key decryption as authenticity.
- Plan key rotation and key IDs when removing exposed keys.
### Logging and debug output
- Logs must never include access keys beyond safe identifiers, secret keys, session tokens, JWT claims, HMAC secrets, expected signatures, license secrets, or raw response bodies containing credentials.
- Treat `Debug` implementations, `?value` tracing, merged config dumps, and dependency-level HTTP body logging as leak surfaces.
- Add log-capture tests or targeted unit tests for redaction wrappers when changing credential structs or response bodies.
### RPC, parsing, and panic safety
- Treat all RPC payload bytes as attacker-controlled. Replace `unwrap`, `expect`, and panic-prone deserialization with typed errors.
- Malformed request tests should cover empty bytes, truncated MessagePack/protobuf, invalid enum values, stale timestamps, and invalid signatures.
- RPC authentication must be independently strong; do not depend on S3 admin credentials unless the fallback is explicit and safe.
- RPC signatures must bind the exact generated gRPC method path, timestamp, and request method. Service-prefix signatures must not authorize a different concrete NodeService call.
### Browser, CORS, and console surfaces
- Do not reflect arbitrary `Origin` while also allowing credentials. Default CORS should be no CORS unless explicitly configured.
- Do not render user-controlled object content in a same-origin iframe with console credentials available to JavaScript.
- Prefer origin separation for object preview/download, `nosniff`, CSP, strict content-type handling, and avoiding durable credentials in `localStorage`.
- Preview safety must be based on trusted content type and sandboxing, not object names or extensions such as `.pdf`.
- Console license/version-like metadata endpoints should expose only coarse public data unless authenticated, especially subject names and expiration timestamps.
### Profiling, debug, and health endpoints
- Profiling and debug endpoints are not health checks. They require admin auth, opt-in enablement, rate limiting, and safe responses.
- Do not return absolute filesystem paths or other deployment layout in unauthenticated or low-privilege responses.
- Ensure health endpoint allowlists cannot accidentally include expensive diagnostics.
### Trusted proxy and network identity
- Only honor `X-Forwarded-For` or `X-Real-IP` when the request came from a configured trusted proxy.
- Apply the same trusted-proxy rule to scheme and host derivation; direct clients must not control security-sensitive redirects through `Host`, `X-Forwarded-Host`, or `X-Forwarded-Proto`.
- Direct clients must use the socket peer address for `aws:SourceIp` and policy condition evaluation.
- Add tests for direct spoofed headers and trusted-proxy headers.
### SSE and storage invariants
- Encryption metadata is not proof that bytes were encrypted on disk.
- When touching reader/writer wrappers such as hashing, encryption, compression, or warp readers, verify wrapper order and inspect stored bytes in regression tests.
- Avoid helper shortcuts that unwrap nested readers and accidentally bypass encryption or integrity layers.
## Review Prompts
Use these prompts while reviewing a diff:
- Could a low-privileged authenticated user reach this path with the wrong action, parent, bucket, or source object?
- Does a non-HTTP protocol path call the same authorization boundary as the S3 API before touching storage?
- Does a public/default/empty config change security behavior from fail-closed to fail-open?
- Is any attacker-controlled value later used as a path, policy condition, credential identity, log field, URL, Origin, or response body?
- Does this response contain stored replication, remote target, or service credentials that need redaction or stricter authorization?
- Does an IAM export/import path expose or trust plaintext credential secrets beyond the caller's intended authority?
- Can this STS/OIDC path issue credentials without SigV4, trusted issuer validation, allowlisted redirects, or trusted-proxy host/scheme handling?
- Can a service-account or STS token omit `exp`, forge `sessionPolicy`, or use a principal-controlled key as signing authority?
- Does this outbound validation path resolve attacker-supplied hostnames and reject private, loopback, link-local, and rebound addresses at the actual connection boundary?
- Is an archive entry, object key, or policy resource normalized differently between authorization and storage?
- Is the same operation implemented in multiple paths, such as `CopyObject` vs `UploadPartCopy`, and do all paths enforce the same security contract?
- Does a preview or browser-surface fix preserve the original security invariant when adding alternate viewers or file-type detection?
- Does the test prove the exploit form is denied, or only that the intended form still works?
@@ -0,0 +1,4 @@
interface:
display_name: "Security Advisory Lessons"
short_description: "Apply advisory lessons in reviews."
default_prompt: "Review code changes against past RustFS security advisory lessons and report concrete risks, missing tests, and recommended fixes."
@@ -0,0 +1,129 @@
# RustFS Advisory Pattern Map
This file is a lesson map, not an advisory inventory mirror. It keeps durable security patterns distilled from RustFS GitHub Security Advisories.
When current advisory state, severity, URLs, or full text matters, fetch it live:
```bash
gh api repos/rustfs/rustfs/security-advisories --paginate \
--jq '.[] | {ghsa_id,state,severity,summary,updated_at}'
gh api repos/rustfs/rustfs/security-advisories/<GHSA_ID>
```
Update this file only when an advisory adds or changes a reusable lesson, affected surface, validation pattern, or regression-test expectation. Do not update it for state-only, URL-only, count-only, or timestamp-only changes.
## Pattern Index
### Admin authorization and route exposure
- `GHSA-pfcq-4gjr-6gjm`: notification target endpoints accepted authenticated users but skipped admin authorization. Lesson: distinguish authn from authz; admin target CRUD must call the operation-specific admin authorization path.
- `GHSA-mm2q-qcmx-gw4w`: `ListServiceAccount` used `UpdateServiceAccountAdminAction`, while update lacked target ownership checks. Lesson: exact action constants and ownership checks are both required; information disclosure can chain into secret rotation and takeover.
- `GHSA-vcwh-pff9-64cc`: `ImportIam` checked `ExportIAMAction` for an import/write operation. Lesson: every admin handler must authorize the action it actually performs.
- `GHSA-jqmc-mg33-v45g` and `GHSA-8784-9m7f-c6p6`: `/profile/cpu` and `/profile/memory` were whitelisted from auth and allowed expensive diagnostics plus path disclosure. Lesson: profiling/debug endpoints need admin auth, opt-in, rate limits, and non-sensitive responses.
- `GHSA-f5cv-v44x-2xgf`: `/rustfs/admin/v3/metrics` accepted any authenticated IAM user and skipped admin authorization. Lesson: read-only metrics and diagnostic admin endpoints still require an operation-specific admin action check.
- `GHSA-796f-j7xp-hwf4`: `/rustfs/admin/v3/list-remote-targets` checked only that credentials existed and leaked replication target credentials. Lesson: replication target reads are privileged admin operations, and stored remote credentials require strict authz plus response redaction review.
- `GHSA-xp32-gxq2-3v52`: console license metadata endpoint was public and exposed subject and expiration fields. Lesson: management metadata endpoints should require admin auth or return only coarse public status.
### IAM import, service accounts, and privilege boundaries
- `GHSA-566f-q62r-wcr8`: `ImportIam` accepted attacker-controlled service account `parent`, `claims`, `accessKey`, and `secretKey`, enabling persistent backdoor accounts under root. Lesson: imported IAM payloads are untrusted data and must be validated against privilege boundaries.
- `GHSA-3495-h8r9-gfqg`: `ExportIAM` wrote regular-user and service-account secret keys into exported ZIP data. Lesson: IAM export is a credential-disclosure boundary; redact, seal, or strictly justify every exported secret before treating export permission as safe.
- `GHSA-5354-r3w2-34m8`: `AddServiceAccount` checked `CreateServiceAccountAdminAction` but trusted caller-supplied `target_user`, allowing service accounts under the root parent. Lesson: service-account create paths must validate parent ownership or root/admin authority, not only the create action.
- `GHSA-xgr5-qc6w-vcg9`: `deny_only=true` skipped allow checks and let restricted service accounts mint unrestricted children. Lesson: deny-only logic must never become implicit allow for privilege creation.
- `GHSA-mm2q-qcmx-gw4w`: leaked service account access keys plus update-without-ownership formed an escalation chain. Lesson: service-account identifiers are security-sensitive because update APIs consume them.
### STS, OIDC, and federation flows
- `GHSA-5qfg-mf7r-jp3w` and `GHSA-3473-5353-xhwh`: `AssumeRoleWithWebIdentity` was reachable through unauthenticated `POST /` routing and could issue temporary credentials from crafted web identity input. Lesson: every STS route needs explicit SigV4 or trusted identity-provider validation before role assumption, and unauthenticated exemptions must be narrowed to the exact action with uniform failure responses.
- `GHSA-ccrv-v8v9-ch9q` and `GHSA-48rf-7j3q-3hfv`: service-account-controlled material could self-sign JWT session tokens with forged policy claims, and missing `exp` was accepted for service-account tokens. Lesson: session tokens must be signed by a trusted issuer/key path, enforce required claims and expiration, and reject self-signed or principal-controlled tokens.
- `GHSA-9pjf-w3c2-m32r`, `GHSA-4x2q-cpx9-9h26`, and `GHSA-xvpm-p3f7-34c3`: public OIDC authorize/callback flows trusted request `Host` or forwarded scheme when building credential-bearing redirects. Lesson: OIDC redirects must use configured allowlisted origins and trusted-proxy handling; never derive the post-login credential destination from direct client headers.
- `GHSA-m479-9x88-94w6`, `GHSA-frwq-mfqx-83p8`, `GHSA-q9q8-rf9r-fg9f`, and `GHSA-j5c2-hhf7-6gf5`: OIDC validation accepted attacker-controlled discovery URLs because hostname checks rejected only literal forbidden IPs, allowing DNS rebinding SSRF. Lesson: outbound federation URL validation must resolve and classify hostnames at the connection boundary and reject loopback, private, link-local, and rebound addresses.
### S3 copy, multipart, and upload policy validation
- `GHSA-mx42-j6wv-px98`: `UploadPartCopy` missed source authorization and allowed cross-bucket object exfiltration. Lesson: multipart copy must enforce the same source and destination contract as `CopyObject`.
- `GHSA-wfxj-ph3v-7mjf`: `UploadPartCopy` checked source and destination independently but missed destination copy-source policy constraints. Lesson: source read and destination write checks are not sufficient when policy constrains allowed copy sources.
- `GHSA-w5fh-f8xh-5x3p`: presigned POST accepted uploads without enforcing signed policy conditions. Lesson: parse and enforce all POST policy constraints server-side, including size, key prefix, and content type.
### Protocol frontends and IAM parity
- `GHSA-3g29-xff2-92vp`: FTP `RETR` and `SIZE`/`MDTM` read paths authenticated the user but skipped IAM before calling storage. Lesson: non-HTTP protocol frontends must enforce the same per-operation authorization as the S3 API before backend access.
- `GHSA-g3vq-vv42-f647`: FTPS `MKD` called `create_bucket` without checking `s3:CreateBucket`. Lesson: protocol command handlers need action-specific checks even when sibling handlers already authorize correctly.
- `GHSA-3p3x-734c-h5vx`: FTPS and WebDAV compared secret keys with early-return string equality, while FTPS also returned distinguishable invalid-user and invalid-password failures. Lesson: password-style protocol auth needs constant-time secret comparison, indistinguishable failures where practical, and rate limiting.
### Filesystem paths and object key traversal
- `GHSA-pq29-69jg-9mxc`: RPC `read_file_stream` joined untrusted paths under a volume directory without canonical boundary checks. Lesson: `PathBuf::join` plus length checks are not path security.
- `GHSA-8r6f-hmq2-28rg`: object keys containing traversal sequences bypassed bucket/object authorization when mapped to filesystem paths. Lesson: reject traversal at object-key parsing and verify final storage paths remain under the expected bucket/key root.
- `GHSA-f4vq-9ffr-m8m3`: Snowball auto-extract accepted archive entries such as `../victim-bucket/object`, authorized the raw attacker-bucket path, then storage path cleaning crossed bucket boundaries. Lesson: archive entries become object keys and need traversal rejection plus consistent authz/storage normalization before writes.
### Secrets, defaults, and cryptographic misuse
- `GHSA-j59h-h7q5-q348`, `GHSA-3wm5-wpm5-hmfm`, `GHSA-6wc8-xm48-qhmx`, `GHSA-9gf3-jx4p-4xxf`, and `GHSA-63xc-c3w3-m2cf`: RustFS shipped known default root credentials that could authenticate to S3, admin APIs, IAM, KMS, console, and token-signing surfaces. Lesson: root credentials must be operator-provided or generated per install; known defaults and warnings are not acceptable for network-reachable deployments.
- `GHSA-h956-rh7x-ppgj`: gRPC used the hard-coded token `rustfs rpc` on both client and server. Lesson: source-visible shared tokens are authentication bypasses.
- `GHSA-r5qv-rc46-hv8q`: internode RPC HMAC secret fell back to the public default `rustfsadmin`. Lesson: RPC/internode auth must fail closed instead of silently using public defaults.
- `GHSA-75fx-qg6f-8rm7` and `GHSA-68cw-96m3-h2cf`: internode RPC secrets were derivable from known root credentials, making raw storage RPC signatures forgeable when explicit RPC secrets were unset. Lesson: RPC auth keys must be independent random secrets, never derived from S3 root credentials, and raw storage RPC should not share the public S3 listener without an internode-only boundary.
- `GHSA-m77q-r63m-pj89`: STS JWTs used the root secret key as the shared token signing key, allowing token forgery when the root secret was known. Lesson: STS signing keys need key separation, rotation, and key IDs; do not reuse root credentials for JWT/HMAC signing.
- `GHSA-923g-jp7v-f97f`: license verification embedded a production RSA private key and used private-key decryption as authenticity. Lesson: ship verifying/public keys only and use real signature verification.
### Sensitive logging and debug output
- `GHSA-r54g-49rx-98cr`: STS credentials were logged at info level. Lesson: generated credentials must never be logged in plaintext.
- `GHSA-8cm2-h255-v749`: debug logs leaked session tokens, secret keys, JWT claims, and raw STS response bodies. Lesson: redaction must cover custom `Debug` implementations and dependency response-body logging.
- `GHSA-333v-68xh-8mmq`: invalid RPC signature logging included the shared HMAC secret and expected signature. Lesson: error paths often leak secrets; never log raw secrets or derived authenticators.
### RPC input validation and panic safety
- `GHSA-gw2x-q739-qhcr`: malformed gRPC `GetMetrics` payloads reached `unwrap()` on deserialization and caused remote DoS. Lesson: every network/RPC deserialization failure returns an error, not a panic.
- `GHSA-h956-rh7x-ppgj` and `GHSA-r5qv-rc46-hv8q`: weak RPC auth increased reachability of otherwise internal handlers. Lesson: panic bugs become more severe when internode auth is weak or defaulted.
- `GHSA-c667-rgrv-99vj`: NodeService authentication signed the service prefix instead of the concrete generated method path, so valid metadata for one RPC could be replayed to another method during the timestamp window. Lesson: RPC HMAC payloads must bind exact gRPC method path, HTTP method surrogate, timestamp, and secret.
### Browser, CORS, and console isolation
- `GHSA-v9fg-3cr2-277j`: object preview rendered attacker-controlled HTML in a same-origin iframe, exposing console credentials stored in `localStorage`. Lesson: user content must be origin-isolated from the console and protected with `nosniff`, CSP, and strict content-type handling.
- `GHSA-7gcx-wg4x-q9x6`: an incomplete preview fix reintroduced extension-based PDF detection and bypassed the sandboxed fallback for attacker-controlled content. Lesson: browser-surface fixes need regression tests for alternate viewers and file-type branches, and preview trust must come from validated content type plus sandboxing rather than object names.
- `GHSA-x5xv-223c-8vm7`: default CORS reflected arbitrary origins with credentials. Lesson: never combine reflected origins with `Access-Control-Allow-Credentials: true`; default should be fail-closed.
### Trusted proxy and source IP conditions
- `GHSA-fc6g-2gcp-2qrq`: `aws:SourceIp` trusted client-supplied `X-Forwarded-For` or `X-Real-IP`. Lesson: forwarded IP headers are valid only behind configured trusted proxies; direct clients use socket peer IP.
### SSE and on-disk storage invariants
- `GHSA-xrrf-67jm-3c2r`: SSE metadata reported encryption while reader composition bypassed `EncryptReader` and stored plaintext. Lesson: test actual bytes on disk and wrapper order, not only API metadata.
### Serde deserialization and input validation
- No `#[serde(deny_unknown_fields)]` found across the entire codebase. Lesson: all structs deserialized from untrusted input (S3 API XML/JSON, lifecycle rules, bucket policies, replication configs) should have `#[serde(deny_unknown_fields)]` to reject malformed or adversarial payloads.
- `#[serde(default)]` on security-critical fields silently accepts missing values as zero/empty. Lesson: when a field has security implications (retention days, permissions, limits), validate the deserialized value explicitly rather than relying on defaults.
- Integer fields deserialized from user input and cast with `as` (e.g., `i32 as u32`) can wrap negative values to large positives. Lesson: validate ranges before casting; use `try_into()` or clamp.
- XML config typos (e.g., `"NoncurentDays"` instead of `"NoncurrentDays"`) are silently accepted when `deny_unknown_fields` is absent. Lesson: strict deserialization prevents silent misconfiguration that could cause data loss or unexpected retention behavior.
## Useful Search Seeds
Use these targeted searches when a diff touches security-sensitive code:
```bash
rg -n "validate_admin_request|check_permissions|AdminAction::|deny_only|is_allowed" rustfs crates
rg -n "authorize_operation|FtpsDriver|SftpDriver|RETR|MKD|SIZE|MDTM|CreateBucket|GetObject|HeadObject" crates/protocols rustfs
rg -n "UploadPartCopy|upload_part_copy|CompleteMultipart|PostObject|content-length-range|starts-with" rustfs crates
rg -n "normalize_extract_entry_key|Snowball|auto-extract|PathBuf::join|canonicalize|\\.\\.|x-forwarded-for|x-real-ip|SourceIp" rustfs crates
rg -n "DEFAULT_SECRET|DEFAULT_ACCESS|TEST_PRIVATE_KEY|rustfs rpc|RUSTFS_RPC_SECRET" rustfs crates
rg -n "TONIC_RPC_PREFIX|verify_rpc_signature|check_auth|NodeServiceServer|x-rustfs-signature" rustfs crates
rg -n "debug!|trace!|info!|error!|\\?resp|\\?merged_config|session_token|secret_key" rustfs crates
rg -n "HashReader|EncryptReader|SSE|server-side encryption|Access-Control-Allow-Credentials|Origin" rustfs crates
rg -n "deny_unknown_fields|serde.default|as u32|as usize|as i32" rustfs crates
```
## Minimum Regression Test Expectations
- Authz fixes: include unauthenticated, valid low-privilege, wrong-action, correct-action, owner, non-owner, and root/admin cases as applicable.
- Protocol frontend authz fixes: include denied `RETR`, `SIZE`/`MDTM`, `MKD`, bucket probe, and sibling allowed-operation cases, and assert denied paths do not reach the storage backend.
- IAM fixes: include import/update/list service-account cases with attacker-controlled parent, claims, access key, secret key, and policy.
- Copy/upload fixes: include cross-bucket, cross-user, source-denied, destination-denied, copy-source-condition, and multipart completion cases.
- Path fixes: include encoded traversal, absolute path, nested traversal, archive entries with `..`, valid object keys that resemble traversal text but should be rejected, and canonical bucket/prefix boundary checks.
- Logging fixes: assert redacted output for structs and response bodies that may contain credentials.
- IAM export fixes: assert exported archives omit plaintext user and service-account secrets unless the format deliberately encrypts or seals them.
- RPC auth fixes: include captured metadata replay across two concrete methods, stale timestamps, wrong path, wrong method surrogate, wrong secret, and valid same-method calls.
- Browser/CORS fixes: assert no credentials on reflected/default origins, correct behavior for explicit allowlists, and no same-origin script execution for previewed object content.
- SSE fixes: inspect stored bytes and verify API metadata, read-back behavior, and on-disk ciphertext together.
+34
View File
@@ -0,0 +1,34 @@
---
name: tier-debug
description: Debug ILM tiering / lifecycle transition issues — NoSuchVersion on tier GET, restore failures, xl.meta inspection, remote-tier versionId tracing. Use when investigating tiered/transitioned objects, warm backends, or transition metadata.
---
# Tier / ILM Debugging
Full playbook: [docs/operations/tier-ilm-debugging.md](../../../docs/operations/tier-ilm-debugging.md)
— read it before changing tier code.
Quick moves:
```bash
# Inspect transition metadata on disk (one xl.meta per erasure shard disk)
cargo run -p rustfs-filemeta --example dump_fileinfo -- "/path/to/{bucket}/{object}/xl.meta"
# Trace what versionId is sent to the remote tier
RUST_LOG=rustfs_ecstore::bucket::lifecycle=debug ./target/debug/rustfs …
```
Interpretation:
- `transition_ver_id: <none>` → correct for an unversioned tier bucket; no
`versionId` must be sent on tier GET/DELETE.
- `transition_ver_id: 00000000-…` (nil) → corrupt legacy write-back; readers
must filter it out, never send it.
- Empty-string `transitioned-versionID` metadata under both
`x-rustfs-internal-*` and `x-minio-internal-*` keys → object went to an
unversioned tier bucket.
Code entry points: `crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs`
(ILM actions), `crates/ecstore/src/services/tier/` (warm backends),
`crates/filemeta/src/filemeta/version.rs` (metadata read/write + regression
tests).
+17 -10
View File
@@ -14,13 +14,20 @@
# RustFS Cargo configuration
# Enable tokio_unstable cfg for dial9-tokio-telemetry support
# This allows dial9 to hook into Tokio's internal runtime events
[build]
# Enable Tokio unstable features required by dial9-tokio-telemetry for runtime tracing.
# See: https://docs.rs/tokio/latest/tokio/#unstable-features
rustflags = ["--cfg", "tokio_unstable"]
# Enable frame pointers for CPU profiling (Linux only, optional but recommended)
# Uncomment the following line for better CPU profiling data
# rustflags = ["--cfg", "tokio_unstable", "-C", "force-frame-pointers=yes"]
# NOTE: `--cfg tokio_unstable` is deliberately NOT set here.
#
# It used to be a global `[build] rustflags` entry so that the (default-off)
# `dial9` telemetry feature could compile. That made every build depend on
# Tokio's unstable, non-semver API, and it broke silently whenever a caller
# exported their own RUSTFLAGS — an environment RUSTFLAGS replaces the value
# from this file rather than appending to it.
#
# The flag is now scoped to telemetry builds, which opt in explicitly:
#
# make build-profiling
# RUSTFLAGS="--cfg tokio_unstable" cargo build --features dial9
#
# `crates/obs/build.rs` fails the compile if the `dial9` feature is on without
# the flag, so the two can no longer drift apart unnoticed.
#
# For CPU profiling, add `-C force-frame-pointers=yes` to that RUSTFLAGS value.
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+2 -2
View File
@@ -9,7 +9,7 @@ build-docker: SOURCE_BUILD_CONTAINER_NAME = rustfs-$(BUILD_OS)-build
build-docker: BUILD_CMD = /root/.cargo/bin/cargo build --release --bin rustfs --target-dir /root/s3-rustfs/target/$(BUILD_OS)
build-docker: ## Build using Docker container # e.g (make build-docker BUILD_OS=ubuntu22.04)
@echo "🐳 Building RustFS using Docker ($(BUILD_OS))..."
$(DOCKER_CLI) buildx build -t $(SOURCE_BUILD_IMAGE_NAME) -f $(DOCKERFILE_SOURCE) .
$(DOCKER_CLI) buildx build -t $(SOURCE_BUILD_IMAGE_NAME) -f $(DOCKERFILE_SOURCE) --load .
$(DOCKER_CLI) run --rm --name $(SOURCE_BUILD_CONTAINER_NAME) -v $(shell pwd):/root/s3-rustfs -it $(SOURCE_BUILD_IMAGE_NAME) $(BUILD_CMD)
.PHONY: docker-inspect-multiarch
@@ -19,4 +19,4 @@ docker-inspect-multiarch: ## Check image architecture support
exit 1; \
fi
@echo "🔍 Inspecting multi-architecture image: $(IMAGE)"
docker buildx imagetools inspect $(IMAGE)
docker buildx imagetools inspect $(IMAGE)
+19
View File
@@ -35,6 +35,25 @@ build-gnu-arm64: ## Build aarch64 GNU version
./build-rustfs.sh --platform aarch64-unknown-linux-gnu
## —— Profiling build (dial9 Tokio runtime telemetry) ------------------------------------------
# dial9 hooks Tokio's unstable runtime instrumentation, so it needs
# `--cfg tokio_unstable`. That flag is deliberately absent from
# .cargo/config.toml: it is not free, and release binaries do not carry it.
# Setting RUSTFLAGS here replaces (never appends to) the config-file value, and
# crates/obs/build.rs fails the build if the feature and the flag disagree.
#
# There are no task-dump or S3-upload features — see the notes in
# crates/obs/Cargo.toml for why.
DIAL9_FEATURES ?= dial9
DIAL9_RUSTFLAGS ?= --cfg tokio_unstable
.PHONY: build-profiling
build-profiling: ## Build RustFS with dial9 Tokio runtime telemetry (diagnostic builds only)
@echo "🔬 Building RustFS with dial9 telemetry (features: $(DIAL9_FEATURES))..."
@echo "⚠️ Diagnostic build: telemetry writes trace segments to disk continuously."
RUSTFLAGS="$(DIAL9_RUSTFLAGS)" cargo build --release --bin rustfs --features $(DIAL9_FEATURES)
.PHONY: build-cross-all
build-cross-all: core-deps ## Build binaries for all architectures
@echo "🔧 Building all target architectures..."
+5 -3
View File
@@ -11,14 +11,16 @@ check-%:
# Warning-only check
# Checks for optional dependencies and issues a warning if not found
# (e.g., cargo-nextest for enhanced testing)
warn-%:
@command -v $* >/dev/null 2>&1 || { \
echo >&2 "⚠️ '$*' is not installed."; \
}
# For checking dependencies use check-<dep-name> or warn-<dep-name>
.PHONY: core-deps fmt-deps test-deps
#
# NOTE: cargo-nextest is a HARD dependency of `make test`, gated inside the
# test recipe itself (with a RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 escape hatch)
# rather than a warn-only prerequisite here — see .config/make/tests.mak.
.PHONY: core-deps fmt-deps
core-deps: check-cargo ## Check core dependencies
fmt-deps: check-rustfmt ## Check lint and formatting dependencies
test-deps: warn-cargo-nextest ## Check tests dependencies
+26
View File
@@ -0,0 +1,26 @@
## —— Coverage --------------------------------------------------------------------------------------
# Local equivalent of the weekly coverage workflow (.github/workflows/coverage.yml,
# backlog#1153 infra-5): same measurement scope (--workspace --exclude e2e_test,
# nextest `ci` profile) and the same per-crate table. Slow — the instrumented
# build cannot reuse your normal target cache and then runs the whole suite.
# Doctests are not measured (needs nightly). Outputs land in target/llvm-cov/.
.PHONY: coverage
coverage: core-deps ## Workspace line coverage (cargo-llvm-cov + nextest; slow, writes target/llvm-cov/)
@if ! command -v cargo-llvm-cov >/dev/null 2>&1; then \
echo >&2 "❌ cargo-llvm-cov is required for 'make coverage' but was not found."; \
echo >&2 " Install it with:"; \
echo >&2 " cargo install cargo-llvm-cov --locked"; \
echo >&2 " rustup component add llvm-tools-preview"; \
exit 1; \
fi
@if ! command -v cargo-nextest >/dev/null 2>&1; then \
echo >&2 "❌ cargo-nextest is required for 'make coverage' (see 'make test')."; \
echo >&2 " Install it with: cargo install cargo-nextest --locked"; \
exit 1; \
fi
NEXTEST_PROFILE=ci cargo llvm-cov nextest --workspace --exclude e2e_test --no-report
@mkdir -p target/llvm-cov
cargo llvm-cov report --lcov --output-path target/llvm-cov/lcov.info
cargo llvm-cov report --json --output-path target/llvm-cov/coverage.json
python3 scripts/coverage_per_crate.py target/llvm-cov/coverage.json
+1 -1
View File
@@ -4,7 +4,7 @@
.PHONY: help
help: ## Shows This Help Menu
echo -e "$$HEADER"
grep -E '(^[a-zA-Z0-9_-]+:.*?## .*$$)|(^## )' $(MAKEFILE_LIST) | sed 's/^[^:]*://g' | awk 'BEGIN {FS = ":.*?## | #"} ; {printf "${cyan}%-30s${reset} ${white}%s${reset} ${green}%s${reset}\n", $$1, $$2, $$3}' | sed -e 's/\[36m##/\n[32m##/'
grep -E '(^[a-zA-Z0-9_-]+:.*?## .*$$)|(^## )' $(MAKEFILE_LIST) | sed 's/^[^:]*://g' | awk 'BEGIN {FS = ":.*?## | #"} /^## / {printf "\n${green}%s${reset}\n", $$0; next} {printf "${cyan}%-30s${reset} ${white}%s${reset} ${green}%s${reset}\n", $$1, $$2, $$3}'
.PHONY: help-build
help-build: ## Shows RustFS build help
+50 -1
View File
@@ -1,5 +1,7 @@
## —— Code quality and Formatting ------------------------------------------------------------------
.NOTPARALLEL: fix
.PHONY: fmt
fmt: core-deps fmt-deps ## Format code
@echo "🔧 Formatting code..."
@@ -13,8 +15,55 @@ fmt-check: core-deps fmt-deps ## Check code formatting
.PHONY: clippy-check
clippy-check: core-deps ## Run clippy checks
@echo "🔍 Running clippy checks..."
cargo clippy --all-targets -- -D warnings
.PHONY: clippy-fix
clippy-fix: core-deps ## Apply clippy fixes
@echo "🔧 Applying clippy fixes..."
cargo clippy --fix --allow-dirty
cargo clippy --all-targets --all-features -- -D warnings
.PHONY: fix
fix: fmt clippy-fix ## Format code and apply clippy fixes
.PHONY: quick-check
quick-check: core-deps ## Run fast workspace compilation check
@echo "🔨 Running fast compilation check..."
cargo check --workspace --exclude e2e_test
.PHONY: unsafe-code-check
unsafe-code-check: ## Check unsafe_code allowances have SAFETY comments
@echo "🔒 Checking unsafe_code allowances..."
./scripts/check_unsafe_code_allowances.sh
.PHONY: architecture-migration-check
architecture-migration-check: ## Check architecture migration guardrails
@echo "🏗️ Checking architecture migration guardrails..."
./scripts/check_architecture_migration_rules.sh
.PHONY: logging-guardrails-check
logging-guardrails-check: ## Check logging guardrails for redaction and noise regressions
@echo "🪵 Checking logging guardrails..."
./scripts/check_logging_guardrails.sh
.PHONY: tokio-io-uring-check
tokio-io-uring-check: ## Check tokio io-uring runtime feature stays removed
@echo "🚫 Checking tokio io-uring feature guard..."
./scripts/check_no_tokio_io_uring.sh
.PHONY: extension-schema-check
extension-schema-check: ## Check extension-schema stays a lightweight contract crate
@echo "🧩 Checking extension schema boundaries..."
./scripts/check_extension_schema_boundaries.sh
.PHONY: body-cache-whitelist-check
body-cache-whitelist-check: ## Check the body-cache eligibility gate stays a fail-closed allow-list
@echo "🧱 Checking body-cache whitelist guard..."
./scripts/check_body_cache_whitelist.sh
.PHONY: log-analyzer-rules-check
log-analyzer-rules-check: core-deps ## Check log-analyzer rule anchors still exist verbatim in source
@echo "🩺 Checking log-analyzer rule anchors..."
./scripts/check_log_analyzer_rules.sh
.PHONY: compilation-check
compilation-check: core-deps ## Run compilation check
+22 -2
View File
@@ -1,11 +1,31 @@
## —— Pre Commit Checks ----------------------------------------------------------------------------
.NOTPARALLEL: pre-commit pre-pr dev-check
.PHONY: setup-hooks
setup-hooks: ## Set up git hooks
@echo "🔧 Setting up git hooks..."
chmod +x .git/hooks/pre-commit
@echo "✅ Git hooks setup complete!"
.PHONY: doc-paths-check
doc-paths-check: ## Check that instruction/architecture docs reference existing file paths
@echo "📄 Checking doc path references..."
./scripts/check_doc_paths.sh
.PHONY: planning-docs-check
planning-docs-check: ## Check that no planning-type documents are committed
@echo "📄 Checking for committed planning docs..."
./scripts/check_no_planning_docs.sh
.PHONY: pre-commit
pre-commit: fmt clippy-check compilation-check test ## Run pre-commit checks
@echo "✅ All pre-commit checks passed!"
pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check doc-paths-check planning-docs-check quick-check ## Run fast pre-commit checks without clippy/full tests
@echo "✅ All pre-commit checks passed!"
.PHONY: pre-pr
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check doc-paths-check planning-docs-check log-analyzer-rules-check clippy-check test ## Run full pre-PR checks with clippy and tests
@echo "✅ All pre-PR checks passed!"
.PHONY: dev-check
dev-check: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check doc-paths-check planning-docs-check quick-check ## Run fast local development checks
@echo "✅ Fast development checks passed!"
+56 -3
View File
@@ -2,14 +2,67 @@
TEST_THREADS ?= 1
# cargo-nextest is a HARD dependency of `make test`.
#
# nextest changes test semantics vs plain `cargo test`: it runs every test in
# its own process (so serial_test's #[serial] mutex does not serialize across
# tests) and it is the only runner that honours .config/nextest.toml
# [test-groups] (e.g. the ecstore-serial-flaky serialization guard). CI runs
# nextest (.github/actions/setup/action.yml installs it), so a silent fallback
# to `cargo test` would run with different serialization behaviour than CI and
# mask (or invent) flakes.
#
# Installing cargo-nextest:
# cargo install cargo-nextest --locked # from source
# # or a prebuilt binary (faster) via the taiki-e installer / get.nexte.st:
# # https://nexte.st/docs/installation/
#
# Escape hatch: set RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 to run the plain
# `cargo test` fallback anyway. Results are NOT authoritative — semantics
# differ from CI and .config/nextest.toml test-groups will NOT apply.
.PHONY: script-tests
script-tests: ## Run shell script tests
@echo "Running script tests..."
./scripts/test_build_rustfs_options.sh
./scripts/test_entrypoint_credentials.sh
bash -n ./scripts/validate_object_data_cache_cold_stampede.sh
python3 ./scripts/check_object_data_cache_follower_samples.py --self-test
./scripts/validate_object_data_cache_cold_stampede.sh --self-test
.PHONY: test
test: core-deps test-deps ## Run all tests
test: core-deps script-tests ## Run all tests (needs cargo-nextest; RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 to override)
@echo "🧪 Running tests..."
@if command -v cargo-nextest >/dev/null 2>&1; then \
cargo nextest run --all --exclude e2e_test; \
else \
echo " cargo-nextest not found; falling back to 'cargo test'"; \
elif [ "$${RUSTFS_ALLOW_CARGO_TEST_FALLBACK:-0}" = "1" ]; then \
echo >&2 " ============================================================================"; \
echo >&2 "⚠️ cargo-nextest NOT found — running the 'cargo test' fallback (opt-in)."; \
echo >&2 "⚠️ TEST SEMANTICS DIFFER FROM CI; results are NOT authoritative:"; \
echo >&2 "⚠️ * nextest runs each test in its own process; 'cargo test' does not,"; \
echo >&2 "⚠️ so serial_test #[serial] serialization behaves differently."; \
echo >&2 "⚠️ * .config/nextest.toml [test-groups] will NOT apply (e.g. the"; \
echo >&2 "⚠️ ecstore-serial-flaky group), so load-sensitive tests may flake here"; \
echo >&2 "⚠️ but pass on CI (or vice versa)."; \
echo >&2 "⚠️ Install cargo-nextest and re-run before trusting these results."; \
echo >&2 "⚠️ ============================================================================"; \
cargo test --workspace --exclude e2e_test -- --nocapture --test-threads="$(TEST_THREADS)"; \
else \
echo >&2 "❌ cargo-nextest is required for 'make test' but was not found."; \
echo >&2 ""; \
echo >&2 " RustFS tests run under cargo-nextest (process-per-test isolation)."; \
echo >&2 " CI runs nextest and .config/nextest.toml [test-groups] only take effect"; \
echo >&2 " under nextest. Plain 'cargo test' has different serialization semantics"; \
echo >&2 " and is NOT a faithful substitute."; \
echo >&2 ""; \
echo >&2 " Install it with either:"; \
echo >&2 " cargo install cargo-nextest --locked"; \
echo >&2 " or a prebuilt binary (faster) — see https://nexte.st/docs/installation/"; \
echo >&2 ""; \
echo >&2 " To run the plain 'cargo test' fallback anyway (results NOT authoritative;"; \
echo >&2 " serialization semantics differ from CI), re-run with:"; \
echo >&2 " RUSTFS_ALLOW_CARGO_TEST_FALLBACK=1 make test"; \
exit 1; \
fi
cargo test --all --doc
+10
View File
@@ -0,0 +1,10 @@
# Committed floor for the number of tests selected by the migration-critical
# CI gate (see scripts/check_migration_gate_count.sh, backlog#1153 infra-12).
#
# The floor equals the exact count of rustfs-ecstore --lib tests matching the
# gate filter (name substrings: data_movement, rebalance, decommission,
# source_cleanup, delete_marker) at the time this file was last updated.
# CI fails if the selected count drops below this number, so renames or
# removals that thin the gate must update this file in the same PR.
# Adding tests does not require a bump, but bumping keeps the guard tight.
571
+316
View File
@@ -0,0 +1,316 @@
# nextest configuration for RustFS.
#
# Serialize two known load-sensitive / global-state-sharing ecstore test groups
# so the full parallel nextest suite stops producing spurious failures
# (backlog #937). These tests pass in isolation but flake under the loaded
# parallel run for two distinct reasons:
#
# * store::bucket::tests::bucket_delete_* share process/global state (disk
# registry, lock client) and race make_bucket into InsufficientWriteQuorum
# when run concurrently with other ecstore tests.
# * bucket_lifecycle_ops::tests::concurrent_resend_same_part_commits_one_generation
# asserts a lock-acquire correctness property whose serialized cross-disk
# commits exceed the (already max'd, 60s) acquire deadline only when the
# suite saturates disk I/O.
#
# serial_test's #[serial] attribute does NOT serialize these across runs:
# nextest executes each test in its own process, where the in-process
# serial_test mutex has no effect. A nextest test-group with max-threads = 1 is
# the mechanism that actually serializes across nextest's process boundary.
#
# ---------------------------------------------------------------------------
# Profiles
# ---------------------------------------------------------------------------
# The `default` profile is what local `cargo nextest run` uses. It NEVER
# retries: a red test locally means a real failure to investigate, not noise to
# paper over. The `ci` profile (below) is the strict CI gate: global
# retries = 0 so a new race's first occurrence is never masked, plus a
# narrowly-scoped quarantine list (retries = 2) for tests with a tracked OPEN
# flake issue. Flake policy lives in docs/testing/README.md.
[test-groups]
ecstore-serial-flaky = { max-threads = 1 }
# Reliability / fault-injection e2e tests each spawn a single-node 4-disk RustFS
# server and manipulate its disk directories at runtime (crates/e2e_test:
# reliability_disk_fault_test, degraded_read_eof_regression_test / dist-13). They
# are correct in isolation but resource-heavy; serialize them under nextest's
# process boundary (serial_test's #[serial] does not cross it) so several 4-disk
# servers never run at once. ci-7's nightly picks these up via the e2e suite;
# they are deliberately NOT in the fast PR `e2e-smoke` filter.
e2e-reliability = { max-threads = 1 }
# --- default profile (local): serialize the flaky groups, never retry --------
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & (test(concurrent_resend_same_part_commits_one_generation) | test(/^store::bucket::tests::bucket_delete_(mark_delete|purge_removes|default_s3_delete)/))'
test-group = 'ecstore-serial-flaky'
# Serialize the multipart crash-consistency scenarios (dist-2, backlog#1150):
# each spawns a 4-disk hermetic erasure set and drives full staged-upload +
# commit + GET cycles — the same cross-disk-commit IO shape that made
# concurrent_resend load-sensitive. Preventive serialization only, no retries.
# The matching ci-profile override is after [profile.ci].
[[profile.default.overrides]]
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
test-group = 'ecstore-serial-flaky'
# Serialize the 4-disk reliability / degraded-read e2e tests (see the
# e2e-reliability test-group note above). The matching ci-profile override is at
# the end of the file, after [profile.ci] is declared.
[[profile.default.overrides]]
filter = 'package(e2e_test) & test(/^(reliability_disk_fault|degraded_read_eof_regression)_test::/)'
test-group = 'e2e-reliability'
# ---------------------------------------------------------------------------
# ci profile — the strict CI gate (ci.yml `cargo nextest run --profile ci`)
# ---------------------------------------------------------------------------
[profile.ci]
# Strict: a new race must fail on its first occurrence, never be retried away.
retries = 0
# Report every failure in one run instead of bailing on the first.
fail-fast = false
[profile.ci.junit]
# Emitted to target/nextest/ci/junit.xml; uploaded as a CI artifact.
# Tests that pass only after a quarantine retry are marked `flaky` here — that
# marker is the observable signal the flake policy is built around.
path = "junit.xml"
# ===========================================================================
# QUARANTINE — flaky tests granted retries = 2 under the ci profile ONLY.
#
# RULES (enforced by review, see docs/testing/README.md):
# * Every entry MUST link exactly one OPEN issue tracking the flake.
# * An entry stays until the issue is fixed (test made robust) or the test is
# deleted — 30-day policy. No entry may exist without a live issue link.
#
# Each entry also re-declares the `ecstore-serial-flaky` test-group so the
# serialization holds under the ci profile (nextest evaluates a named
# profile's own overrides list, not the default profile's).
# ===========================================================================
# QUARANTINE: OPEN backlog#937 — concurrent_resend lock-acquire deadline flakes
# under saturated disk I/O in the full parallel suite.
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(concurrent_resend_same_part_commits_one_generation)'
test-group = 'ecstore-serial-flaky'
retries = 2
# QUARANTINE: OPEN backlog#937 — store::bucket::tests::bucket_delete_* race
# make_bucket into InsufficientWriteQuorum via shared global state under load.
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(/^store::bucket::tests::bucket_delete_(mark_delete|purge_removes|default_s3_delete)/)'
test-group = 'ecstore-serial-flaky'
retries = 2
# QUARANTINE: OPEN rustfs#4690 — walk_dir stall-budget accounting test depends
# on producer/consumer timing windows that stretch past the budget on loaded
# CI runners (regression test for rustfs#4644; failed on a zero-Rust-diff PR).
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(walk_dir_does_not_charge_consumer_backpressure_to_the_stall_budget)'
retries = 2
# Serialize the 4-disk reliability / degraded-read e2e tests under the ci
# profile too (see the e2e-reliability test-group note near the top). Not a
# quarantine: no retries, just single-threaded so several 4-disk servers never
# run concurrently when ci-7's nightly runs the full e2e suite.
[[profile.ci.overrides]]
filter = 'package(e2e_test) & test(/^(reliability_disk_fault|degraded_read_eof_regression)_test::/)'
test-group = 'e2e-reliability'
# Serialize the multipart crash-consistency scenarios under the ci profile too
# (see the matching default-profile override near the top). Not a quarantine:
# no retries, just serialized 4-disk cross-disk-commit IO.
[[profile.ci.overrides]]
filter = 'package(rustfs-ecstore) & test(/^set_disk::ops::multipart::tests::crash_consistency::/)'
test-group = 'ecstore-serial-flaky'
# ---------------------------------------------------------------------------
# e2e-smoke profile — PR smoke subset of the e2e_test crate (backlog#1149 ci-4)
# ---------------------------------------------------------------------------
# PR smoke subset of the e2e_test crate (backlog#1149 ci-4). This profile is
# the single wiring mechanism for e2e tests in CI: other suites join by
# extending this filter (or a sibling profile), never by adding ad-hoc e2e
# jobs to ci.yml. Admission criteria (see crates/e2e_test/README.md): fast,
# single-node topology, no external dependencies (no awscurl / Vault / fixed
# ports / pre-started server), no #[ignore].
#
# Each e2e test spawns its own rustfs server on a random port with an isolated
# temp dir (crates/e2e_test/src/common.rs), so the subset is parallel-safe.
#
# Replication failure harness (backlog#1147 repl-8): the first clause admits
# its four in-process fake-target self-tests. They bind random loopback ports,
# use no external service, and finish in under a second.
#
# Replication PR subset (backlog#1147 repl-1): the second clause admits the 20
# FAST bucket-replication tests from replication_extension_test — the
# target-registration / replication-check / list / remove / delete admin paths
# that validate config synchronously and never wait for asynchronous
# replication convergence. Each spawns its own single-node rustfs server(s) on
# random ports (source, plus an independent single-node target for the pair
# checks — NOT a cluster), so the subset stays parallel-safe and single-digit
# seconds. The data-plane tests that poll for convergence and all
# `_real_dual_node` / `_real_single_node`
# site-replication tests run in the [profile.e2e-repl-nightly] lane below, NOT
# here. This allowlist is the single source of truth for the PR/nightly split:
# the nightly profile derives its set as "the replication module MINUS this
# allowlist", so any new replication test lands in nightly by default (never
# silently unrun) until it is explicitly blessed as fast here. Keep the two
# regexes byte-identical. Count invariant: 20 here + 27 nightly = 47 total
# (authority: `cargo nextest list`; docs/testing/e2e-suite-inventory.md).
# HISTORY (2026-07-11): the 20 fast tests were briefly pulled out of this lane
# (#4724) because they set a loopback (127.0.0.1) replication target that the
# SSRF egress guard rejected on every PR after repl-1 (#4712). That is fixed —
# the guard now honours an off-by-default opt-in and this suite's source servers
# set it (RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET) — so the allowlist below is
# restored.
#
# Security negative-auth subset (backlog#1151 sec-5): the three attacker-facing
# S3 auth-rejection suites join the first clause above by module name —
# presigned_negative (sec-2), negative_sigv4 (sec-1, header SigV4), and
# admin_auth (sec-4, admin gate + root-credential lifecycle). All three use
# RustFSTestEnvironment on a random port and are parallel-safe, so they meet the
# smoke admission criteria unchanged. This is the wiring step that makes those
# merged suites actually execute on every PR (they were dead until listed here).
# A rename that drops any of them out of this filter would silently thin the
# security gate with no CI signal, so scripts/check_security_smoke_count.sh owns
# a count-floor guard over exactly this subset (infra-12 mechanism, floor in
# .config/security-smoke-floor.txt), invoked from the e2e-tests job in ci.yml.
# NOT here by topology: the GHSA-3p3x FTPS/WebDAV constant-time e2e
# (protocols::test_protocol_core_suite) binds fixed ports and needs the
# ftps,webdav features, so it cannot join this random-port, default-feature
# profile; its GHSA-r5qv sibling is a unit test that already runs in the
# test-and-lint `--all --exclude e2e_test` pass. See
# docs/testing/security-regressions.md for the full CI-execution map.
#
# ILM tiering main path (backlog#1148 ilm-7): the `reliant::tiering::` clause
# admits the hermetic transition e2e. Like the fast replication pair checks it
# spawns a second independent single-node server (the cold RustFS tier), not a
# cluster, so it keeps the lane's parallel-safe / no-external-dependency
# properties. The RustFS warm backend has no loopback guard (that guard is
# replication-only), so it needs no opt-in env for its 127.0.0.1 tier target.
[profile.e2e-smoke]
default-filter = """
package(e2e_test) & (
test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_source_invalid_date|content_encoding|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools)_test::|^fake_s3_target::/)
| test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
| test(/^reliant::lifecycle::/)
| test(/^reliant::tiering::/)
)
"""
fail-fast = false
# ---------------------------------------------------------------------------
# e2e-repl-nightly profile — scheduled full replication e2e lane (repl-1)
# ---------------------------------------------------------------------------
# backlog#1147 repl-1 (deps: ci-4). Runs the SLOW / cross-process replication
# tests that are unfit for the per-PR e2e-smoke gate:
#
# * 2 remote-target TLS validation tests.
# * 12 bucket-replication data-plane/helper tests — they PUT/delete objects
# and poll until source and target converge; two replicate over HTTPS, two
# pin active SSE failure contracts, and one guards event/history observers.
# The SSE-S3 contract remains ignored under backlog#1291.
# * 11 `_real_dual_node` site-replication tests — each spawns TWO full rustfs
# servers and drives the cross-process site-replication control plane.
# * 1 `_real_three_node` site-replication test.
# * 1 `_real_single_node` service-account round-trip test.
#
# The set is defined as "everything in replication_extension_test that is NOT
# in the e2e-smoke PR allowlist above" (the negated clause is byte-identical to
# the allowlist), so a newly added replication test automatically runs here
# until it is explicitly promoted to the fast PR subset — no replication test
# is ever silently left out of CI.
#
# #[serial] does NOT serialize under nextest (process-per-test; see the file
# header). These tests need no cross-test serialization: each spawns its own
# server(s) on random ports with isolated temp dirs, so they are parallel-safe
# by construction — the same property the e2e-smoke subset relies on. If load
# on the runner surfaces a real flake, quarantine the specific test with an
# OPEN issue link (ci-10 / backlog#937 policy), never blanket-retry or exclude.
#
# Wired by .github/workflows/e2e-replication-nightly.yml (schedule +
# workflow_dispatch), which builds the rustfs binary once, installs awscurl so
# the STS dual-node test actually exercises its path (it skips gracefully with
# a visible log line when awscurl is absent), and routes scheduled failures
# through .github/actions/schedule-failure-issue (ci-8). Explicit division of
# labor with ci-5's future e2e-full merge gate: these tests run ONLY here, not
# double-run there. TODO(ci-7): fold this interim repl-owned lane into the ci
# domain's consolidated scheduled e2e workflow once it exists.
[profile.e2e-repl-nightly]
default-filter = """
package(e2e_test)
& test(/^replication_extension_test::/)
& !test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
"""
fail-fast = false
[profile.e2e-repl-nightly.junit]
# Emitted to target/nextest/e2e-repl-nightly/junit.xml; uploaded by the nightly
# workflow as the failure-triage artifact.
path = "junit.xml"
# ---------------------------------------------------------------------------
# e2e-full profile — merge-gate full single-node e2e lane (backlog#1149 ci-5)
# ---------------------------------------------------------------------------
# The merge gate (ci.yml `e2e-full` job: push main + merge_group +
# workflow_dispatch). Runs the never-automated user-visible suites — KMS (40),
# object_lock (33), multipart_auth (109), quota, checksum, encryption,
# security-boundary, ... — that the fast PR `e2e-smoke` subset deliberately
# skips. Budget <= 45 min; authority for the suite count is `cargo nextest list
# --profile e2e-full` (see docs/testing/e2e-suite-inventory.md).
#
# The filter is "the whole e2e_test crate MINUS the sets owned by other lanes":
# * protocols:: — FTPS/SFTP/WebDAV, still pinned to --test-threads=1 by fixed
# ports; they join a scheduled lane once ci-6 randomises the ports (ci-7).
# * the 7 cluster suites that spin up a RustFSTestClusterEnvironment
# (cluster_concurrency, cluster_multidrive_pool, stale_multipart_cleanup_cluster,
# namespace_lock_quorum, heal_erasure_disk_rebuild, admin_timeout_regression,
# object_lambda) — too heavy for the merge budget; they run in ci-7's
# nightly 4-node lane.
# * replication_extension_test — repl-1 already splits it into the PR
# `e2e-smoke` (20 fast) and `e2e-repl-nightly` (27 slow) lanes and reserves
# it for those, so e2e-full does not double-run it.
# * #[ignore]d tests — nextest skips them by default (no --run-ignored); the
# manual-localhost:9000 reliant/policy tests are ci-13's migration.
#
# Each e2e test spawns its own single-node rustfs server on a random port with
# an isolated temp dir (crates/e2e_test/src/common.rs), so the set is
# parallel-safe — the same property e2e-smoke relies on. The exception is the
# 4-disk reliability / degraded-read fault-injection tests, serialized below
# (identical to the ci profile) so several 4-disk servers never run at once.
# KNOWN-FAILURE EXCLUSIONS (characterization run 29381309848, 2026-07-15:
# 341 ran / 32 failed on the suites' first automated run ever). Deterministic
# product failures cannot be quarantined away with retries, so each family is
# excluded here with its tracking issue, under the same discipline as the
# ci-profile quarantine (docs/testing/README.md): every entry MUST cite one
# OPEN issue, and the fixing PR MUST delete the exclusion. The passing
# negative-path siblings of each family stay in as regression guards.
# * rustfs#4843 — over-limit archive entry paths hard-reject the whole
# archive even under ignore-errors semantics.
# * rustfs#4846 — distributed-lock quorum tests misclassify as timeout
# under parallel load (multi-node in-process clusters; natural home is
# ci-7's nightly cluster lane).
[profile.e2e-full]
default-filter = """
package(e2e_test)
& !test(/^protocols::/)
& !test(/^(admin_timeout_regression_test|cluster_concurrency_test|cluster_multidrive_pool_test|heal_erasure_disk_rebuild_test|namespace_lock_quorum_test|object_lambda_test|stale_multipart_cleanup_cluster_test)::/)
& !test(/^replication_extension_test::/)
& !test(/^multipart_auth_test::test_signed_put_object_extract_skips_invalid_entry_when_ignore_errors_enabled$/)
& !test(/^snowball_auto_extract_test::tests::snowball_auto_extract_(ignores_invalid_entries_when_requested|supports_standard_headers_with_combined_extract_options)$/)
& !test(/^reliant::lock::test_distributed_lock_(2_nodes_grpc_read_survives_failed_node|4_nodes_grpc_read_write_quorum_split_with_two_failed_nodes)$/)
"""
fail-fast = false
[profile.e2e-full.junit]
# Emitted to target/nextest/e2e-full/junit.xml; uploaded by the e2e-full job.
path = "junit.xml"
# Serialize the 4-disk reliability / degraded-read e2e tests under e2e-full too
# (see the e2e-reliability test-group note near the top of this file). Not a
# quarantine: no retries, just single-threaded so several 4-disk servers never
# run concurrently.
[[profile.e2e-full.overrides]]
filter = 'package(e2e_test) & test(/^(reliability_disk_fault|degraded_read_eof_regression)_test::/)'
test-group = 'e2e-reliability'
+12
View File
@@ -0,0 +1,12 @@
# Committed floor for the number of security negative-auth tests selected by the
# e2e-smoke PR profile (see scripts/check_security_smoke_count.sh, backlog#1151
# sec-5).
#
# The floor equals the exact count of e2e_test cases whose name starts with a
# security module prefix (negative_sigv4_test, presigned_negative_test,
# admin_auth_test) that the [profile.e2e-smoke] default-filter in
# .config/nextest.toml selects, at the time this file was last updated. CI fails
# if the selected count drops below this number, so a rename or removal that
# thins the security smoke gate must update this file in the same PR.
# Adding tests does not require a bump, but bumping keeps the guard tight.
16
+58
View File
@@ -36,6 +36,64 @@ To start a 4-node cluster for distributed testing:
docker compose -f .docker/compose/docker-compose.cluster.yaml up -d
```
### Script-Based 4-Node Validation (Recommended)
Use the local validation script when you need local-source image build, failover checks,
and benchmark workflow in one command:
```bash
# Default mode: WAIT_PROBE_MODE=service
# This avoids false negatives where /health/ready remains 503 locally
# while the service path is already available.
./scripts/run_four_node_cluster_failover_bench.sh
```
Strict mode is available when you explicitly want `/health/ready == 200` as the gate:
```bash
WAIT_PROBE_MODE=ready ./scripts/run_four_node_cluster_failover_bench.sh
```
### Profiling + Trace Validation
The profiling-focused 4-node compose keeps profiling enabled and points RustFS
to an OTLP/HTTP collector endpoint:
```bash
docker compose -f .docker/compose/docker-compose.cluster.local-build.profiling-amd64.yml up -d
```
Important behavior notes:
- `RUSTFS_OBS_ENDPOINT` is the OTLP/HTTP base URL. RustFS automatically sends
traces to `/v1/traces`, metrics to `/v1/metrics`, and logs to `/v1/logs`.
- Startup usually produces logs and metrics first. That does not guarantee
visible traces yet.
- Trace data becomes obvious only after real HTTP/S3/gRPC requests hit RustFS.
- `RUSTFS_OBS_LOGGER_LEVEL=info` keeps the top-level request span but filters
many nested `debug` spans. If Tempo/Jaeger looks sparse, retry with
`RUSTFS_OBS_LOGGER_LEVEL=debug` before suspecting the collector.
Minimal trace verification flow:
```bash
# 1. Start the profiling compose with richer span visibility.
RUSTFS_OBS_LOGGER_LEVEL=debug \
docker compose -f .docker/compose/docker-compose.cluster.local-build.profiling-amd64.yml up -d
# 2. Generate real request traffic after startup.
curl -I http://127.0.0.1:9000/health
curl -I http://127.0.0.1:9000/health/ready
# 3. Then inspect Tempo or Jaeger.
# Grafana: http://localhost:3000
# Jaeger: http://localhost:16686
```
If logs and metrics are present but traces are sparse, the most common cause is
"no real request traffic yet" or "`info` level filtered nested spans", not an
OTLP routing failure.
### (Deprecated) Minimal Observability
```bash
@@ -0,0 +1,236 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Profiling-first 4-node local-build compose.
#
# Goals:
# - force linux/amd64 runtime/build on Apple Silicon hosts;
# - enable RustFS built-in CPU profiling;
# - keep all tuning knobs host-overridable via env.
#
# Observability notes:
# - `RUSTFS_OBS_ENDPOINT` is the OTLP/HTTP base URL. RustFS appends
# `/v1/traces`, `/v1/metrics`, and `/v1/logs` automatically.
# - Logs and metrics usually appear during startup. Traces mainly appear after
# real HTTP/S3/gRPC requests create spans.
# - `RUSTFS_OBS_LOGGER_LEVEL=info` keeps the top-level request trace span but
# filters many `debug`-level nested spans. Use `debug` when validating trace
# richness rather than collector reachability.
services:
node1:
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node1
environment:
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node1_data_0:/data/rustfs0
- node1_data_1:/data/rustfs1
- node1_data_2:/data/rustfs2
- node1_data_3:/data/rustfs3
ports:
- "9000:9000"
networks:
- rustfs-cluster-net
node2:
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node2
environment:
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node2_data_0:/data/rustfs0
- node2_data_1:/data/rustfs1
- node2_data_2:/data/rustfs2
- node2_data_3:/data/rustfs3
ports:
- "9001:9000"
networks:
- rustfs-cluster-net
node3:
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node3
environment:
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node3_data_0:/data/rustfs0
- node3_data_1:/data/rustfs1
- node3_data_2:/data/rustfs2
- node3_data_3:/data/rustfs3
ports:
- "9002:9000"
networks:
- rustfs-cluster-net
node4:
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node4
environment:
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
# should show richer nested spans during request-path verification.
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node4_data_0:/data/rustfs0
- node4_data_1:/data/rustfs1
- node4_data_2:/data/rustfs2
- node4_data_3:/data/rustfs3
ports:
- "9003:9000"
networks:
- rustfs-cluster-net
volumes:
node1_data_0:
node1_data_1:
node1_data_2:
node1_data_3:
node2_data_0:
node2_data_1:
node2_data_2:
node2_data_3:
node3_data_0:
node3_data_1:
node3_data_2:
node3_data_3:
node4_data_0:
node4_data_1:
node4_data_2:
node4_data_3:
networks:
rustfs-cluster-net:
driver: bridge
@@ -0,0 +1,220 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
services:
node1:
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node1
environment:
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
- RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
# Internode gRPC optimization knobs (grpc-optimization P0-P3). Defaults match the binary
# defaults, so leaving these unset is a no-op; the A/B driver exports them to toggle a stage.
- RUSTFS_INTERNODE_RPC_TCP_NODELAY=${RUSTFS_INTERNODE_RPC_TCP_NODELAY:-true}
- RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE:-1048576}
- RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE:-2097152}
- RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE=${RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE:-104857600}
- RUSTFS_INTERNODE_CHANNEL_ISOLATION=${RUSTFS_INTERNODE_CHANNEL_ISOLATION:-false}
- RUSTFS_INTERNODE_BULK_CHANNELS=${RUSTFS_INTERNODE_BULK_CHANNELS:-2}
- RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=${RUSTFS_INTERNODE_RPC_MSGPACK_ONLY:-false}
- RUSTFS_INTERNODE_PREWARM=${RUSTFS_INTERNODE_PREWARM:-false}
- RUSTFS_INTERNODE_OFFLINE_BYPASS=${RUSTFS_INTERNODE_OFFLINE_BYPASS:-false}
- RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS=${RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS:-5}
- RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD=${RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD:-3}
- RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES=${RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES:-1}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node1_data_0:/data/rustfs0
- node1_data_1:/data/rustfs1
- node1_data_2:/data/rustfs2
- node1_data_3:/data/rustfs3
ports:
- "9000:9000"
networks:
- rustfs-cluster-net
node2:
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node2
environment:
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
- RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
# Internode gRPC optimization knobs (grpc-optimization P0-P3). Defaults match the binary
# defaults, so leaving these unset is a no-op; the A/B driver exports them to toggle a stage.
- RUSTFS_INTERNODE_RPC_TCP_NODELAY=${RUSTFS_INTERNODE_RPC_TCP_NODELAY:-true}
- RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE:-1048576}
- RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE:-2097152}
- RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE=${RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE:-104857600}
- RUSTFS_INTERNODE_CHANNEL_ISOLATION=${RUSTFS_INTERNODE_CHANNEL_ISOLATION:-false}
- RUSTFS_INTERNODE_BULK_CHANNELS=${RUSTFS_INTERNODE_BULK_CHANNELS:-2}
- RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=${RUSTFS_INTERNODE_RPC_MSGPACK_ONLY:-false}
- RUSTFS_INTERNODE_PREWARM=${RUSTFS_INTERNODE_PREWARM:-false}
- RUSTFS_INTERNODE_OFFLINE_BYPASS=${RUSTFS_INTERNODE_OFFLINE_BYPASS:-false}
- RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS=${RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS:-5}
- RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD=${RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD:-3}
- RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES=${RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES:-1}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node2_data_0:/data/rustfs0
- node2_data_1:/data/rustfs1
- node2_data_2:/data/rustfs2
- node2_data_3:/data/rustfs3
ports:
- "9001:9000"
networks:
- rustfs-cluster-net
node3:
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node3
environment:
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
- RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
# Internode gRPC optimization knobs (grpc-optimization P0-P3). Defaults match the binary
# defaults, so leaving these unset is a no-op; the A/B driver exports them to toggle a stage.
- RUSTFS_INTERNODE_RPC_TCP_NODELAY=${RUSTFS_INTERNODE_RPC_TCP_NODELAY:-true}
- RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE:-1048576}
- RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE:-2097152}
- RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE=${RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE:-104857600}
- RUSTFS_INTERNODE_CHANNEL_ISOLATION=${RUSTFS_INTERNODE_CHANNEL_ISOLATION:-false}
- RUSTFS_INTERNODE_BULK_CHANNELS=${RUSTFS_INTERNODE_BULK_CHANNELS:-2}
- RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=${RUSTFS_INTERNODE_RPC_MSGPACK_ONLY:-false}
- RUSTFS_INTERNODE_PREWARM=${RUSTFS_INTERNODE_PREWARM:-false}
- RUSTFS_INTERNODE_OFFLINE_BYPASS=${RUSTFS_INTERNODE_OFFLINE_BYPASS:-false}
- RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS=${RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS:-5}
- RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD=${RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD:-3}
- RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES=${RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES:-1}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node3_data_0:/data/rustfs0
- node3_data_1:/data/rustfs1
- node3_data_2:/data/rustfs2
- node3_data_3:/data/rustfs3
ports:
- "9002:9000"
networks:
- rustfs-cluster-net
node4:
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
build:
context: ../..
dockerfile: Dockerfile.source
hostname: node4
environment:
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
- RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_OBJECT_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_LOCK_ACQUIRE_TIMEOUT=${RUSTFS_LOCK_ACQUIRE_TIMEOUT:-5}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
# Internode gRPC optimization knobs (grpc-optimization P0-P3). Defaults match the binary
# defaults, so leaving these unset is a no-op; the A/B driver exports them to toggle a stage.
- RUSTFS_INTERNODE_RPC_TCP_NODELAY=${RUSTFS_INTERNODE_RPC_TCP_NODELAY:-true}
- RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE:-1048576}
- RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE=${RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE:-2097152}
- RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE=${RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE:-104857600}
- RUSTFS_INTERNODE_CHANNEL_ISOLATION=${RUSTFS_INTERNODE_CHANNEL_ISOLATION:-false}
- RUSTFS_INTERNODE_BULK_CHANNELS=${RUSTFS_INTERNODE_BULK_CHANNELS:-2}
- RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=${RUSTFS_INTERNODE_RPC_MSGPACK_ONLY:-false}
- RUSTFS_INTERNODE_PREWARM=${RUSTFS_INTERNODE_PREWARM:-false}
- RUSTFS_INTERNODE_OFFLINE_BYPASS=${RUSTFS_INTERNODE_OFFLINE_BYPASS:-false}
- RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS=${RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS:-5}
- RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD=${RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD:-3}
- RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES=${RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES:-1}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- node4_data_0:/data/rustfs0
- node4_data_1:/data/rustfs1
- node4_data_2:/data/rustfs2
- node4_data_3:/data/rustfs3
ports:
- "9003:9000"
networks:
- rustfs-cluster-net
volumes:
node1_data_0:
node1_data_1:
node1_data_2:
node1_data_3:
node2_data_0:
node2_data_1:
node2_data_2:
node2_data_3:
node3_data_0:
node3_data_1:
node3_data_2:
node3_data_3:
node4_data_0:
node4_data_1:
node4_data_2:
node4_data_3:
networks:
rustfs-cluster-net:
driver: bridge
@@ -0,0 +1,56 @@
services:
node1:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=16
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
node2:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=16
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
node3:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=16
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
node4:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=16
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
@@ -0,0 +1,56 @@
services:
node1:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
- RUSTFS_RUNTIME_WORKER_THREADS=24
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
node2:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
- RUSTFS_RUNTIME_WORKER_THREADS=24
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
node3:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
- RUSTFS_RUNTIME_WORKER_THREADS=24
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
node4:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
- RUSTFS_RUNTIME_WORKER_THREADS=24
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
@@ -0,0 +1,56 @@
services:
node1:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
- RUSTFS_RUNTIME_WORKER_THREADS=20
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
node2:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
- RUSTFS_RUNTIME_WORKER_THREADS=20
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
node3:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
- RUSTFS_RUNTIME_WORKER_THREADS=20
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
node4:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
- RUSTFS_RUNTIME_WORKER_THREADS=20
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
@@ -0,0 +1,56 @@
services:
node1:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=12
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
node2:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=12
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
node3:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=12
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
node4:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
- RUSTFS_RUNTIME_WORKER_THREADS=12
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
@@ -0,0 +1,56 @@
services:
node1:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
- RUSTFS_RUNTIME_WORKER_THREADS=6
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
node2:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
- RUSTFS_RUNTIME_WORKER_THREADS=6
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
node3:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
- RUSTFS_RUNTIME_WORKER_THREADS=6
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
node4:
environment:
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
- RUSTFS_RUNTIME_WORKER_THREADS=6
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
+8 -8
View File
@@ -21,8 +21,8 @@ services:
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=rustfsadmin
- RUSTFS_SECRET_KEY=rustfsadmin
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
platform: linux/amd64
ports:
- "9000:9000" # Map port 9001 of the host to port 9000 of the container
@@ -38,8 +38,8 @@ services:
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=rustfsadmin
- RUSTFS_SECRET_KEY=rustfsadmin
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
platform: linux/amd64
ports:
- "9001:9000" # Map port 9002 of the host to port 9000 of the container
@@ -55,8 +55,8 @@ services:
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=rustfsadmin
- RUSTFS_SECRET_KEY=rustfsadmin
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
platform: linux/amd64
ports:
- "9002:9000" # Map port 9003 of the host to port 9000 of the container
@@ -72,8 +72,8 @@ services:
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ACCESS_KEY=rustfsadmin
- RUSTFS_SECRET_KEY=rustfsadmin
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin-local}
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfssecret-local}
platform: linux/amd64
ports:
- "9003:9000" # Map port 9004 of the host to port 9000 of the container
@@ -26,7 +26,7 @@ services:
restart: "no"
tempo:
image: grafana/tempo:latest
image: grafana/tempo:2.10.5
user: "10001"
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
@@ -36,9 +36,19 @@ services:
- "3200:3200" # tempo
- "4317" # otlp grpc
- "4318" # otlp http
- "7946" # memberlist
restart: unless-stopped
networks:
- rustfs-network
depends_on:
tempo-init:
condition: service_completed_successfully
healthcheck:
test: [ "CMD", "/tempo", "-version" ]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
@@ -61,6 +71,12 @@ services:
- jaeger
- prometheus
- loki
restart: unless-stopped
healthcheck:
test: [ "CMD", "/otelcol-contrib", "--version" ]
interval: 10s
timeout: 5s
retries: 3
jaeger:
image: jaegertracing/jaeger:latest
@@ -72,12 +88,23 @@ services:
- BADGER_DIRECTORY_KEY=/badger/key
- COLLECTOR_OTLP_ENABLED=true
volumes:
- ../../.docker/observability/jaeger.yaml:/etc/jaeger/config.yml:ro
- jaeger-data:/badger
ports:
- "16686:16686" # Web UI
- "14269:14269" # Admin/Metrics
- "4317" # otlp grpc
- "4318" # otlp http
command: [ "--config", "/etc/jaeger/config.yml" ]
networks:
- rustfs-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:14269" ]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
prometheus:
image: prom/prometheus:latest
@@ -85,6 +112,7 @@ services:
- TZ=Asia/Shanghai
volumes:
- ../../.docker/observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ../../.docker/observability/prometheus-rules:/etc/prometheus/rules:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
@@ -94,23 +122,45 @@ services:
- '--web.enable-remote-write-receiver'
- '--enable-feature=promql-experimental-functions'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
- '--storage.tsdb.retention.time=30d'
networks:
- rustfs-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy" ]
interval: 10s
timeout: 5s
retries: 3
loki:
image: grafana/loki:latest
environment:
- TZ=Asia/Shanghai
volumes:
- ../../.docker/observability/loki.yaml:/etc/loki/local-config.yaml:ro
- ../../.docker/observability/loki.yaml:/etc/loki/loki.yaml:ro
- loki-data:/loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
command: -config.file=/etc/loki/loki.yaml
networks:
- rustfs-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
interval: 15s
timeout: 10s
retries: 5
start_period: 60s
pyroscope:
image: grafana/pyroscope:latest
ports:
- "4040:4040"
command:
- -self-profiling.disable-push=true
networks:
- rustfs-network
restart: unless-stopped
grafana:
image: grafana/grafana:latest
@@ -127,10 +177,17 @@ services:
volumes:
- ../../.docker/observability/grafana/provisioning:/etc/grafana/provisioning:ro
- ../../.docker/observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
depends_on:
- prometheus
- tempo
- loki
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health" ]
interval: 10s
timeout: 5s
retries: 3
# --- RustFS Cluster ---
@@ -215,6 +272,7 @@ volumes:
tempo-data:
loki-data:
jaeger-data:
grafana-data:
networks:
rustfs-network:
+11
View File
@@ -74,6 +74,17 @@ http {
# ssl_certificate /etc/nginx/ssl/server.crt;
# ssl_certificate_key /etc/nginx/ssl/server.key;
#
# # Restrict to modern TLS versions and ciphers. Operators copying this
# # example must keep at least these directives — without them, nginx
# # may negotiate older protocol versions that have known weaknesses.
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_prefer_server_ciphers on;
# ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
# ssl_session_timeout 1d;
# ssl_session_cache shared:SSL:10m;
# ssl_session_tickets off;
# # add_header Strict-Transport-Security "max-age=63072000" always;
#
# location / {
# proxy_pass http://rustfs:9000;
# ...
+111
View File
@@ -13,6 +13,9 @@ The stack is composed of the following best-in-class open-source components:
- **Jaeger** (v1.59.0): Distributed tracing system (configured as a secondary UI/storage).
- **OpenTelemetry Collector** (v0.104.0): A vendor-agnostic implementation for receiving, processing, and exporting telemetry data.
By default, this stack uses Tempo in single-binary mode and does not require Kafka/Redpanda.
If you want the Kafka-backed HA Tempo path, use `docker-compose-example-for-rustfs.yml` together with `docker-compose-tempo-ha-override.yml`.
## Architecture
1. **Telemetry Collection**: Applications send OTLP (OpenTelemetry Protocol) data (Metrics, Logs, Traces) to the **OpenTelemetry Collector**.
@@ -31,6 +34,57 @@ The stack is composed of the following best-in-class open-source components:
- **High Performance**: Optimized configurations for batching, compression, and memory management.
- **Standardized Protocols**: Built entirely on OpenTelemetry standards.
## GET Performance Optimization Dashboards
Three pre-built Grafana dashboards are included for monitoring RustFS GET performance optimization rollout:
### Available Dashboards
| Dashboard | File | Description |
|-----------|------|-------------|
| **GET Rollout Health** | `grafana-get-rollout-health.json` | Monitors optimization rollout: latency by reader path, early-stop hit rate, codec streaming usage, pipeline failures |
| **GET Data Integrity** | `grafana-get-data-integrity.json` | Monitors data safety: bitrot verify failures, decode errors, short reads, shard read outcomes |
| **GET Resource Impact** | `grafana-get-resource-impact.json` | Monitors resource usage: concurrent requests, IO queue utilization, disk permit wait, RSS trend |
| **Object Data Cache** | `grafana-object-data-cache.json` | Monitors the GET body cache (`rustfs_object_data_cache_*`): hit ratio, lookup/plan/fill outcomes, fill duration quantiles, hit vs fill throughput, entries/weighted bytes, inflight fills, memory-pressure skips, invalidations, and size-class breakdowns |
### Prometheus Alert Rules
The file `prometheus-rules/rustfs-get-optimization-alerts.yaml` contains pre-configured alerting rules:
| Alert | Severity | Condition |
|-------|----------|-----------|
| `GetP99Regression` | Critical | GET p99 latency > 2x baseline for 10m |
| `PipelineFailureSpike` | Critical | Pipeline failure rate > 5x baseline for 5m |
| `BitrotMismatchSpike` | Critical | Bitrot mismatch rate > 3x baseline for 5m |
| `EarlyStopInsufficientQuorum` | Warning | Early-stop insufficient quorum rate > 0.1/s for 5m |
| `CodecStreamingFallbackSpike` | Warning | Codec streaming fallback > 10x baseline for 10m |
| `IoQueueSaturation` | Warning | IO queue utilization > 90% for 5m |
### Enabling Alert Rules
Add the alert rules file to your Prometheus configuration:
```yaml
# prometheus.yml
rule_files:
- "/etc/prometheus/rules/*.yml"
# Or mount the file in docker-compose.yml:
# volumes:
# - ./prometheus-rules:/etc/prometheus/rules
```
### Dashboard Usage
The dashboards are automatically provisioned when Grafana starts. They use the `${DS_PROMETHEUS}` datasource variable, so you need a Prometheus datasource configured in Grafana.
Key panels to monitor during optimization rollout:
1. **GET Latency by Reader Path** - Compare `codec_streaming` vs `legacy_duplex` latency
2. **Early-Stop Hit Rate** - Verify early-stop is triggering effectively
3. **Pipeline Failure Rate** - Detect any new failure modes introduced by optimizations
4. **Bitrot Verify Failures** - Ensure data integrity is maintained
## Quick Start
### Prerequisites
@@ -46,6 +100,15 @@ Run the following command to start the entire stack:
docker compose up -d
```
### High Availability Tempo
The default `docker-compose.yml` is the single-node stack.
If you need the Kafka-backed HA Tempo configuration, start it with:
```bash
docker compose -f docker-compose-example-for-rustfs.yml -f docker-compose-tempo-ha-override.yml up -d
```
### Access Dashboards
| Service | URL | Credentials | Description |
@@ -78,6 +141,54 @@ docker compose down -v
- **Grafana**: Dashboards and datasources are provisioned from the `grafana/` directory.
- **Collector**: Edit `otel-collector-config.yaml` to modify pipelines, processors, or exporters.
### Verifying RustFS Traces
When RustFS points `RUSTFS_OBS_ENDPOINT` at this stack, treat the value as the
OTLP/HTTP base URL, for example:
```bash
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
```
RustFS automatically expands that base URL to:
- `/v1/traces`
- `/v1/metrics`
- `/v1/logs`
Important behavior notes:
- Logs and metrics usually appear during startup, so seeing those two signals
first is expected.
- Visible trace data usually requires real HTTP/S3/gRPC request traffic after
startup, because request-path spans are created on demand.
- `RUSTFS_OBS_LOGGER_LEVEL=info` keeps the top-level request span but filters
many nested `debug` spans. If Tempo or Jaeger looks sparse, retry with
`RUSTFS_OBS_LOGGER_LEVEL=debug` before suspecting collector or Tempo issues.
Minimal validation flow:
```bash
# 1. Start this observability stack.
docker compose up -d
# 2. Start RustFS with OTLP/HTTP export and richer span visibility.
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
export RUSTFS_OBS_LOGGER_LEVEL=debug
# 3. Generate real request traffic.
curl -I http://127.0.0.1:9000/health
curl -I http://127.0.0.1:9000/health/ready
# 4. Inspect Grafana or Jaeger.
# Grafana: http://localhost:3000
# Jaeger: http://localhost:16686
```
If logs and metrics are present but traces are sparse, the most common cause is
"no real request traffic yet" or "`info` level filtered nested spans", not an
OTLP routing failure.
## Troubleshooting
- **Service Health**: Check the health of services using `docker compose ps`.
+107
View File
@@ -13,6 +13,9 @@
- **Jaeger** (v1.59.0): 分布式追踪系统(配置为辅助 UI/存储)。
- **OpenTelemetry Collector** (v0.104.0): 接收、处理和导出遥测数据的供应商无关实现。
默认情况下,这套技术栈使用 Tempo 单二进制模式,不依赖 Kafka/Redpanda。
如果需要基于 Kafka 的 HA Tempo 路径,请使用 `docker-compose-example-for-rustfs.yml` 配合 `docker-compose-tempo-ha-override.yml`
## 架构
1. **遥测收集**: 应用程序将 OTLP (OpenTelemetry Protocol) 数据(指标、日志、追踪)发送到 **OpenTelemetry Collector**
@@ -31,6 +34,57 @@
- **高性能**: 针对批处理、压缩和内存管理进行了优化配置。
- **标准化协议**: 完全基于 OpenTelemetry 标准构建。
## GET 性能优化仪表盘
包含三个预构建的 Grafana 仪表盘,用于监控 RustFS GET 性能优化发布:
### 可用仪表盘
| 仪表盘 | 文件 | 描述 |
|--------|------|------|
| **GET 发布健康度** | `grafana-get-rollout-health.json` | 监控优化发布:按 reader path 的延迟、early-stop 命中率、codec streaming 使用率、pipeline 失败率 |
| **GET 数据完整性** | `grafana-get-data-integrity.json` | 监控数据安全:bitrot 校验失败、decode 错误、short read、shard 读取结果 |
| **GET 资源影响** | `grafana-get-resource-impact.json` | 监控资源使用:并发请求数、IO 队列利用率、disk permit 等待、RSS 趋势 |
| **对象数据缓存** | `grafana-object-data-cache.json` | 监控 GET body 缓存(`rustfs_object_data_cache_*`):命中率、查找/规划/填充结果、填充耗时分位、命中 vs 填充吞吐、条目数/加权字节、在途填充、内存压力拒绝、失效、按尺寸档拆分 |
### Prometheus 告警规则
文件 `prometheus-rules/rustfs-get-optimization-alerts.yaml` 包含预配置的告警规则:
| 告警 | 级别 | 条件 |
|------|------|------|
| `GetP99Regression` | 严重 | GET p99 延迟 > 2x 基线,持续 10 分钟 |
| `PipelineFailureSpike` | 严重 | Pipeline 失败率 > 5x 基线,持续 5 分钟 |
| `BitrotMismatchSpike` | 严重 | Bitrot 不匹配率 > 3x 基线,持续 5 分钟 |
| `EarlyStopInsufficientQuorum` | 警告 | Early-stop quorum 不足率 > 0.1/s,持续 5 分钟 |
| `CodecStreamingFallbackSpike` | 警告 | Codec streaming 回退 > 10x 基线,持续 10 分钟 |
| `IoQueueSaturation` | 警告 | IO 队列利用率 > 90%,持续 5 分钟 |
### 启用告警规则
在 Prometheus 配置中添加告警规则文件:
```yaml
# prometheus.yml
rule_files:
- "/etc/prometheus/rules/*.yml"
# 或在 docker-compose.yml 中挂载文件:
# volumes:
# - ./prometheus-rules:/etc/prometheus/rules
```
### 仪表盘使用
仪表盘在 Grafana 启动时自动预置。它们使用 `${DS_PROMETHEUS}` 数据源变量,因此需要在 Grafana 中配置 Prometheus 数据源。
优化发布期间需要关注的关键面板:
1. **GET 延迟按 Reader Path** - 对比 `codec_streaming` vs `legacy_duplex` 延迟
2. **Early-Stop 命中率** - 验证 early-stop 是否有效触发
3. **Pipeline 失败率** - 检测优化引入的新故障模式
4. **Bitrot 校验失败** - 确保数据完整性
## 快速开始
### 前置条件
@@ -46,6 +100,15 @@
docker compose up -d
```
### Tempo 高可用模式
默认的 `docker-compose.yml` 对应单机栈。
如果需要基于 Kafka 的 HA Tempo 配置,请使用:
```bash
docker compose -f docker-compose-example-for-rustfs.yml -f docker-compose-tempo-ha-override.yml up -d
```
### 访问仪表盘
| 服务 | URL | 凭据 | 描述 |
@@ -78,6 +141,50 @@ docker compose down -v
- **Grafana**: 仪表盘和数据源从 `grafana/` 目录预置。
- **Collector**: 编辑 `otel-collector-config.yaml` 以修改管道、处理器或导出器。
### 验证 RustFS Trace
当 RustFS 将 `RUSTFS_OBS_ENDPOINT` 指向这套技术栈时,应将该值视为
OTLP/HTTP 的基础 URL,例如:
```bash
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
```
RustFS 会自动在该基础 URL 后补全:
- `/v1/traces`
- `/v1/metrics`
- `/v1/logs`
需要注意:
- 启动阶段通常会先看到日志和指标,因此“先有日志/指标、后有 trace”是正常现象。
- 可见的 trace 数据通常依赖启动后的真实 HTTP/S3/gRPC 请求流量,因为请求路径上的 span 是按需创建的。
- `RUSTFS_OBS_LOGGER_LEVEL=info` 会保留顶层请求 span,但会过滤掉很多 `debug` 级别的嵌套 span。
如果 Tempo 或 Jaeger 中的 trace 看起来很稀疏,建议先改成 `RUSTFS_OBS_LOGGER_LEVEL=debug`,再判断是否是 collector 或 Tempo 问题。
最小验证流程:
```bash
# 1. 启动本目录下的可观测性技术栈。
docker compose up -d
# 2. 以 OTLP/HTTP 导出方式启动 RustFS,并提高 span 可见性。
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
export RUSTFS_OBS_LOGGER_LEVEL=debug
# 3. 产生真实请求流量。
curl -I http://127.0.0.1:9000/health
curl -I http://127.0.0.1:9000/health/ready
# 4. 到 Grafana 或 Jaeger 中检查。
# Grafana: http://localhost:3000
# Jaeger: http://localhost:16686
```
如果日志和指标已经正常,但 trace 仍然稀疏,最常见的原因通常是
“还没有真实请求流量”或“`info` 级别过滤了嵌套 span”,而不是 OTLP 路由失败。
## 故障排除
- **服务健康**: 使用 `docker compose ps` 检查服务健康状况。
@@ -85,10 +85,8 @@ services:
networks:
- otel-network
restart: unless-stopped
depends_on:
- redpanda
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3200/ready" ]
test: [ "CMD", "/tempo", "-version" ]
interval: 10s
timeout: 5s
retries: 3
@@ -205,7 +203,7 @@ services:
- prometheus
- loki
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:13133" ]
test: [ "CMD", "/otelcol-contrib", "--version" ]
interval: 10s
timeout: 5s
retries: 3
+7 -28
View File
@@ -16,10 +16,8 @@ services:
# --- Tracing ---
tempo:
image: grafana/tempo:2.10.3
image: grafana/tempo:2.10.5
container_name: tempo
depends_on:
- redpanda
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./tempo.yaml:/etc/tempo.yaml:ro
@@ -33,31 +31,11 @@ services:
- otel-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3200/ready" ]
test: [ "CMD", "/tempo", "-version" ]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
redpanda:
image: redpandadata/redpanda:latest
ports:
- "9092:9092" # Kafka API for clients
command: >
redpanda start --overprovisioned
--mode=dev-container
--kafka-addr=PLAINTEXT://0.0.0.0:9092
--advertise-kafka-addr=PLAINTEXT://redpanda:9092
redpanda-console:
image: docker.redpanda.com/redpandadata/console:latest
environment:
- CONFIG_FILEPATH=/etc/redpanda/redpanda-console-config.yaml
volumes:
- ./redpanda-console.yaml:/etc/redpanda/redpanda-console-config.yaml
ports:
- "8080:8080"
depends_on:
- redpanda
vulture:
image: grafana/tempo-vulture:latest
@@ -85,7 +63,7 @@ services:
- jaeger-data:/badger
ports:
- "16686:16686" # Web UI
- "14269:14269" # Admin/Metrics
- "18888:8888" # Metrics
- "4317" # otlp grpc
- "4318" # otlp http
command: [ "--config", "/etc/jaeger/config.yml" ]
@@ -93,7 +71,7 @@ services:
- otel-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:14269" ]
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8888/metrics" ]
interval: 10s
timeout: 5s
retries: 3
@@ -106,6 +84,7 @@ services:
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus-rules:/etc/prometheus/rules:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
@@ -139,7 +118,7 @@ services:
- otel-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
test: [ "CMD", "/usr/bin/loki", "--version" ]
interval: 15s
timeout: 10s
retries: 5
@@ -168,7 +147,7 @@ services:
- prometheus
- loki
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:13133" ]
test: [ "CMD", "/otelcol-contrib", "--version" ]
interval: 10s
timeout: 5s
retries: 3
@@ -0,0 +1,500 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 20,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "line+area"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "transparent",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max", "sum"]
}
},
"title": "Bitrot Verify Failures",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_pipeline_failures_total{stage=\"bitrot_verify\"}[$__rate_interval])) by (path, reason)",
"legendFormat": "bitrot_fail {{path}} / {{reason}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_pipeline_failures_total{reason=\"bitrot_verify_failed\"}[$__rate_interval])) by (path, stage)",
"legendFormat": "verify_failed {{path}} / {{stage}}",
"refId": "B"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "Bitrot Verify Duration (p50 / p95 / p99)",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.50, sum(rate(rustfs_io_get_object_shard_bitrot_verify_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p50",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum(rate(rustfs_io_get_object_shard_bitrot_verify_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p95",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.99, sum(rate(rustfs_io_get_object_shard_bitrot_verify_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p99",
"refId": "C"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 20,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "line+area"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "transparent",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max", "sum"]
}
},
"title": "Decode Errors",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_pipeline_failures_total{reason=\"decode_error\"}[$__rate_interval])) by (path, stage)",
"legendFormat": "decode_error {{path}} / {{stage}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 20,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "line+area"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "transparent",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max", "sum"]
}
},
"title": "Short Reads",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_pipeline_failures_total{reason=\"short_read\"}[$__rate_interval])) by (path, stage)",
"legendFormat": "short_read {{path}} / {{stage}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "Shard Read Outcomes by Role",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_shard_read_total[$__rate_interval])) by (path, role, outcome)",
"legendFormat": "{{path}} {{role}}/{{outcome}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "Request Result Status Rate",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_request_results_total[$__rate_interval])) by (status)",
"legendFormat": "{{status}}",
"refId": "A"
}
]
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": ["rustfs", "get-optimization", "data-integrity"],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "RustFS GET Data Integrity",
"uid": "rustfs-get-data-integrity",
"version": 1
}
@@ -0,0 +1,716 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_strategy_total[$__rate_interval])) or vector(0)",
"legendFormat": "{{strategy}} / {{mode}}",
"refId": "A"
}
],
"title": "GET Reader Setup Strategy Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (path, strategy, size_bucket) (rate(rustfs_io_get_object_reader_setup_strategy_by_size_total[$__rate_interval])) or vector(0)",
"legendFormat": "{{path}} / {{strategy}} / {{size_bucket}}",
"refId": "A"
}
],
"title": "GET Reader Setup Strategy by Size",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_scheduled_sum[$__rate_interval])) / clamp_min(sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_scheduled_count[$__rate_interval])), 1) or vector(0)",
"legendFormat": "scheduled {{strategy}} / {{mode}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_attempted_sum[$__rate_interval])) / clamp_min(sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_attempted_count[$__rate_interval])), 1) or vector(0)",
"legendFormat": "attempted {{strategy}} / {{mode}}",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_ready_sum[$__rate_interval])) / clamp_min(sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_ready_count[$__rate_interval])), 1) or vector(0)",
"legendFormat": "ready {{strategy}} / {{mode}}",
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_deferred_sum[$__rate_interval])) / clamp_min(sum by (strategy, mode) (rate(rustfs_io_get_object_reader_setup_deferred_count[$__rate_interval])), 1) or vector(0)",
"legendFormat": "deferred {{strategy}} / {{mode}}",
"refId": "D"
}
],
"title": "GET Reader Setup Fanout Average",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (path, strategy, size_bucket) (rate(rustfs_io_get_object_reader_setup_scheduled_by_size_sum[$__rate_interval])) / clamp_min(sum by (path, strategy, size_bucket) (rate(rustfs_io_get_object_reader_setup_scheduled_by_size_count[$__rate_interval])), 1) or vector(0)",
"legendFormat": "scheduled {{path}} / {{strategy}} / {{size_bucket}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (path, strategy, size_bucket) (rate(rustfs_io_get_object_reader_setup_ready_by_size_sum[$__rate_interval])) / clamp_min(sum by (path, strategy, size_bucket) (rate(rustfs_io_get_object_reader_setup_ready_by_size_count[$__rate_interval])), 1) or vector(0)",
"legendFormat": "ready {{path}} / {{strategy}} / {{size_bucket}}",
"refId": "B"
}
],
"title": "GET Reader Setup Fanout by Size",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum by (path, stage, size_bucket, le) (rate(rustfs_io_get_object_stage_duration_seconds_by_size_bucket{stage=~\"reader_setup|reader_setup_schedule|reader_setup_wait_quorum|reader_setup_drop_pending|reader_task_file_open|reader_task_reader_construction|reader_task_bitrot_reader_init\"}[$__rate_interval]))) or vector(0)",
"legendFormat": "p95 {{path}} / {{stage}} / {{size_bucket}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.99, sum by (path, stage, size_bucket, le) (rate(rustfs_io_get_object_stage_duration_seconds_by_size_bucket{stage=~\"reader_setup|reader_setup_schedule|reader_setup_wait_quorum|reader_setup_drop_pending|reader_task_file_open|reader_task_reader_construction|reader_task_bitrot_reader_init\"}[$__rate_interval]))) or vector(0)",
"legendFormat": "p99 {{path}} / {{stage}} / {{size_bucket}}",
"refId": "B"
}
],
"title": "GET Hot Stage Latency by Size",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum by (path, stage, le) (rate(rustfs_io_get_object_stage_duration_seconds_bucket{stage=~\"request_context|first_byte|response_handoff|full_body|body_build|output_poll|output_lock_wait|reader_open_mmap_copy_success|reader_open_mmap_copy_fallback|reader_open_stream|reader_stream_first_read|reader_mmap_blocking_wait|reader_mmap_blocking_task|reader_mmap_file_open|reader_mmap_map|reader_mmap_copy_buffer|reader_mmap_direct_read_copy\"}[$__rate_interval]))) or vector(0)",
"legendFormat": "p95 {{path}} / {{stage}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.99, sum by (path, stage, le) (rate(rustfs_io_get_object_stage_duration_seconds_bucket{stage=~\"request_context|first_byte|response_handoff|full_body|body_build|output_poll|output_lock_wait|reader_open_mmap_copy_success|reader_open_mmap_copy_fallback|reader_open_stream|reader_stream_first_read|reader_mmap_blocking_wait|reader_mmap_blocking_task|reader_mmap_file_open|reader_mmap_map|reader_mmap_copy_buffer|reader_mmap_direct_read_copy\"}[$__rate_interval]))) or vector(0)",
"legendFormat": "p99 {{path}} / {{stage}}",
"refId": "B"
}
],
"title": "GET Reader, Handler and Body Latency",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 24
},
"id": 7,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (subpath) (rate(rustfs_io_get_object_direct_memory_subpath_total[$__rate_interval])) or vector(0)",
"legendFormat": "{{subpath}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (subpath, size_bucket) (rate(rustfs_io_get_object_direct_memory_subpath_by_size_total[$__rate_interval])) or vector(0)",
"legendFormat": "{{subpath}} / {{size_bucket}}",
"refId": "B"
}
],
"title": "GET Direct-Memory Subpath Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 24
},
"id": 8,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (path, stage, kind) (rate(rustfs_io_get_object_mmap_page_faults_total[$__rate_interval])) or vector(0)",
"legendFormat": "mmap {{path}} / {{stage}} / {{kind}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (path, stage, kind) (rate(rustfs_io_get_object_direct_read_page_faults_total[$__rate_interval])) or vector(0)",
"legendFormat": "direct-read {{path}} / {{stage}} / {{kind}}",
"refId": "B"
}
],
"title": "GET mmap vs Direct-Read Page Fault Rate",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": [
"rustfs",
"get",
"performance",
"attribution"
],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timezone": "",
"title": "RustFS GET Performance Attribution",
"uid": "rustfs-get-performance-attribution",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,525 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 20,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "Concurrent GET Requests",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "rustfs_io_get_object_concurrent_requests",
"legendFormat": "concurrent GETs",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 20,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "line+area"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "transparent",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "IO Queue Utilization",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "rustfs_io_queue_utilization_percent",
"legendFormat": "queue utilization %",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "rustfs_io_queue_permits_in_use",
"legendFormat": "permits in use",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "rustfs_io_queue_permits_available",
"legendFormat": "permits available",
"refId": "C"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "Disk Permit Wait Duration (p50 / p95 / p99)",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.50, sum(rate(rustfs_io_disk_permit_wait_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p50",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum(rate(rustfs_io_disk_permit_wait_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p95",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.99, sum(rate(rustfs_io_disk_permit_wait_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p99",
"refId": "C"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 15,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["lastNotNull"]
}
},
"title": "RSS Trend (Process Memory)",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "process_resident_memory_bytes{job=~\"rustfs.*\"}",
"legendFormat": "RSS {{instance}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "rustfs_process_memory_bytes",
"legendFormat": "RustFS memory {{instance}}",
"refId": "B"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "IO Queue Operations by Priority",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_queue_operations[$__rate_interval])) by (operation, priority)",
"legendFormat": "{{operation}} / {{priority}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 15,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["sum"]
}
},
"title": "IO Queue Congestion Events",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_queue_congestion_total[$__rate_interval]))",
"legendFormat": "congestion events/s",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_timeout_total[$__rate_interval])) by (stage)",
"legendFormat": "timeout {{stage}}",
"refId": "B"
}
]
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": ["rustfs", "get-optimization", "resource-impact"],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "RustFS GET Resource Impact",
"uid": "rustfs-get-resource-impact",
"version": 1
}
@@ -0,0 +1,530 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"axisLabel": "",
"axisColorMode": "text",
"scaleDistribution": {
"type": "linear"
},
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "GET Latency by Reader Path (p50 / p95 / p99)",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.50, sum(rate(rustfs_io_get_object_stage_duration_seconds_bucket{stage=\"total\"}[$__rate_interval])) by (le, path))",
"legendFormat": "p50 {{path}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum(rate(rustfs_io_get_object_stage_duration_seconds_bucket{stage=\"total\"}[$__rate_interval])) by (le, path))",
"legendFormat": "p95 {{path}}",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.99, sum(rate(rustfs_io_get_object_stage_duration_seconds_bucket{stage=\"total\"}[$__rate_interval])) by (le, path))",
"legendFormat": "p99 {{path}}",
"refId": "C"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 15,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 0.3
},
{
"color": "red",
"value": 0.7
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "Early-Stop Hit Rate",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_metadata_early_stop_total{decision=\"hit\"}[$__rate_interval])) by (path) / clamp_min(sum(rate(rustfs_io_get_object_metadata_early_stop_total[$__rate_interval])) by (path), 1)",
"legendFormat": "hit rate {{path}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 15,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "Codec Streaming Usage Rate",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_codec_streaming_decision_total[$__rate_interval])) by (decision) / clamp_min(sum(rate(rustfs_io_get_object_codec_streaming_decision_total[$__rate_interval])), 1)",
"legendFormat": "codec_streaming {{decision}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_stream_strategy_total[$__rate_interval])) by (strategy) / clamp_min(sum(rate(rustfs_io_get_object_stream_strategy_total[$__rate_interval])), 1)",
"legendFormat": "stream_strategy {{strategy}}",
"refId": "B"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "bars",
"fillOpacity": 80,
"lineWidth": 0,
"pointSize": 5,
"showPoints": "never",
"spanNulls": false,
"stacking": {
"mode": "normal",
"group": "A"
},
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["sum"]
}
},
"title": "Pipeline Failure Rate by Stage",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_pipeline_failures_total[$__rate_interval])) by (stage, reason)",
"legendFormat": "{{stage}} / {{reason}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "reqps",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "GET Request Rate by Path",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_io_get_object_reader_path_total[$__rate_interval])) by (path)",
"legendFormat": "{{path}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"tooltip": {
"mode": "multi",
"sort": "desc"
},
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": ["mean", "max"]
}
},
"title": "GET Total Latency (p50 / p95 / p99)",
"type": "timeseries",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.50, sum(rate(rustfs_io_get_object_total_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p50",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum(rate(rustfs_io_get_object_total_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p95",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.99, sum(rate(rustfs_io_get_object_total_duration_seconds_bucket[$__rate_interval])) by (le))",
"legendFormat": "p99",
"refId": "C"
}
]
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": ["rustfs", "get-optimization"],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "RustFS GET Rollout Health",
"uid": "rustfs-get-rollout-health",
"version": 1
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,534 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (path, eager_status, size_bucket, buffer_bucket, large_concurrency_tuning) (rate(rustfs_s3_put_object_diagnostics_total[$__rate_interval])) or vector(0)",
"legendFormat": "{{path}} / {{eager_status}} / {{size_bucket}} / {{buffer_bucket}} / large={{large_concurrency_tuning}}",
"refId": "A"
}
],
"title": "PUT Diagnostics Decision Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (path) (rate(rustfs_s3_put_object_path_total[$__rate_interval])) or vector(0)",
"legendFormat": "{{path}}",
"refId": "A"
}
],
"title": "PUT Path Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ms",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum by (stage, le) (rate(rustfs_s3_put_object_stage_duration_ms_bucket{stage=~\"ingress_prepare|set_disk_writer_setup|set_disk_encode|set_disk_rename|set_disk_old_data_cleanup|multipart_ingress_prepare|multipart_set_disk_writer_setup|multipart_set_disk_encode|multipart_complete_tail\"}[$__rate_interval]))) or vector(0)",
"legendFormat": "p95 {{stage}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.99, sum by (stage, le) (rate(rustfs_s3_put_object_stage_duration_ms_bucket{stage=~\"ingress_prepare|set_disk_writer_setup|set_disk_encode|set_disk_rename|set_disk_old_data_cleanup|multipart_ingress_prepare|multipart_set_disk_writer_setup|multipart_set_disk_encode|multipart_complete_tail\"}[$__rate_interval]))) or vector(0)",
"legendFormat": "p99 {{stage}}",
"refId": "B"
}
],
"title": "PUT Hot Stage Latency",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.50, sum by (path, size_bucket, le) (rate(rustfs_s3_put_object_selected_buffer_size_bytes_bucket[$__rate_interval]))) or vector(0)",
"legendFormat": "p50 {{path}} / {{size_bucket}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "histogram_quantile(0.95, sum by (path, size_bucket, le) (rate(rustfs_s3_put_object_selected_buffer_size_bytes_bucket[$__rate_interval]))) or vector(0)",
"legendFormat": "p95 {{path}} / {{size_bucket}}",
"refId": "B"
}
],
"title": "PUT Selected Buffer Size",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 15,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_s3_put_object_zero_copy_eligible_total[$__rate_interval])) / clamp_min(sum(rate(rustfs_s3_put_object_total[$__rate_interval])), 1) or vector(0)",
"legendFormat": "eligible / total",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_s3_put_object_zero_copy_enabled_total[$__rate_interval])) / clamp_min(sum(rate(rustfs_s3_put_object_total[$__rate_interval])), 1) or vector(0)",
"legendFormat": "enabled / total",
"refId": "B"
}
],
"title": "PUT Zero-Copy Eligibility Ratio",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"unit": "ops",
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"fillOpacity": 10,
"pointSize": 5,
"lineWidth": 1,
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"legend": {
"calcs": [
"mean",
"max"
],
"displayMode": "table",
"placement": "bottom"
},
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum(rate(rustfs_s3_put_object_total[$__rate_interval])) or vector(0)",
"legendFormat": "PUT total",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"expr": "sum by (stage) (rate(rustfs_system_storage_erasure_write_quorum_failures_total[$__rate_interval])) or vector(0)",
"legendFormat": "write quorum failure {{stage}}",
"refId": "B"
}
],
"title": "PUT Throughput and Quorum Failure Signals",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": [
"rustfs",
"put",
"performance",
"attribution"
],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timezone": "",
"title": "RustFS PUT Performance Attribution",
"uid": "rustfs-put-performance-attribution",
"version": 1,
"weekStart": ""
}
File diff suppressed because it is too large Load Diff
@@ -1,97 +0,0 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prometheus
access: proxy
orgId: 1
url: http://prometheus:9090
isDefault: true
version: 1
editable: false
jsonData:
httpMethod: GET
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempo
- name: Tempo
type: tempo
uid: tempo
access: proxy
orgId: 1
url: http://tempo:3200
isDefault: false
version: 1
editable: false
jsonData:
httpMethod: GET
serviceMap:
datasourceUid: prometheus
tracesToLogs:
datasourceUid: loki
tags: [ 'job', 'instance', 'pod', 'namespace', 'service.name' ]
mappedTags: [ { key: 'service.name', value: 'app' } ]
spanStartTimeShift: '1s'
spanEndTimeShift: '-1s'
filterByTraceID: true
filterBySpanID: false
tracesToMetrics:
datasourceUid: prometheus
tags: [ { key: 'service.name' }, { key: 'job' } ]
queries:
- name: 'Service-Level Latency'
query: 'sum(rate(traces_spanmetrics_latency_bucket{$$__tags}[5m])) by (le)'
- name: 'Service-Level Calls'
query: 'sum(rate(traces_spanmetrics_calls_total{$$__tags}[5m]))'
- name: 'Service-Level Errors'
query: 'sum(rate(traces_spanmetrics_calls_total{status_code="ERROR", $$__tags}[5m]))'
nodeGraph:
enabled: true
- name: Loki
type: loki
uid: loki
orgId: 1
url: http://loki:3100
isDefault: false
version: 1
editable: false
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: 'trace_id=(\w+)'
name: 'TraceID'
url: '$${__value.raw}'
- name: Jaeger
type: jaeger
uid: jaeger
url: http://jaeger:16686
access: proxy
isDefault: false
editable: false
jsonData:
tracesToLogs:
datasourceUid: loki
tags: [ 'job', 'instance', 'pod', 'namespace', 'service.name' ]
mappedTags: [ { key: 'service.name', value: 'app' } ]
spanStartTimeShift: '1s'
spanEndTimeShift: '-1s'
filterByTraceID: true
filterBySpanID: false
@@ -0,0 +1,53 @@
groups:
- name: rustfs-dashboard
interval: 30s
rules:
- record: rustfs:http_server_requests:rate5m
expr: sum by (job) (rate(rustfs_http_server_requests_total[5m]))
- record: rustfs:http_server_request_duration_seconds:p50_5m
expr: histogram_quantile(0.50, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
- record: rustfs:http_server_request_duration_seconds:p95_5m
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
- record: rustfs:http_server_request_duration_seconds:p99_5m
expr: histogram_quantile(0.99, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
- record: rustfs:http_server_response_body_size_bytes:p50_5m
expr: histogram_quantile(0.50, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
- record: rustfs:http_server_response_body_size_bytes:p95_5m
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
- record: rustfs:http_server_response_body_size_bytes:p99_5m
expr: histogram_quantile(0.99, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
- record: rustfs:log_cleaner_runs:rate15m
expr: sum by (job) (rate(rustfs_log_cleaner_runs_total[15m]))
- record: rustfs:log_cleaner_failure_ratio:rate5m
expr: sum by (job) (rate(rustfs_log_cleaner_run_failures_total[5m])) / clamp_min(sum by (job) (rate(rustfs_log_cleaner_runs_total[5m])), 1e-9)
- record: rustfs:log_cleaner_rotation_failure_ratio:rate5m
expr: sum by (job) (rate(rustfs_log_cleaner_rotation_failures_total[5m])) / clamp_min(sum by (job) (rate(rustfs_log_cleaner_rotation_total[5m])), 1e-9)
- record: rustfs:log_cleaner_rotation_duration_seconds:p95_5m
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_log_cleaner_rotation_duration_seconds_bucket[5m])))
- record: rustfs:log_cleaner_compress_duration_seconds:p95_5m
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_log_cleaner_compress_duration_seconds_bucket[5m])))
- record: rustfs:scanner_objects_scanned:rate5m
expr: sum by (job) (rate(rustfs_scanner_objects_scanned_total[5m]))
- record: rustfs:scanner_directories_scanned:rate5m
expr: sum by (job) (rate(rustfs_scanner_directories_scanned_total[5m]))
- record: rustfs:scanner_buckets_scanned:rate5m
expr: sum by (job) (rate(rustfs_scanner_buckets_scanned_total[5m]))
- record: rustfs:scanner_cycles_success:rate5m
expr: sum by (job) (rate(rustfs_scanner_cycles_total{result="success"}[5m]))
- record: rustfs:log_chain_op_event_mismatch:rate5m
expr: sum by (job) (rate(rustfs_log_chain_op_event_mismatch_total[5m]))
- alert: RustFSLogChainOpEventMismatchDetected
expr: rustfs:log_chain_op_event_mismatch:rate5m > 0
for: 10m
labels:
severity: warning
component: s3-log-chain
annotations:
summary: "RustFS log-chain op/event mismatch detected"
description: "job={{ $labels.job }} has non-zero rustfs_log_chain_op_event_mismatch_total rate for more than 10m. Check s3 op/event mapping changes."
@@ -0,0 +1,260 @@
# =============================================================================
# RustFS GET Optimization — Prometheus Alerting Rules
# =============================================================================
#
# Import into Prometheus:
# 1. Copy this file to your Prometheus rules directory
# 2. Add to prometheus.yml:
# rule_files:
# - "prometheus-alert-rules.yaml"
# 3. Validate: promtool check rules prometheus-alert-rules.yaml
# 4. Reload: curl -X POST http://localhost:9090/-/reload
#
# All metric names match those registered in crates/io-metrics/src/lib.rs
# and documented in crates/ecstore/src/diagnostics/get.rs.
#
# Baseline comparison uses "offset 1d" — adjust to "offset 7d" for weekly
# seasonality if your traffic pattern varies by day of week.
# =============================================================================
groups:
# ==========================================================================
# Critical alerts — immediate action required
# ==========================================================================
- name: rustfs-get-optimization-critical
interval: 30s
rules:
# ------------------------------------------------------------------
# 1. GetP99Regression
# GET p99 latency exceeds 2x the baseline (same time yesterday)
# sustained for 10 minutes.
# Action: Roll back the GET optimization immediately.
# ------------------------------------------------------------------
- alert: GetP99Regression
expr: |
histogram_quantile(0.99,
sum(rate(rustfs_io_get_object_total_duration_seconds_bucket[5m])) by (le)
)
>
2
*
histogram_quantile(0.99,
sum(rate(rustfs_io_get_object_total_duration_seconds_bucket[5m] offset 1d)) by (le)
)
for: 10m
labels:
severity: critical
team: rustfs-storage
area: get-optimization
annotations:
summary: "GET p99 latency regression detected (>2x baseline for 10m)"
description: >-
The 99th-percentile GET object latency is {{ $value | humanizeDuration }}
which is more than double the baseline measured 24 hours ago.
This indicates a severe performance regression introduced by
a recent GET optimization change.
runbook_url: "https://internal.wiki/runbooks/rustfs/get-p99-regression"
action: >
1. Verify the regression is not caused by external factors
(disk health, network, load spike).
2. If confirmed optimization-related, roll back:
- Set RUSTFS_GET_CODEC_STREAMING=0
- Set RUSTFS_GET_METADATA_EARLY_STOP=0
- Restart affected nodes.
3. Collect flamegraphs and open a P0 incident.
# ------------------------------------------------------------------
# 2. PipelineFailureSpike
# Pipeline failure rate exceeds 5x the baseline sustained for
# 5 minutes. Covers all failure reasons: bitrot_mismatch,
# decode_error, downstream_closed, io, read_quorum, timeout, etc.
# Action: Investigate pipeline health and roll back if needed.
# ------------------------------------------------------------------
- alert: PipelineFailureSpike
expr: |
sum(rate(rustfs_io_get_object_pipeline_failures_total[5m]))
>
5
*
sum(rate(rustfs_io_get_object_pipeline_failures_total[5m] offset 1d))
for: 5m
labels:
severity: critical
team: rustfs-storage
area: get-optimization
annotations:
summary: "GET pipeline failure rate spike (>5x baseline for 5m)"
description: >-
The GET pipeline failure rate is {{ $value | printf "%.2f" }}/s,
more than 5x the baseline from 24 hours ago.
Failure reasons may include: bitrot_mismatch, decode_error,
downstream_closed, io, range_or_length_invalid, read_quorum,
short_read, timeout, unknown.
runbook_url: "https://internal.wiki/runbooks/rustfs/pipeline-failure-spike"
action: >
1. Check Grafana "GET Data Integrity" dashboard for failure
breakdown by reason label.
2. If decode_error or bitrot_mismatch dominates, stop
optimization and investigate data integrity.
3. If io or timeout dominates, check disk and network health.
4. Roll back optimization if failures persist.
# ------------------------------------------------------------------
# 3. BitrotMismatchSpike
# Bitrot verification mismatch rate exceeds 3x baseline for
# 5 minutes. This is a data-integrity signal — shard checksums
# do not match after read.
#
# The "bitrot_mismatch" reason is recorded on the
# rustfs_io_get_object_pipeline_failures_total counter when a
# StorageError::FileCorrupt or DiskError::FileCorrupt /
# DiskError::PartMissingOrCorrupt is classified during the GET
# pipeline (see classify_storage_error / classify_disk_error in
# crates/ecstore/src/diagnostics/get.rs).
#
# Action: Stop optimization, investigate data integrity urgently.
# ------------------------------------------------------------------
- alert: BitrotMismatchSpike
expr: |
sum(rate(rustfs_io_get_object_pipeline_failures_total{reason="bitrot_mismatch"}[5m]))
>
3
*
sum(rate(rustfs_io_get_object_pipeline_failures_total{reason="bitrot_mismatch"}[5m] offset 1d))
for: 5m
labels:
severity: critical
team: rustfs-storage
area: get-optimization
annotations:
summary: "Bitrot mismatch rate spike (>3x baseline for 5m)"
description: >-
The rate of pipeline failures classified as bitrot_mismatch is
{{ $value | printf "%.2f" }}/s, more than 3x the baseline from
24 hours ago. This indicates shard checksum verification
failures (FileCorrupt / PartMissingOrCorrupt) which may point
to data corruption introduced by the GET optimization pipeline
(e.g., incorrect decode, buffer reuse bug).
runbook_url: "https://internal.wiki/runbooks/rustfs/bitrot-mismatch-spike"
action: >
1. Immediately disable codec streaming:
RUSTFS_GET_CODEC_STREAMING=0
2. Run "mc admin scan" on affected buckets to verify on-disk
integrity independent of the GET path.
3. Compare xl.meta checksums across erasure shards.
4. If corruption confirmed, initiate data recovery from parity.
5. Do NOT re-enable optimization until root cause is identified.
# ==========================================================================
# Warning alerts — investigation needed
# ==========================================================================
- name: rustfs-get-optimization-warning
interval: 30s
rules:
# ------------------------------------------------------------------
# 4. EarlyStopInsufficientQuorum
# The metadata early-stop path is hitting "insufficient_quorum"
# at a rate above 0.1/s for 5 minutes. This means too many
# disks are failing to return valid metadata in time.
# Action: Check disk health and metadata fanout latency.
# ------------------------------------------------------------------
- alert: EarlyStopInsufficientQuorum
expr: |
sum(rate(rustfs_io_get_object_metadata_early_stop_total{reason="insufficient_quorum"}[5m]))
> 0.1
for: 5m
labels:
severity: warning
team: rustfs-storage
area: get-optimization
annotations:
summary: "Early-stop insufficient quorum rate elevated (>0.1/s for 5m)"
description: >-
The metadata early-stop path is returning "insufficient_quorum"
at {{ $value | printf "%.3f" }}/s. This means the bounded
metadata fanout cannot gather enough valid responses before
the quorum deadline, suggesting disk or network issues.
runbook_url: "https://internal.wiki/runbooks/rustfs/early-stop-quorum"
action: >
1. Check disk health: mc admin info --json | jq '.disks'
2. Review rustfs_io_get_object_metadata_response_total by
outcome (error, timeout, disk_not_found) in Grafana.
3. Check rustfs_io_disk_permit_wait_duration_seconds for
I/O scheduler saturation.
4. If disks are healthy, consider increasing the early-stop
timeout or temporarily disabling early-stop.
# ------------------------------------------------------------------
# 5. CodecStreamingFallbackSpike
# The codec streaming fallback rate is >10x baseline for 10
# minutes. This means the optimized codec streaming path is
# being bypassed much more often than expected.
# Action: Check fallback reasons and object eligibility.
# ------------------------------------------------------------------
- alert: CodecStreamingFallbackSpike
expr: |
sum(rate(rustfs_io_get_object_codec_streaming_fallback_total[5m]))
>
10
*
sum(rate(rustfs_io_get_object_codec_streaming_fallback_total[5m] offset 1d))
for: 10m
labels:
severity: warning
team: rustfs-storage
area: get-optimization
annotations:
summary: "Codec streaming fallback rate spike (>10x baseline for 10m)"
description: >-
The codec streaming fallback rate is {{ $value | printf "%.2f" }}/s,
more than 10x the baseline from 24 hours ago. Fallback reasons
are labeled by "reason" — check Grafana for breakdown.
Common reasons: object too small, multipart not supported,
unsupported erasure layout, feature flag disabled.
runbook_url: "https://internal.wiki/runbooks/rustfs/codec-fallback-spike"
action: >
1. Query by reason label:
sum by (reason) (rate(rustfs_io_get_object_codec_streaming_fallback_total[5m]))
2. If dominated by a single reason, investigate why that
condition became more frequent (e.g., workload change,
configuration drift).
3. Cross-reference with rustfs_io_get_object_reader_path_total
to verify the fallback path (legacy_duplex) is healthy.
4. If fallback is expected (e.g., workload shifted to small
objects), update the alert baseline.
# ------------------------------------------------------------------
# 6. IoQueueSaturation
# I/O queue utilization exceeds 90% for 5 minutes. High
# utilization causes disk permit wait latency to increase and
# can cascade into pipeline timeouts.
# Action: Check disk load and consider reducing concurrency.
# ------------------------------------------------------------------
- alert: IoQueueSaturation
expr: |
rustfs_io_queue_utilization_percent > 90
for: 5m
labels:
severity: warning
team: rustfs-storage
area: get-optimization
annotations:
summary: "I/O queue utilization >90% for 5m"
description: >-
The I/O queue utilization is {{ $value | printf "%.1f" }}%,
sustained above 90% for 5 minutes. This indicates the disk
I/O scheduler is near saturation, which will increase
rustfs_io_disk_permit_wait_duration_seconds and may trigger
pipeline timeouts.
runbook_url: "https://internal.wiki/runbooks/rustfs/io-queue-saturation"
action: >
1. Check disk I/O metrics (iostat, node_exporter) for
individual disk saturation.
2. Review rustfs_io_queue_permits_in_use vs
rustfs_io_queue_permits_available for permit exhaustion.
3. Check rustfs_io_starvation_events for priority starvation.
4. If GET optimization increased concurrency, consider:
- Reducing RUSTFS_GET_PIPELINE_PARALLELISM
- Lowering RUSTFS_IO_QUEUE_PERMITS
5. If caused by background operations (ILM, healing), throttle
those before adjusting GET concurrency.
+3
View File
@@ -19,6 +19,9 @@ global:
cluster: 'rustfs-dev' # Label to identify the cluster
replica: '1' # Replica identifier
rule_files:
- /etc/prometheus/rules/*.yml
scrape_configs:
- job_name: 'otel-collector'
static_configs:
+4 -31
View File
@@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
partition_ring_live_store: true
stream_over_http_enabled: true
server:
@@ -33,33 +32,17 @@ distributor:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
#log_received_spans:
# enabled: true
# log_discarded_spans:
# enabled: true
backend_scheduler:
provider:
compaction:
compaction:
block_retention: 1h
backend_worker:
backend_scheduler_addr: localhost:3200
compaction:
block_retention: 1h
ring:
kvstore:
store: memberlist
querier:
query_live_store: true
ingester:
max_block_duration: 5m
metrics_generator:
registry:
external_labels:
source: tempo
cluster: docker-compose
traces_storage:
path: /var/tempo/generator/traces
storage:
path: /var/tempo/generator/wal
remote_write:
@@ -85,15 +68,5 @@ overrides:
processors: [ "span-metrics", "service-graphs", "local-blocks" ]
generate_native_histograms: both
ingest:
enabled: true
kafka:
address: redpanda:9092
topic: tempo-ingest
block_builder:
consume_cycle_duration: 30s
usage_report:
reporting_enabled: false
+91
View File
@@ -0,0 +1,91 @@
# Rio compatibility compose files
These compose files prepare 4-node, 4-disk clusters for rio/rio-v2 storage format compatibility checks. All disks are bind-mounted under `.docker/compat/data` so the on-disk files remain available on the host.
## Clusters
```bash
docker compose -f .docker/compat/docker-compose.rustfs-beta5.yml up -d --build
docker compose -f .docker/compat/docker-compose.minio.yml up -d
docker compose -f .docker/compat/docker-compose.rustfs-rio-v2.yml up -d --build
```
Default API endpoints:
- RustFS `1.0.0-beta.5`: `http://127.0.0.1:9100`
- MinIO: `http://127.0.0.1:9200`
- current main with `rio-v2`: `http://127.0.0.1:9300`
## Reading old datasets with rio-v2
Stop the writer cluster before mounting its disks into the rio-v2 cluster.
```bash
docker compose -f .docker/compat/docker-compose.rustfs-beta5.yml down
RUSTFS_RIO_V2_DATASET=./data/rustfs-beta5 \
docker compose -f .docker/compat/docker-compose.rustfs-rio-v2.yml up -d --build
docker compose -f .docker/compat/docker-compose.minio.yml down
RUSTFS_RIO_V2_DATASET=./data/minio \
docker compose -f .docker/compat/docker-compose.rustfs-rio-v2.yml up -d --build
```
## 200G object mix
Use the same bucket/object matrix against the beta5 and MinIO endpoints, then read it back through the rio-v2 endpoint. A practical 200G mix is:
- 1 KiB x 1024
- 1 MiB x 1024
- 64 MiB x 512
- 1 GiB x 64
- 8 GiB x 12
- 6 GiB x 1
Compression is enabled by default for RustFS and MinIO. Server-side KMS/SSE settings are intentionally left to environment variables or mounted key directories so real key material is not committed. For SSE-C cases, run the clusters with TLS because MinIO requires HTTPS for SSE-C.
## High-concurrency write/read stress
Use `run_rw_compat_stress.sh` to generate a manifest on an old endpoint, then verify the same objects through the rio-v2 endpoint after mounting the old disks.
```bash
.docker/compat/run_rw_compat_stress.sh \
--mode write \
--endpoint http://127.0.0.1:9100 \
--access-key rustfsadmin \
--secret-key rustfsadmin \
--bucket compat-beta5 \
--concurrency 96
RUSTFS_RIO_V2_DATASET=./data/rustfs-beta5 \
docker compose -f .docker/compat/docker-compose.rustfs-rio-v2.yml up -d --build
.docker/compat/run_rw_compat_stress.sh \
--mode verify \
--endpoint http://127.0.0.1:9300 \
--access-key rustfsadmin \
--secret-key rustfsadmin \
--bucket compat-beta5 \
--concurrency 96 \
--manifest target/compat/rw-stress-YYYYmmdd-HHMMSS/manifest.csv
```
For encrypted datasets, add `--encryption sse-s3`, `--encryption sse-kms --sse-kms-key-id <key-id>`, or `--encryption sse-c --sse-c-key-file <raw-32-byte-key-file>` to both the write and verify commands.
## 5 GiB encrypted compatibility run
The `5g` profile covers 1 KiB, 1 MiB, 16 MiB, 64 MiB, and 1 GiB objects and totals exactly 5 GiB. Generate `compat-key.key` under `.docker/compat/kms/rustfs-compat`, enable local KMS on both RustFS clusters, then use the same encryption arguments while writing with beta5 and verifying with rio-v2. Set non-default local test credentials first because distributed listeners reject the built-in default credentials.
```bash
export COMPAT_ACCESS_KEY='<non-default-access-key>'
export COMPAT_SECRET_KEY='<non-default-secret-key>'
RUSTFS_ACCESS_KEY="$COMPAT_ACCESS_KEY" RUSTFS_SECRET_KEY="$COMPAT_SECRET_KEY" \
RUSTFS_KMS_ENABLE=true RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS=true \
docker compose -f .docker/compat/docker-compose.rustfs-beta5.yml up -d
.docker/compat/run_rw_compat_stress.sh \
--mode write --endpoint http://127.0.0.1:9100 \
--access-key "$COMPAT_ACCESS_KEY" --secret-key "$COMPAT_SECRET_KEY" \
--bucket compat-beta5-sse-s3 --profile 5g --concurrency 16 \
--encryption sse-s3
```
@@ -0,0 +1,88 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0.
x-minio-env: &minio-env
MINIO_ROOT_USER: "${MINIO_ROOT_USER:-minioadmin}"
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:-minioadmin}"
MINIO_COMPRESSION_ENABLE: "${MINIO_COMPRESSION_ENABLE:-on}"
MINIO_COMPRESSION_ALLOW_ENCRYPTION: "${MINIO_COMPRESSION_ALLOW_ENCRYPTION:-on}"
MINIO_COMPRESSION_EXTENSIONS: "${MINIO_COMPRESSION_EXTENSIONS:-.txt,.log,.csv,.json,.tar,.xml,.bin}"
MINIO_COMPRESSION_MIME_TYPES: "${MINIO_COMPRESSION_MIME_TYPES:-text/*,application/json,application/xml,binary/octet-stream}"
x-minio-node: &minio-node
image: "${MINIO_IMAGE:-quay.io/minio/minio:latest}"
command: server --console-address ":9001" "http://minio{1...4}:9000/data/disk{1...4}"
environment: *minio-env
networks:
- minio-compat-net
restart: unless-stopped
services:
minio-permission-helper:
image: alpine:3.23
command: sh -c "mkdir -p /compat-data && chown -R 1000:1000 /compat-data"
volumes:
- ./data/minio:/compat-data
restart: "no"
minio1:
<<: *minio-node
hostname: minio1
depends_on:
minio-permission-helper:
condition: service_completed_successfully
volumes:
- ./data/minio/node1/disk1:/data/disk1
- ./data/minio/node1/disk2:/data/disk2
- ./data/minio/node1/disk3:/data/disk3
- ./data/minio/node1/disk4:/data/disk4
ports:
- "${MINIO_API_PORT:-9200}:9000"
- "${MINIO_CONSOLE_PORT:-9201}:9001"
minio2:
<<: *minio-node
hostname: minio2
depends_on:
minio-permission-helper:
condition: service_completed_successfully
volumes:
- ./data/minio/node2/disk1:/data/disk1
- ./data/minio/node2/disk2:/data/disk2
- ./data/minio/node2/disk3:/data/disk3
- ./data/minio/node2/disk4:/data/disk4
ports:
- "${MINIO_NODE2_PORT:-9202}:9000"
minio3:
<<: *minio-node
hostname: minio3
depends_on:
minio-permission-helper:
condition: service_completed_successfully
volumes:
- ./data/minio/node3/disk1:/data/disk1
- ./data/minio/node3/disk2:/data/disk2
- ./data/minio/node3/disk3:/data/disk3
- ./data/minio/node3/disk4:/data/disk4
ports:
- "${MINIO_NODE3_PORT:-9203}:9000"
minio4:
<<: *minio-node
hostname: minio4
depends_on:
minio-permission-helper:
condition: service_completed_successfully
volumes:
- ./data/minio/node4/disk1:/data/disk1
- ./data/minio/node4/disk2:/data/disk2
- ./data/minio/node4/disk3:/data/disk3
- ./data/minio/node4/disk4:/data/disk4
ports:
- "${MINIO_NODE4_PORT:-9204}:9000"
networks:
minio-compat-net:
driver: bridge
@@ -0,0 +1,104 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0.
x-rustfs-env: &rustfs-env
RUSTFS_VOLUMES: "http://rustfs-beta5-node{1...4}:9000/data/disk{1...4}"
RUSTFS_ADDRESS: ":9000"
RUSTFS_CONSOLE_ADDRESS: ":9001"
RUSTFS_CONSOLE_ENABLE: "true"
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS: "*"
RUSTFS_ACCESS_KEY: "${RUSTFS_ACCESS_KEY:-rustfsadmin}"
RUSTFS_SECRET_KEY: "${RUSTFS_SECRET_KEY:-rustfsadmin}"
RUSTFS_OBS_LOGGER_LEVEL: "${RUSTFS_OBS_LOGGER_LEVEL:-info}"
RUSTFS_OBS_LOG_DIRECTORY: "/logs"
RUSTFS_COMPRESSION_ENABLED: "${RUSTFS_COMPRESSION_ENABLED:-true}"
RUSTFS_COMPRESSION_EXTENSIONS: "${RUSTFS_COMPRESSION_EXTENSIONS:-.txt,.log,.csv,.json,.tar,.xml,.bin}"
RUSTFS_COMPRESSION_MIME_TYPES: "${RUSTFS_COMPRESSION_MIME_TYPES:-text/*,application/json,application/xml,binary/octet-stream}"
RUSTFS_KMS_ENABLE: "${RUSTFS_KMS_ENABLE:-false}"
RUSTFS_KMS_BACKEND: "${RUSTFS_KMS_BACKEND:-local}"
RUSTFS_KMS_KEY_DIR: "${RUSTFS_KMS_KEY_DIR:-/kms}"
RUSTFS_KMS_DEFAULT_KEY_ID: "${RUSTFS_KMS_DEFAULT_KEY_ID:-compat-key}"
RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS: "${RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS:-false}"
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}"
x-rustfs-node: &rustfs-node
image: "${RUSTFS_BETA5_IMAGE:-rustfs/rustfs:1.0.0-beta.5}"
build:
context: ../..
dockerfile: Dockerfile
args:
RELEASE: "1.0.0-beta.5"
environment: *rustfs-env
depends_on:
rustfs-beta5-permission-helper:
condition: service_completed_successfully
networks:
- rustfs-beta5-net
restart: unless-stopped
services:
rustfs-beta5-permission-helper:
image: alpine:3.23
command: sh -c "mkdir -p /compat-data /kms && chown -R 10001:10001 /compat-data /kms"
volumes:
- ./data/rustfs-beta5:/compat-data
- ./kms/rustfs-compat:/kms
restart: "no"
rustfs-beta5-node1:
<<: *rustfs-node
hostname: rustfs-beta5-node1
volumes:
- ./data/rustfs-beta5/node1/disk1:/data/disk1
- ./data/rustfs-beta5/node1/disk2:/data/disk2
- ./data/rustfs-beta5/node1/disk3:/data/disk3
- ./data/rustfs-beta5/node1/disk4:/data/disk4
- ./data/rustfs-beta5/logs/node1:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_BETA5_API_PORT:-9100}:9000"
- "${RUSTFS_BETA5_CONSOLE_PORT:-9101}:9001"
rustfs-beta5-node2:
<<: *rustfs-node
hostname: rustfs-beta5-node2
volumes:
- ./data/rustfs-beta5/node2/disk1:/data/disk1
- ./data/rustfs-beta5/node2/disk2:/data/disk2
- ./data/rustfs-beta5/node2/disk3:/data/disk3
- ./data/rustfs-beta5/node2/disk4:/data/disk4
- ./data/rustfs-beta5/logs/node2:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_BETA5_NODE2_PORT:-9102}:9000"
rustfs-beta5-node3:
<<: *rustfs-node
hostname: rustfs-beta5-node3
volumes:
- ./data/rustfs-beta5/node3/disk1:/data/disk1
- ./data/rustfs-beta5/node3/disk2:/data/disk2
- ./data/rustfs-beta5/node3/disk3:/data/disk3
- ./data/rustfs-beta5/node3/disk4:/data/disk4
- ./data/rustfs-beta5/logs/node3:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_BETA5_NODE3_PORT:-9103}:9000"
rustfs-beta5-node4:
<<: *rustfs-node
hostname: rustfs-beta5-node4
volumes:
- ./data/rustfs-beta5/node4/disk1:/data/disk1
- ./data/rustfs-beta5/node4/disk2:/data/disk2
- ./data/rustfs-beta5/node4/disk3:/data/disk3
- ./data/rustfs-beta5/node4/disk4:/data/disk4
- ./data/rustfs-beta5/logs/node4:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_BETA5_NODE4_PORT:-9104}:9000"
networks:
rustfs-beta5-net:
driver: bridge
@@ -0,0 +1,115 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0.
x-rustfs-rio-v2-env: &rustfs-rio-v2-env
RUSTFS_VOLUMES: "http://rustfs-rio-v2-node{1...4}:9000/data/disk{1...4}"
RUSTFS_ADDRESS: ":9000"
RUSTFS_CONSOLE_ADDRESS: ":9001"
RUSTFS_CONSOLE_ENABLE: "true"
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS: "*"
RUSTFS_ACCESS_KEY: "${RUSTFS_ACCESS_KEY:-rustfsadmin}"
RUSTFS_SECRET_KEY: "${RUSTFS_SECRET_KEY:-rustfsadmin}"
RUSTFS_OBS_LOGGER_LEVEL: "${RUSTFS_OBS_LOGGER_LEVEL:-info}"
RUSTFS_OBS_LOG_DIRECTORY: "/logs"
RUSTFS_COMPRESSION_ENABLED: "${RUSTFS_COMPRESSION_ENABLED:-true}"
RUSTFS_COMPRESSION_EXTENSIONS: "${RUSTFS_COMPRESSION_EXTENSIONS:-.txt,.log,.csv,.json,.tar,.xml,.bin}"
RUSTFS_COMPRESSION_MIME_TYPES: "${RUSTFS_COMPRESSION_MIME_TYPES:-text/*,application/json,application/xml,binary/octet-stream}"
RUSTFS_KMS_ENABLE: "${RUSTFS_KMS_ENABLE:-false}"
RUSTFS_KMS_BACKEND: "${RUSTFS_KMS_BACKEND:-local}"
RUSTFS_KMS_KEY_DIR: "${RUSTFS_KMS_KEY_DIR:-/kms}"
RUSTFS_KMS_DEFAULT_KEY_ID: "${RUSTFS_KMS_DEFAULT_KEY_ID:-compat-key}"
RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS: "${RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS:-false}"
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}"
x-rustfs-rio-v2-node: &rustfs-rio-v2-node
image: "${RUSTFS_RIO_V2_IMAGE:-rustfs/rustfs:compat-rio-v2}"
entrypoint:
- /bin/sh
- -c
- |
until getent hosts rustfs-rio-v2-node1 >/dev/null &&
getent hosts rustfs-rio-v2-node2 >/dev/null &&
getent hosts rustfs-rio-v2-node3 >/dev/null &&
getent hosts rustfs-rio-v2-node4 >/dev/null; do
sleep 1
done
exec /entrypoint.sh /usr/bin/rustfs
build:
context: ../..
dockerfile: Dockerfile.source
args:
RUSTFS_BUILD_FEATURES: "rio-v2"
environment: *rustfs-rio-v2-env
depends_on:
rustfs-rio-v2-permission-helper:
condition: service_completed_successfully
networks:
- rustfs-rio-v2-net
restart: unless-stopped
services:
rustfs-rio-v2-permission-helper:
image: alpine:3.23
command: sh -c "mkdir -p /compat-data /kms && chown -R 10001:10001 /compat-data /kms"
volumes:
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}:/compat-data
- ./kms/rustfs-compat:/kms
restart: "no"
rustfs-rio-v2-node1:
<<: *rustfs-rio-v2-node
hostname: rustfs-rio-v2-node1
volumes:
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node1/disk1:/data/disk1
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node1/disk2:/data/disk2
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node1/disk3:/data/disk3
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node1/disk4:/data/disk4
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/logs/node1:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_RIO_V2_API_PORT:-9300}:9000"
- "${RUSTFS_RIO_V2_CONSOLE_PORT:-9301}:9001"
rustfs-rio-v2-node2:
<<: *rustfs-rio-v2-node
hostname: rustfs-rio-v2-node2
volumes:
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node2/disk1:/data/disk1
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node2/disk2:/data/disk2
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node2/disk3:/data/disk3
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node2/disk4:/data/disk4
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/logs/node2:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_RIO_V2_NODE2_PORT:-9302}:9000"
rustfs-rio-v2-node3:
<<: *rustfs-rio-v2-node
hostname: rustfs-rio-v2-node3
volumes:
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node3/disk1:/data/disk1
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node3/disk2:/data/disk2
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node3/disk3:/data/disk3
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node3/disk4:/data/disk4
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/logs/node3:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_RIO_V2_NODE3_PORT:-9303}:9000"
rustfs-rio-v2-node4:
<<: *rustfs-rio-v2-node
hostname: rustfs-rio-v2-node4
volumes:
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node4/disk1:/data/disk1
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node4/disk2:/data/disk2
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node4/disk3:/data/disk3
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/node4/disk4:/data/disk4
- ${RUSTFS_RIO_V2_DATASET:-./data/rustfs-rio-v2}/logs/node4:/logs
- ./kms/rustfs-compat:/kms
ports:
- "${RUSTFS_RIO_V2_NODE4_PORT:-9304}:9000"
networks:
rustfs-rio-v2-net:
driver: bridge
+639
View File
@@ -0,0 +1,639 @@
#!/usr/bin/env bash
set -euo pipefail
# High-concurrency S3 read/write stress runner for rio/rio-v2 format compatibility.
# Write a manifest on an old endpoint, then verify the same manifest through rio-v2.
MODE="write"
ENDPOINT=""
ACCESS_KEY="${AWS_ACCESS_KEY_ID:-}"
SECRET_KEY="${AWS_SECRET_ACCESS_KEY:-}"
BUCKET="compat-rw-stress"
REGION="us-east-1"
CONCURRENCY=64
OUT_DIR=""
WORK_DIR=""
MANIFEST=""
PROFILE="200g"
OBJECT_SPEC=""
DATA_PATTERN="compressible"
ENCRYPTION="none"
SSE_KMS_KEY_ID=""
SSE_C_KEY_FILE=""
CLIENT="mc"
AWS_BIN="${AWS_BIN:-aws}"
MC_BIN="${MC_BIN:-}"
KEEP_PAYLOADS=false
DRY_RUN=false
RESUME=false
usage() {
cat <<'USAGE'
Usage:
.docker/compat/run_rw_compat_stress.sh --mode <write|verify|mixed> \
--endpoint <url> --access-key <ak> --secret-key <sk> [options]
Modes:
write Create bucket, upload objects concurrently, and write manifest.csv.
verify Read objects concurrently from --endpoint and verify against manifest.csv.
mixed Write and verify against the same endpoint.
Required:
--endpoint S3 endpoint URL, for example http://127.0.0.1:9100
--access-key S3 access key
--secret-key S3 secret key
Core options:
--bucket Bucket name (default: compat-rw-stress)
--region Region (default: us-east-1)
--concurrency Parallel object operations (default: 64)
--out-dir Output directory (default: target/compat/rw-stress-<timestamp>)
--work-dir Payload scratch directory (default: <out-dir>/payloads)
--manifest Manifest path (default: <out-dir>/manifest.csv for write/mixed)
--profile compact | 5g | 200g (default: 200g)
--object-spec Override profile. Format: size:count,size:count
Example: 1KiB:1024,1MiB:1024,64MiB:512,1GiB:64
--data-pattern compressible | random | mixed (default: compressible)
--keep-payloads Do not delete local payload files after upload
--resume Skip write tasks that already have rows in <out-dir>/tasks/write-rows
--dry-run Print planned tasks and commands without executing S3 operations
--client mc | aws (default: mc)
--mc-bin Path to mc binary (default: first tmp/mc.* or mc in PATH)
--aws-bin Path to aws binary (used with --client aws)
Encryption options:
--encryption none | sse-s3 | sse-kms | sse-c (default: none)
--sse-kms-key-id KMS key id for --encryption sse-kms
--sse-c-key-file Raw 32-byte SSE-C key file for --encryption sse-c
Examples:
# Generate the old RustFS beta5 dataset.
.docker/compat/run_rw_compat_stress.sh \
--mode write --endpoint http://127.0.0.1:9100 \
--access-key rustfsadmin --secret-key rustfsadmin \
--bucket compat-beta5 --concurrency 96
# Verify that dataset after mounting beta5 disks into the rio-v2 compose.
.docker/compat/run_rw_compat_stress.sh \
--mode verify --endpoint http://127.0.0.1:9300 \
--access-key rustfsadmin --secret-key rustfsadmin \
--bucket compat-beta5 --concurrency 96 \
--manifest target/compat/rw-stress-YYYYmmdd-HHMMSS/manifest.csv
# Faster smoke run.
.docker/compat/run_rw_compat_stress.sh \
--mode mixed --endpoint http://127.0.0.1:9300 \
--access-key rustfsadmin --secret-key rustfsadmin \
--profile compact --concurrency 16
USAGE
}
die() {
echo "ERROR: $*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "command not found: $1"
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="$2"; shift 2 ;;
--endpoint) ENDPOINT="$2"; shift 2 ;;
--access-key) ACCESS_KEY="$2"; shift 2 ;;
--secret-key) SECRET_KEY="$2"; shift 2 ;;
--bucket) BUCKET="$2"; shift 2 ;;
--region) REGION="$2"; shift 2 ;;
--concurrency) CONCURRENCY="$2"; shift 2 ;;
--out-dir) OUT_DIR="$2"; shift 2 ;;
--work-dir) WORK_DIR="$2"; shift 2 ;;
--manifest) MANIFEST="$2"; shift 2 ;;
--profile) PROFILE="$2"; shift 2 ;;
--object-spec) OBJECT_SPEC="$2"; shift 2 ;;
--data-pattern) DATA_PATTERN="$2"; shift 2 ;;
--encryption) ENCRYPTION="$2"; shift 2 ;;
--sse-kms-key-id) SSE_KMS_KEY_ID="$2"; shift 2 ;;
--sse-c-key-file) SSE_C_KEY_FILE="$2"; shift 2 ;;
--client) CLIENT="$2"; shift 2 ;;
--mc-bin) MC_BIN="$2"; shift 2 ;;
--aws-bin) AWS_BIN="$2"; shift 2 ;;
--keep-payloads) KEEP_PAYLOADS=true; shift ;;
--resume) RESUME=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage; exit 0 ;;
*) die "unknown arg: $1" ;;
esac
done
}
validate_args() {
[[ "$MODE" =~ ^(write|verify|mixed)$ ]] || die "--mode must be write, verify, or mixed"
[[ "$CLIENT" =~ ^(mc|aws)$ ]] || die "--client must be mc or aws"
[[ "$PROFILE" =~ ^(compact|5g|200g)$ ]] || die "--profile must be compact, 5g, or 200g"
[[ "$DATA_PATTERN" =~ ^(compressible|random|mixed)$ ]] || die "--data-pattern must be compressible, random, or mixed"
[[ "$ENCRYPTION" =~ ^(none|sse-s3|sse-kms|sse-c)$ ]] || die "--encryption must be none, sse-s3, sse-kms, or sse-c"
[[ -n "$ENDPOINT" && -n "$ACCESS_KEY" && -n "$SECRET_KEY" ]] || die "--endpoint/--access-key/--secret-key are required"
[[ "$CONCURRENCY" =~ ^[0-9]+$ && "$CONCURRENCY" -gt 0 ]] || die "--concurrency must be a positive integer"
if [[ "$ENCRYPTION" == "sse-kms" && -z "$SSE_KMS_KEY_ID" ]]; then
die "--sse-kms-key-id is required for --encryption sse-kms"
fi
if [[ "$ENCRYPTION" == "sse-c" ]]; then
[[ -n "$SSE_C_KEY_FILE" && -f "$SSE_C_KEY_FILE" ]] || die "--sse-c-key-file must point to an existing key file"
fi
if [[ "$MODE" == "verify" && -z "$MANIFEST" ]]; then
die "--manifest is required for --mode verify"
fi
}
setup_paths() {
if [[ -z "$OUT_DIR" ]]; then
OUT_DIR="target/compat/rw-stress-$(date +%Y%m%d-%H%M%S)"
fi
if [[ -z "$WORK_DIR" ]]; then
WORK_DIR="$OUT_DIR/payloads"
fi
if [[ -z "$MANIFEST" ]]; then
MANIFEST="$OUT_DIR/manifest.csv"
fi
mkdir -p "$OUT_DIR" "$WORK_DIR" "$OUT_DIR/tasks" "$OUT_DIR/logs"
TASKS_FILE="$OUT_DIR/tasks/tasks.tsv"
WRITE_ROWS_DIR="$OUT_DIR/tasks/write-rows"
VERIFY_ROWS_DIR="$OUT_DIR/tasks/verify-rows"
MC_CONFIG_DIR_LOCAL="$OUT_DIR/mc-config"
MC_ALIAS="compat"
mkdir -p "$WRITE_ROWS_DIR" "$VERIFY_ROWS_DIR"
}
resolve_mc_bin() {
if [[ -n "$MC_BIN" ]]; then
echo "$MC_BIN"
return
fi
local candidate
candidate="$(find tmp -maxdepth 1 -type f -name 'mc.*' -perm -111 2>/dev/null | sort | tail -n 1 || true)"
if [[ -n "$candidate" ]]; then
echo "$candidate"
return
fi
command -v mc 2>/dev/null || true
}
size_to_bytes() {
local raw="$1"
local num unit
if [[ "$raw" =~ ^([0-9]+)(B|KiB|MiB|GiB|KB|MB|GB)?$ ]]; then
num="${BASH_REMATCH[1]}"
unit="${BASH_REMATCH[2]:-B}"
else
die "invalid size: $raw"
fi
case "$unit" in
B) echo "$num" ;;
KiB) echo $((num * 1024)) ;;
MiB) echo $((num * 1024 * 1024)) ;;
GiB) echo $((num * 1024 * 1024 * 1024)) ;;
KB) echo $((num * 1000)) ;;
MB) echo $((num * 1000 * 1000)) ;;
GB) echo $((num * 1000 * 1000 * 1000)) ;;
*) die "invalid size unit: $unit" ;;
esac
}
profile_spec() {
if [[ -n "$OBJECT_SPEC" ]]; then
echo "$OBJECT_SPEC"
return
fi
case "$PROFILE" in
compact)
echo "1KiB:64,1MiB:64,16MiB:16,128MiB:4"
;;
5g)
echo "1KiB:1024,1MiB:255,16MiB:64,64MiB:28,1GiB:2"
;;
200g)
echo "1KiB:1024,1MiB:1024,64MiB:512,1GiB:64,8GiB:12,6GiB:1"
;;
esac
}
content_for_index() {
local index="$1"
case $((index % 3)) in
0) echo "txt|text/plain" ;;
1) echo "json|application/json" ;;
2) echo "bin|binary/octet-stream" ;;
esac
}
generate_tasks() {
local spec item size count bytes i content ext mime key seed index=0
spec="$(profile_spec)"
: > "$TASKS_FILE"
IFS=',' read -r -a items <<< "$spec"
for item in "${items[@]}"; do
item="${item//[[:space:]]/}"
[[ -n "$item" ]] || continue
[[ "$item" =~ ^([^:]+):([0-9]+)$ ]] || die "invalid object spec item: $item"
size="${BASH_REMATCH[1]}"
count="${BASH_REMATCH[2]}"
bytes="$(size_to_bytes "$size")"
for ((i = 1; i <= count; i++)); do
content="$(content_for_index "$index")"
ext="${content%%|*}"
mime="${content#*|}"
key="rw-stress/${size}/obj-$(printf '%06d' "$i").${ext}"
seed="${BUCKET}:${key}:${bytes}"
printf '%s\t%s\t%s\t%s\t%s\n' "$key" "$size" "$bytes" "$mime" "$seed" >> "$TASKS_FILE"
index=$((index + 1))
done
done
}
write_row_file_for_key() {
local key="$1"
echo "$WRITE_ROWS_DIR/${key//\//_}.csv"
}
prepare_write_input() {
WRITE_INPUT="$TASKS_FILE"
if [[ "$RESUME" != "true" ]]; then
return
fi
WRITE_INPUT="$OUT_DIR/tasks/write-input.tsv"
: > "$WRITE_INPUT"
local line key _size _bytes _mime _seed row_file
while IFS= read -r line || [[ -n "$line" ]]; do
IFS=$'\t' read -r key _size _bytes _mime _seed <<< "$line"
row_file="$(write_row_file_for_key "$key")"
[[ -s "$row_file" ]] && continue
printf '%s\n' "$line" >> "$WRITE_INPUT"
done < "$TASKS_FILE"
}
print_plan() {
local total_objects total_bytes profile_label
if [[ -f "$TASKS_FILE" ]]; then
total_objects="$(wc -l < "$TASKS_FILE" | tr -d ' ')"
total_bytes="$(awk -F '\t' '{sum += $3} END {print sum + 0}' "$TASKS_FILE")"
profile_label="$(profile_spec)"
else
total_objects="$(awk 'END {count = NR - 1; if (count < 0) count = 0; print count}' "$MANIFEST")"
total_bytes="$(awk -F ',' 'NR > 1 {sum += $3} END {print sum + 0}' "$MANIFEST")"
profile_label="from manifest"
fi
cat <<PLAN
Mode: $MODE
Endpoint: $ENDPOINT
Bucket: $BUCKET
Profile: $profile_label
Objects: $total_objects
Bytes: $total_bytes
Concurrency: $CONCURRENCY
Encryption: $ENCRYPTION
Client: $CLIENT
Manifest: $MANIFEST
Out dir: $OUT_DIR
Work dir: $WORK_DIR
PLAN
}
aws_base() {
AWS_ACCESS_KEY_ID="$ACCESS_KEY" AWS_SECRET_ACCESS_KEY="$SECRET_KEY" AWS_DEFAULT_REGION="$REGION" \
"$AWS_BIN" --endpoint-url "$ENDPOINT" "$@"
}
mc_base() {
"$MC_BIN" --config-dir "$MC_CONFIG_DIR_LOCAL" "$@"
}
aws_cp_args() {
case "$ENCRYPTION" in
none) ;;
sse-s3) printf '%s\n' "--sse" "AES256" ;;
sse-kms) printf '%s\n' "--sse" "aws:kms" "--sse-kms-key-id" "$SSE_KMS_KEY_ID" ;;
sse-c) printf '%s\n' "--sse-c" "AES256" "--sse-c-key" "fileb://$SSE_C_KEY_FILE" ;;
esac
}
mc_enc_target() {
printf '%s/%s/rw-stress/' "$MC_ALIAS" "$BUCKET"
}
mc_cp_args() {
local target
target="$(mc_enc_target)"
case "$ENCRYPTION" in
none) ;;
sse-s3) printf '%s\n' "--enc-s3" "$target" ;;
sse-kms) printf '%s\n' "--enc-kms" "${target}=${SSE_KMS_KEY_ID}" ;;
sse-c)
local key_b64
key_b64="$(base64 < "$SSE_C_KEY_FILE" | tr -d '\n')"
printf '%s\n' "--enc-c" "${target}=${key_b64}"
;;
esac
}
aws_get_args() {
if [[ "$ENCRYPTION" == "sse-c" ]]; then
printf '%s\n' "--sse-c" "AES256" "--sse-c-key" "fileb://$SSE_C_KEY_FILE"
fi
}
mc_get_args() {
if [[ "$ENCRYPTION" == "sse-c" ]]; then
local target key_b64
target="$(mc_enc_target)"
key_b64="$(base64 < "$SSE_C_KEY_FILE" | tr -d '\n')"
printf '%s\n' "--enc-c" "${target}=${key_b64}"
fi
}
setup_mc_alias() {
if [[ "$CLIENT" != "mc" || "$DRY_RUN" == "true" ]]; then
return
fi
mkdir -p "$MC_CONFIG_DIR_LOCAL"
mc_base alias set "$MC_ALIAS" "$ENDPOINT" "$ACCESS_KEY" "$SECRET_KEY" --api S3v4 --path auto >/dev/null
}
create_bucket_if_needed() {
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY-RUN] create bucket if missing: $BUCKET"
return
fi
if [[ "$CLIENT" == "mc" ]]; then
mc_base mb --ignore-existing --region "$REGION" "$MC_ALIAS/$BUCKET" >/dev/null
return
fi
if aws_base s3api head-bucket --bucket "$BUCKET" >/dev/null 2>&1; then
return
fi
aws_base s3api create-bucket --bucket "$BUCKET" >/dev/null
}
payload_path_for_key() {
local key="$1"
echo "$WORK_DIR/${key//\//_}"
}
generate_payload() {
local file="$1"
local bytes="$2"
local seed="$3"
local pattern="$DATA_PATTERN"
mkdir -p "$(dirname "$file")"
if [[ "$pattern" == "mixed" ]]; then
if [[ $((bytes % 2)) -eq 0 ]]; then
pattern="compressible"
else
pattern="random"
fi
fi
if [[ "$pattern" == "random" ]]; then
head -c "$bytes" /dev/zero | openssl enc -aes-256-ctr -nosalt -pass "pass:$seed" -out "$file"
else
yes "$seed payload-for-rio-compatibility" | tr '\n' ' ' | head -c "$bytes" > "$file"
fi
}
sha256_file() {
shasum -a 256 "$1" | awk '{print $1}'
}
sha256_object() {
local key="$1"
shift
if [[ "$CLIENT" == "mc" ]]; then
mc_base cat "$@" "$MC_ALIAS/$BUCKET/$key" | shasum -a 256 | awk '{print $1}'
else
aws_base s3 cp "s3://$BUCKET/$key" - --no-progress "$@" | shasum -a 256 | awk '{print $1}'
fi
}
collect_args() {
local generator="$1"
extra_args=()
while IFS= read -r arg; do
extra_args+=("$arg")
done < <("$generator")
}
write_one() {
local line="$1"
local key size bytes mime seed file sha row_file
local -a extra_args
IFS=$'\t' read -r key size bytes mime seed <<< "$line"
file="$(payload_path_for_key "$key")"
row_file="$(write_row_file_for_key "$key")"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY-RUN] upload $bytes bytes to s3://$BUCKET/$key content-type=$mime"
printf '%s,%s,%s,%s,%s,%s\n' "$key" "$size" "$bytes" "$mime" "DRY_RUN" "$ENCRYPTION" > "$row_file"
return
fi
generate_payload "$file" "$bytes" "$seed"
sha="$(sha256_file "$file")"
if [[ "$CLIENT" == "mc" ]]; then
collect_args mc_cp_args
mc_base cp --quiet --attr "Content-Type=$mime" ${extra_args[@]+"${extra_args[@]}"} "$file" "$MC_ALIAS/$BUCKET/$key" \
> "$OUT_DIR/logs/${key//\//_}.put.log" 2>&1
else
collect_args aws_cp_args
aws_base s3 cp "$file" "s3://$BUCKET/$key" --no-progress --content-type "$mime" ${extra_args[@]+"${extra_args[@]}"} \
> "$OUT_DIR/logs/${key//\//_}.put.log" 2>&1
fi
printf '%s,%s,%s,%s,%s,%s\n' "$key" "$size" "$bytes" "$mime" "$sha" "$ENCRYPTION" > "$row_file"
if [[ "$KEEP_PAYLOADS" != "true" ]]; then
rm -f "$file"
fi
}
verify_one() {
local line="$1"
local key size bytes mime expected encryption actual row_file
local -a extra_args
IFS=',' read -r key size bytes mime expected encryption <<< "$line"
row_file="$VERIFY_ROWS_DIR/${key//\//_}.csv"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY-RUN] verify s3://$BUCKET/$key expected=$expected"
printf '%s,%s,%s,%s,%s,%s,%s\n' "$key" "$size" "$bytes" "$mime" "$expected" "DRY_RUN" "dry-run" > "$row_file"
return
fi
if [[ "$CLIENT" == "mc" ]]; then
collect_args mc_get_args
else
collect_args aws_get_args
fi
if ! actual="$(sha256_object "$key" ${extra_args[@]+"${extra_args[@]}"})"; then
printf '%s,%s,%s,%s,%s,%s,%s\n' "$key" "$size" "$bytes" "$mime" "$expected" "ERROR" "download failed" > "$row_file"
return 1
fi
if [[ "$actual" == "$expected" ]]; then
printf '%s,%s,%s,%s,%s,%s,%s\n' "$key" "$size" "$bytes" "$mime" "$expected" "$actual" "ok" > "$row_file"
else
printf '%s,%s,%s,%s,%s,%s,%s\n' "$key" "$size" "$bytes" "$mime" "$expected" "$actual" "sha256-mismatch" > "$row_file"
return 1
fi
}
run_parallel_tasks() {
local action="$1"
local input_file="$2"
local failure_file="$OUT_DIR/tasks/${action}.failed"
local line
rm -f "$failure_file"
while IFS= read -r line || [[ -n "$line" ]]; do
while [[ "$(jobs -rp | wc -l | tr -d ' ')" -ge "$CONCURRENCY" ]]; do
sleep 0.2
done
({
if [[ "$action" == "write" ]]; then
write_one "$line"
else
verify_one "$line"
fi
} || touch "$failure_file") &
done < "$input_file"
wait
[[ ! -f "$failure_file" ]]
}
combine_write_manifest() {
echo "key,size,bytes,content_type,sha256,encryption" > "$MANIFEST"
find "$WRITE_ROWS_DIR" -type f -name '*.csv' -print0 | sort -z | xargs -0 cat >> "$MANIFEST"
local expected actual
expected="$(wc -l < "$TASKS_FILE" | tr -d ' ')"
actual="$(( $(wc -l < "$MANIFEST" | tr -d ' ') - 1 ))"
if [[ "$actual" -ne "$expected" ]]; then
die "manifest row count mismatch: expected $expected, got $actual"
fi
}
prepare_verify_input() {
VERIFY_INPUT="$OUT_DIR/tasks/verify-input.csv"
tail -n +2 "$MANIFEST" > "$VERIFY_INPUT"
}
combine_verify_summary() {
VERIFY_SUMMARY="$OUT_DIR/verify-summary.csv"
echo "key,size,bytes,content_type,expected_sha256,actual_sha256,status" > "$VERIFY_SUMMARY"
find "$VERIFY_ROWS_DIR" -type f -name '*.csv' -print0 | sort -z | xargs -0 cat >> "$VERIFY_SUMMARY"
local failed
if [[ "$DRY_RUN" == "true" ]]; then
echo "Verification dry run complete. Summary: $VERIFY_SUMMARY"
return
fi
failed="$(awk -F ',' 'NR > 1 && $7 != "ok" {count++} END {print count + 0}' "$VERIFY_SUMMARY")"
local expected actual
expected="$(wc -l < "$VERIFY_INPUT" | tr -d ' ')"
actual="$(( $(wc -l < "$VERIFY_SUMMARY" | tr -d ' ') - 1 ))"
if [[ "$actual" -ne "$expected" ]]; then
echo "Verification row count mismatch: expected $expected, got $actual" >&2
return 1
fi
if [[ "$failed" -ne 0 ]]; then
echo "Verification failed: $failed object(s). See $VERIFY_SUMMARY" >&2
return 1
fi
echo "Verification passed. Summary: $VERIFY_SUMMARY"
}
export_functions() {
export ENDPOINT ACCESS_KEY SECRET_KEY BUCKET REGION ENCRYPTION SSE_KMS_KEY_ID SSE_C_KEY_FILE AWS_BIN
export CLIENT MC_BIN MC_CONFIG_DIR_LOCAL MC_ALIAS
export WORK_DIR OUT_DIR WRITE_ROWS_DIR VERIFY_ROWS_DIR DATA_PATTERN KEEP_PAYLOADS DRY_RUN
export -f aws_base mc_base aws_cp_args aws_get_args mc_enc_target mc_cp_args mc_get_args payload_path_for_key generate_payload sha256_file sha256_object write_one verify_one
}
main() {
parse_args "$@"
validate_args
setup_paths
if [[ "$DRY_RUN" != "true" ]]; then
if [[ "$CLIENT" == "mc" ]]; then
MC_BIN="$(resolve_mc_bin)"
[[ -n "$MC_BIN" ]] || die "mc binary not found; pass --mc-bin or put mc in PATH"
require_cmd "$MC_BIN"
else
require_cmd "$AWS_BIN"
fi
require_cmd shasum
require_cmd head
require_cmd yes
require_cmd openssl
elif [[ "$CLIENT" == "mc" ]]; then
MC_BIN="$(resolve_mc_bin)"
fi
setup_mc_alias
if [[ "$MODE" == "write" || "$MODE" == "mixed" ]]; then
local write_failed=false
generate_tasks
prepare_write_input
print_plan
create_bucket_if_needed
export_functions
if ! run_parallel_tasks write "$WRITE_INPUT"; then
write_failed=true
fi
combine_write_manifest
echo "Write manifest: $MANIFEST"
if [[ "$write_failed" == "true" ]]; then
die "one or more write tasks failed; see $OUT_DIR/logs"
fi
fi
if [[ "$MODE" == "verify" || "$MODE" == "mixed" ]]; then
local verify_failed=false
[[ -f "$MANIFEST" ]] || die "manifest not found: $MANIFEST"
if [[ "$MODE" == "verify" ]]; then
cp "$MANIFEST" "$OUT_DIR/manifest.csv"
MANIFEST="$OUT_DIR/manifest.csv"
fi
prepare_verify_input
print_plan
export_functions
if ! run_parallel_tasks verify "$VERIFY_INPUT"; then
verify_failed=true
fi
combine_verify_summary
if [[ "$verify_failed" == "true" ]]; then
die "one or more verify tasks failed; see $VERIFY_SUMMARY"
fi
fi
}
main "$@"
@@ -0,0 +1,52 @@
services:
rustfs:
image: rustfs/rustfs:1.0.0-alpha.99-glibc
container_name: rustfs-issue-2715-test
security_opt:
- "no-new-privileges:true"
ports:
- "19000:9000"
- "19001:9001"
environment:
- RUSTFS_VOLUMES=/data/rustfs{0...8}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_CORS_ALLOWED_ORIGINS=*
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=admin
- RUSTFS_SECRET_KEY=admin
- RUSTFS_OBS_LOGGER_LEVEL=info
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
- RUSTFS_OBS_PROFILING_ENDPOINT=http://pyroscope:4040
- RUSTFS_STORAGE_CLASS_STANDARD=EC:2
- RUSTFS_STORAGE_CLASS_RRS=EC:1
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true
- RUSTFS_OBS_LOG_DIRECTORY=/opt/rustfs/logs
extra_hosts:
- "otel-collector:host-gateway"
- "pyroscope:host-gateway"
volumes:
- ./deploy/data/issue-2715/rustfs0:/data/rustfs0
- ./deploy/data/issue-2715/rustfs1:/data/rustfs1
- ./deploy/data/issue-2715/rustfs2:/data/rustfs2
- ./deploy/data/issue-2715/rustfs3:/data/rustfs3
- ./deploy/data/issue-2715/rustfs4:/data/rustfs4
- ./deploy/data/issue-2715/rustfs5:/data/rustfs5
- ./deploy/data/issue-2715/rustfs6:/data/rustfs6
- ./deploy/data/issue-2715/rustfs7:/data/rustfs7
- ./deploy/data/issue-2715/rustfs8:/data/rustfs8
- ./deploy/logs/issue-2715:/opt/rustfs/logs
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"sh",
"-c",
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health"
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
+1
View File
@@ -0,0 +1 @@
data/
+106
View File
@@ -0,0 +1,106 @@
# Issue 2815 Local Docker Verification
## Purpose
This directory contains the local distributed Docker verification assets used to validate issue `#2815` against the current source build.
The target behavior is:
- 4-node distributed cluster starts successfully
- `/health/ready` becomes reachable on each node
- logs no longer contain `storage_info failed: Io error: wrong msgpack marker FixArray(1)`
- internode RPC authentication succeeds with an explicit non-default RPC secret
## Files
- `docker-compose.yml`: 4-node distributed cluster using a locally built image
## Data Directories
Create the bind-mount directories before `docker compose up`:
```bash
mkdir -p .docker/test/issues-2815/data/rustfs{1..4}-disk{0..3}
```
## Build
Apple Silicon / arm64 host:
```bash
docker build --platform linux/arm64 -f Dockerfile.source -t rustfs-issue-2815-local .
```
If you intentionally want amd64 emulation:
```bash
docker build --platform linux/amd64 -f Dockerfile.source -t rustfs-issue-2815-local .
```
## Run
```bash
docker compose -f .docker/test/issues-2815/docker-compose.yml up -d
```
If the image platform is not `linux/arm64`, align compose explicitly:
```bash
RUSTFS_DOCKER_PLATFORM=linux/amd64 docker compose -f .docker/test/issues-2815/docker-compose.yml up -d
```
## Health Checks
Container-level healthcheck is now included and probes:
```bash
curl -fsS http://127.0.0.1:9000/health
```
Manual checks:
```bash
curl -i http://127.0.0.1:9101/health/ready
curl -i http://127.0.0.1:9102/health/ready
curl -i http://127.0.0.1:9103/health/ready
curl -i http://127.0.0.1:9104/health/ready
```
## RPC Secret Requirement
The current source build no longer reproduces the original `FixArray(1)` decode error from issue `#2815`.
Earlier local Docker attempts failed during erasure bootstrap with:
```text
No valid auth token
store init failed to load formats after 10 retries: erasure read quorum
```
Root cause:
- RPC authentication rejects the default secret `rustfsadmin`
- distributed local Docker validation therefore needs an explicit non-default secret
This compose now sets both:
- `RUSTFS_SECRET_KEY=issue-2815-secret`
- `RUSTFS_RPC_SECRET=issue-2815-rpc-secret`
With those values in place, the current 4-node local Docker cluster reaches healthy state and `/health/ready` returns `200`.
In other words:
- `RUSTFS_ACCESS_KEY` may still be `rustfsadmin` for local service credentials if desired
- `RUSTFS_SECRET_KEY` can still be used for service credentials
- but RPC authentication must not resolve to the default secret value `rustfsadmin`
- if `RUSTFS_RPC_SECRET` is unset, the code falls back to `RUSTFS_SECRET_KEY`
- so at least one of them must provide a non-default shared secret for internode RPC signing
## Suggested Debug Commands
```bash
docker compose -f .docker/test/issues-2815/docker-compose.yml ps
docker compose -f .docker/test/issues-2815/docker-compose.yml logs --no-color --tail=200
docker compose -f .docker/test/issues-2815/docker-compose.yml down -v
```
+120
View File
@@ -0,0 +1,120 @@
services:
rustfs1:
image: rustfs-issue-2815-local
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
hostname: rustfs1
container_name: rustfs-issue-2815-rustfs1
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: "rustfsadmin"
RUSTFS_SECRET_KEY: "issue-2815-secret"
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
RUSTFS_CONSOLE_ENABLE: "false"
RUST_LOG: "info"
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
volumes:
- ./data/rustfs1-disk0:/data/rustfs0
- ./data/rustfs1-disk1:/data/rustfs1
- ./data/rustfs1-disk2:/data/rustfs2
- ./data/rustfs1-disk3:/data/rustfs3
networks: [rustfs-issue-2815-net]
ports:
- "9101:9000"
healthcheck:
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
interval: 15s
timeout: 5s
retries: 8
start_period: 30s
rustfs2:
image: rustfs-issue-2815-local
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
hostname: rustfs2
container_name: rustfs-issue-2815-rustfs2
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: "rustfsadmin"
RUSTFS_SECRET_KEY: "issue-2815-secret"
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
RUSTFS_CONSOLE_ENABLE: "false"
RUST_LOG: "info"
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
volumes:
- ./data/rustfs2-disk0:/data/rustfs0
- ./data/rustfs2-disk1:/data/rustfs1
- ./data/rustfs2-disk2:/data/rustfs2
- ./data/rustfs2-disk3:/data/rustfs3
networks: [rustfs-issue-2815-net]
ports:
- "9102:9000"
healthcheck:
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
interval: 15s
timeout: 5s
retries: 8
start_period: 30s
rustfs3:
image: rustfs-issue-2815-local
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
hostname: rustfs3
container_name: rustfs-issue-2815-rustfs3
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: "rustfsadmin"
RUSTFS_SECRET_KEY: "issue-2815-secret"
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
RUSTFS_CONSOLE_ENABLE: "false"
RUST_LOG: "info"
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
volumes:
- ./data/rustfs3-disk0:/data/rustfs0
- ./data/rustfs3-disk1:/data/rustfs1
- ./data/rustfs3-disk2:/data/rustfs2
- ./data/rustfs3-disk3:/data/rustfs3
networks: [rustfs-issue-2815-net]
ports:
- "9103:9000"
healthcheck:
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
interval: 15s
timeout: 5s
retries: 8
start_period: 30s
rustfs4:
image: rustfs-issue-2815-local
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
hostname: rustfs4
container_name: rustfs-issue-2815-rustfs4
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: "rustfsadmin"
RUSTFS_SECRET_KEY: "issue-2815-secret"
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
RUSTFS_CONSOLE_ENABLE: "false"
RUST_LOG: "info"
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
volumes:
- ./data/rustfs4-disk0:/data/rustfs0
- ./data/rustfs4-disk1:/data/rustfs1
- ./data/rustfs4-disk2:/data/rustfs2
- ./data/rustfs4-disk3:/data/rustfs3
networks: [rustfs-issue-2815-net]
ports:
- "9104:9000"
healthcheck:
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
interval: 15s
timeout: 5s
retries: 8
start_period: 30s
networks:
rustfs-issue-2815-net:
name: rustfs-issue-2815-net
+183
View File
@@ -0,0 +1,183 @@
# Site Replication Docker Compose Test
## Purpose
This directory contains a local three-site RustFS site replication check. It is intended to verify the admin site-replication flow against real containers:
- three independent RustFS sites start successfully
- the MinIO-compatible `mc admin replicate add` command configures all three sites
- a bucket and object written to site 1 are replicated to site 2 and site 3
- `mc admin replicate status` can read the resulting site-replication state
The compose file uses named volumes so the test does not require preparing host bind-mount directories.
Because Docker named volumes usually share the same physical device in local desktop environments, this test compose defaults `RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true`. Keep that setting limited to local test and CI environments.
## Files
- `docker-compose.yml`: three RustFS sites, a volume permission helper, and a one-shot setup/check container
- `run-object-flow-check.sh`: host-side upload/download verification for replicated 10 MiB to 100 MiB objects
## Ports
Default host endpoints:
- Site 1 API: `http://127.0.0.1:9000`
- Site 1 Console: `http://127.0.0.1:9001`
- Site 2 API: `http://127.0.0.1:9010`
- Site 2 Console: `http://127.0.0.1:9011`
- Site 3 API: `http://127.0.0.1:9020`
- Site 3 Console: `http://127.0.0.1:9021`
Default credentials are `rustfsadmin` / `rustfsadmin`. These are for local testing only.
## Run
From the repository root:
```bash
docker compose -f .docker/test/site-replication/docker-compose.yml up
```
To test a locally built image instead of Docker Hub `rustfs/rustfs:latest`, set `RUSTFS_SITE_REPL_IMAGE`:
```bash
docker build -f Dockerfile.source -t rustfs-site-repl-local:latest .
RUSTFS_SITE_REPL_IMAGE=rustfs-site-repl-local:latest \
docker compose -f .docker/test/site-replication/docker-compose.yml up
```
For a detached run:
```bash
docker compose -f .docker/test/site-replication/docker-compose.yml up -d
docker compose -f .docker/test/site-replication/docker-compose.yml logs -f site-replication-setup
```
## Test Flow
The compose stack performs these steps:
1. `site-replication-volume-permission-helper` fixes ownership on all named volumes for the RustFS runtime user.
2. `rustfs-site1`, `rustfs-site2`, and `rustfs-site3` start as separate RustFS sites.
3. Each site exposes its S3 API and Console on a unique host port.
4. Health checks wait for `/health/ready` on each RustFS container.
5. `site-replication-setup` configures `mc` aliases for all three sites.
6. The setup container waits until `mc admin info` succeeds for all sites.
7. It runs:
```bash
mc admin replicate add site1 site2 site3
```
8. It creates the test bucket on site 1 and uploads `from-site1.txt`.
9. It polls site 2 and site 3 until the replicated object is visible.
10. It prints `mc admin replicate status site1`.
The setup container exits with status `0` only after the object replication check passes.
## Object Flow Check
After the compose setup succeeds, run the larger object flow check from the repository root:
```bash
.docker/test/site-replication/run-object-flow-check.sh
```
The script creates five local files and uploads them from different sites:
- 10 MiB from site 1
- 25 MiB from site 2
- 50 MiB from site 3
- 75 MiB from site 1
- 100 MiB from site 2
For each uploaded object, the script waits for replication to the other two sites, downloads the object from those sites, and verifies both byte size and SHA-256 checksum. It uses a temporary `mc` config directory, so it does not overwrite existing host aliases.
The default bucket is `site-repl-flow-check`. Override it when needed:
```bash
RUSTFS_SITE_REPL_FLOW_BUCKET='site-repl-large-flow' \
.docker/test/site-replication/run-object-flow-check.sh
```
The script keeps uploaded objects under a timestamped prefix. Override the prefix for repeatable runs:
```bash
RUSTFS_SITE_REPL_FLOW_PREFIX='manual-check-001' \
.docker/test/site-replication/run-object-flow-check.sh
```
If replication is slow on the local machine, increase polling:
```bash
RUSTFS_SITE_REPL_WAIT_ATTEMPTS=180 \
RUSTFS_SITE_REPL_WAIT_SLEEP_SECONDS=2 \
.docker/test/site-replication/run-object-flow-check.sh
```
## Optional Settings
Override local test credentials:
```bash
RUSTFS_SITE_REPL_ACCESS_KEY='localadmin' \
RUSTFS_SITE_REPL_SECRET_KEY='localadmin-secret' \
docker compose -f .docker/test/site-replication/docker-compose.yml up
```
Use a different test bucket:
```bash
RUSTFS_SITE_REPL_BUCKET='site-repl-check' \
docker compose -f .docker/test/site-replication/docker-compose.yml up
```
Enable ILM expiry rule replication during site setup:
```bash
RUSTFS_SITE_REPL_ENABLE_ILM_EXPIRY=true \
docker compose -f .docker/test/site-replication/docker-compose.yml up
```
Use this only when the test needs lifecycle expiry metadata included in site replication.
## Manual Checks
After the setup container succeeds, you can inspect the sites with `mc` from the host:
```bash
mc alias set site1 http://127.0.0.1:9000 rustfsadmin rustfsadmin
mc alias set site2 http://127.0.0.1:9010 rustfsadmin rustfsadmin
mc alias set site3 http://127.0.0.1:9020 rustfsadmin rustfsadmin
mc admin replicate info site1
mc admin replicate status site1
mc stat site2/site-repl-demo/from-site1.txt
mc stat site3/site-repl-demo/from-site1.txt
```
After larger object flow checks, replication should converge without a growing queue:
```bash
mc admin replicate status site1
mc admin replicate status site2
mc admin replicate status site3
```
Useful Docker commands:
```bash
docker compose -f .docker/test/site-replication/docker-compose.yml ps
docker compose -f .docker/test/site-replication/docker-compose.yml logs --no-color --tail=200
docker compose -f .docker/test/site-replication/docker-compose.yml logs --no-color site-replication-setup
```
## Cleanup
Remove containers and named volumes:
```bash
docker compose -f .docker/test/site-replication/docker-compose.yml down -v
```
Use `down -v` before rerunning the full setup from scratch. Site replication state is persisted in the named volumes, so rerunning without deleting volumes may attempt to add an already-configured replication topology.
@@ -0,0 +1,243 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
services:
rustfs-site1:
image: ${RUSTFS_SITE_REPL_IMAGE:-rustfs/rustfs:latest}
container_name: rustfs-site-repl-1
security_opt:
- "no-new-privileges:true"
ports:
- "9000:9000"
- "9001:9001"
environment:
- RUSTFS_VOLUMES=/data/rustfs{0...3}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=${RUSTFS_SITE_REPL_ACCESS_KEY:-rustfsadmin}
- RUSTFS_SECRET_KEY=${RUSTFS_SITE_REPL_SECRET_KEY:-rustfsadmin}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_SITE_REPL_LOG_LEVEL:-info}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
volumes:
- site1_data_0:/data/rustfs0
- site1_data_1:/data/rustfs1
- site1_data_2:/data/rustfs2
- site1_data_3:/data/rustfs3
- site1_logs:/app/logs
networks:
- rustfs-site-replication
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9000/health/ready"]
interval: 10s
timeout: 5s
retries: 30
start_period: 20s
depends_on:
site-replication-volume-permission-helper:
condition: service_completed_successfully
rustfs-site2:
image: ${RUSTFS_SITE_REPL_IMAGE:-rustfs/rustfs:latest}
container_name: rustfs-site-repl-2
security_opt:
- "no-new-privileges:true"
ports:
- "9010:9000"
- "9011:9001"
environment:
- RUSTFS_VOLUMES=/data/rustfs{0...3}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=${RUSTFS_SITE_REPL_ACCESS_KEY:-rustfsadmin}
- RUSTFS_SECRET_KEY=${RUSTFS_SITE_REPL_SECRET_KEY:-rustfsadmin}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_SITE_REPL_LOG_LEVEL:-info}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
volumes:
- site2_data_0:/data/rustfs0
- site2_data_1:/data/rustfs1
- site2_data_2:/data/rustfs2
- site2_data_3:/data/rustfs3
- site2_logs:/app/logs
networks:
- rustfs-site-replication
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9000/health/ready"]
interval: 10s
timeout: 5s
retries: 30
start_period: 20s
depends_on:
site-replication-volume-permission-helper:
condition: service_completed_successfully
rustfs-site3:
image: ${RUSTFS_SITE_REPL_IMAGE:-rustfs/rustfs:latest}
container_name: rustfs-site-repl-3
security_opt:
- "no-new-privileges:true"
ports:
- "9020:9000"
- "9021:9001"
environment:
- RUSTFS_VOLUMES=/data/rustfs{0...3}
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=${RUSTFS_SITE_REPL_ACCESS_KEY:-rustfsadmin}
- RUSTFS_SECRET_KEY=${RUSTFS_SITE_REPL_SECRET_KEY:-rustfsadmin}
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_SITE_REPL_LOG_LEVEL:-info}
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
volumes:
- site3_data_0:/data/rustfs0
- site3_data_1:/data/rustfs1
- site3_data_2:/data/rustfs2
- site3_data_3:/data/rustfs3
- site3_logs:/app/logs
networks:
- rustfs-site-replication
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9000/health/ready"]
interval: 10s
timeout: 5s
retries: 30
start_period: 20s
depends_on:
site-replication-volume-permission-helper:
condition: service_completed_successfully
site-replication-setup:
image: minio/mc:latest
container_name: rustfs-site-repl-setup
depends_on:
rustfs-site1:
condition: service_healthy
rustfs-site2:
condition: service_healthy
rustfs-site3:
condition: service_healthy
environment:
- RUSTFS_SITE_REPL_ACCESS_KEY=${RUSTFS_SITE_REPL_ACCESS_KEY:-rustfsadmin}
- RUSTFS_SITE_REPL_SECRET_KEY=${RUSTFS_SITE_REPL_SECRET_KEY:-rustfsadmin}
- RUSTFS_SITE_REPL_BUCKET=${RUSTFS_SITE_REPL_BUCKET:-site-repl-demo}
- RUSTFS_SITE_REPL_ENABLE_ILM_EXPIRY=${RUSTFS_SITE_REPL_ENABLE_ILM_EXPIRY:-false}
networks:
- rustfs-site-replication
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
access_key="$${RUSTFS_SITE_REPL_ACCESS_KEY}"
secret_key="$${RUSTFS_SITE_REPL_SECRET_KEY}"
bucket="$${RUSTFS_SITE_REPL_BUCKET}"
mc alias set site1 http://rustfs-site1:9000 "$${access_key}" "$${secret_key}"
mc alias set site2 http://rustfs-site2:9000 "$${access_key}" "$${secret_key}"
mc alias set site3 http://rustfs-site3:9000 "$${access_key}" "$${secret_key}"
for site in site1 site2 site3; do
until mc ls "$${site}" >/dev/null 2>&1; do
echo "waiting for $${site} S3 API"
sleep 2
done
done
ilm_flag=""
if [ "$${RUSTFS_SITE_REPL_ENABLE_ILM_EXPIRY}" = "true" ]; then
ilm_flag="--replicate-ilm-expiry"
fi
echo "configuring 3-site replication"
if ! mc admin replicate add site1 site2 site3 $${ilm_flag}; then
echo "replicate add failed; showing current replication info before exiting"
mc admin replicate info site1 || true
exit 1
fi
mc mb --ignore-existing "site1/$${bucket}"
printf 'rustfs 3-site replication check\n' > /tmp/site-replication-check.txt
mc cp /tmp/site-replication-check.txt "site1/$${bucket}/from-site1.txt"
for site in site2 site3; do
for attempt in $$(seq 1 60); do
if mc stat "$${site}/$${bucket}/from-site1.txt" >/dev/null 2>&1; then
echo "$${site} received replicated object"
break
fi
if [ "$${attempt}" = "60" ]; then
echo "$${site} did not receive replicated object in time"
exit 1
fi
sleep 2
done
done
mc admin replicate status site1
echo "3-site replication example is ready"
restart: "no"
site-replication-volume-permission-helper:
image: alpine:3.23.4
volumes:
- site1_data_0:/site1/data0
- site1_data_1:/site1/data1
- site1_data_2:/site1/data2
- site1_data_3:/site1/data3
- site1_logs:/site1/logs
- site2_data_0:/site2/data0
- site2_data_1:/site2/data1
- site2_data_2:/site2/data2
- site2_data_3:/site2/data3
- site2_logs:/site2/logs
- site3_data_0:/site3/data0
- site3_data_1:/site3/data1
- site3_data_2:/site3/data2
- site3_data_3:/site3/data3
- site3_logs:/site3/logs
command: >
sh -c "
chown -R 10001:10001 /site1 /site2 /site3 &&
echo 'site replication volume permissions fixed'
"
networks:
- rustfs-site-replication
restart: "no"
networks:
rustfs-site-replication:
volumes:
site1_data_0:
site1_data_1:
site1_data_2:
site1_data_3:
site1_logs:
site2_data_0:
site2_data_1:
site2_data_2:
site2_data_3:
site2_logs:
site3_data_0:
site3_data_1:
site3_data_2:
site3_data_3:
site3_logs:
+228
View File
@@ -0,0 +1,228 @@
#!/bin/sh
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -eu
ACCESS_KEY="${RUSTFS_SITE_REPL_ACCESS_KEY:-rustfsadmin}"
SECRET_KEY="${RUSTFS_SITE_REPL_SECRET_KEY:-rustfsadmin}"
BUCKET="${RUSTFS_SITE_REPL_FLOW_BUCKET:-site-repl-flow-check}"
DELETE_BUCKET="${RUSTFS_SITE_REPL_DELETE_BUCKET:-site-repl-delete-$(date +%Y%m%d-%H%M%S)-$$}"
PREFIX="${RUSTFS_SITE_REPL_FLOW_PREFIX:-flow-$(date +%Y%m%d-%H%M%S)}"
WAIT_ATTEMPTS="${RUSTFS_SITE_REPL_WAIT_ATTEMPTS:-90}"
WAIT_SLEEP_SECONDS="${RUSTFS_SITE_REPL_WAIT_SLEEP_SECONDS:-2}"
SITE1_ENDPOINT="${RUSTFS_SITE1_ENDPOINT:-http://127.0.0.1:9000}"
SITE2_ENDPOINT="${RUSTFS_SITE2_ENDPOINT:-http://127.0.0.1:9010}"
SITE3_ENDPOINT="${RUSTFS_SITE3_ENDPOINT:-http://127.0.0.1:9020}"
command_exists() {
command -v "$1" >/dev/null 2>&1
}
require_command() {
if ! command_exists "$1"; then
echo "missing required command: $1" >&2
exit 1
fi
}
checksum_file() {
if command_exists sha256sum; then
sha256sum "$1" | awk '{print $1}'
elif command_exists shasum; then
shasum -a 256 "$1" | awk '{print $1}'
else
echo "missing required command: sha256sum or shasum" >&2
exit 1
fi
}
site_endpoint() {
case "$1" in
site1) printf '%s\n' "$SITE1_ENDPOINT" ;;
site2) printf '%s\n' "$SITE2_ENDPOINT" ;;
site3) printf '%s\n' "$SITE3_ENDPOINT" ;;
*) echo "unknown site alias: $1" >&2; exit 1 ;;
esac
}
other_sites() {
case "$1" in
site1) printf '%s\n' "site2 site3" ;;
site2) printf '%s\n' "site1 site3" ;;
site3) printf '%s\n' "site1 site2" ;;
*) echo "unknown source site: $1" >&2; exit 1 ;;
esac
}
wait_for_object() {
site="$1"
object="$2"
attempt=1
while [ "$attempt" -le "$WAIT_ATTEMPTS" ]; do
if mc stat "$site/$BUCKET/$object" >/dev/null 2>&1; then
return 0
fi
sleep "$WAIT_SLEEP_SECONDS"
attempt=$((attempt + 1))
done
echo "object was not replicated in time: $site/$BUCKET/$object" >&2
return 1
}
wait_for_bucket() {
site="$1"
bucket="${2:-$BUCKET}"
attempt=1
while [ "$attempt" -le "$WAIT_ATTEMPTS" ]; do
if mc stat "$site/$bucket" >/dev/null 2>&1; then
return 0
fi
sleep "$WAIT_SLEEP_SECONDS"
attempt=$((attempt + 1))
done
echo "bucket was not replicated in time: $site/$bucket" >&2
return 1
}
wait_for_bucket_delete() {
site="$1"
bucket="$2"
attempt=1
while [ "$attempt" -le "$WAIT_ATTEMPTS" ]; do
if result="$(mc stat --json "$site/$bucket" 2>&1)"; then
:
else
case "$result" in
*NoSuchBucket*) return 0 ;;
esac
fi
sleep "$WAIT_SLEEP_SECONDS"
attempt=$((attempt + 1))
done
echo "bucket deletion was not replicated in time: $site/$bucket" >&2
return 1
}
require_command mc
require_command dd
require_command awk
require_command date
require_command mktemp
require_command tr
require_command wc
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/rustfs-site-repl-flow.XXXXXX")"
MC_CONFIG_DIR="$WORK_DIR/mc"
export MC_CONFIG_DIR
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT INT TERM
mkdir -p "$MC_CONFIG_DIR" "$WORK_DIR/src" "$WORK_DIR/downloads"
for site in site1 site2 site3; do
mc alias set "$site" "$(site_endpoint "$site")" "$ACCESS_KEY" "$SECRET_KEY" >/dev/null
done
for site in site1 site2 site3; do
echo "checking S3 API for $site"
mc ls "$site" >/dev/null
done
echo "ensuring bucket exists on site1: $BUCKET"
mc mb --ignore-existing "site1/$BUCKET" >/dev/null
for site in site1 site2 site3; do
wait_for_bucket "$site"
done
cat <<'EOF' | while read -r size_mb source_site object_name; do
10 site1 object-010m.bin
25 site2 object-025m.bin
50 site3 object-050m.bin
75 site1 object-075m.bin
100 site2 object-100m.bin
EOF
src_file="$WORK_DIR/src/$object_name"
object="$PREFIX/$object_name"
echo "creating ${size_mb}MiB file: $object_name"
dd if=/dev/urandom of="$src_file" bs=1048576 count="$size_mb" >/dev/null 2>&1
src_checksum="$(checksum_file "$src_file")"
src_bytes="$(wc -c < "$src_file" | tr -d ' ')"
echo "uploading $object_name to $source_site ($src_bytes bytes)"
mc cp "$src_file" "$source_site/$BUCKET/$object" >/dev/null
for site in $(other_sites "$source_site"); do
wait_for_object "$site" "$object"
dst_file="$WORK_DIR/downloads/$site-$object_name"
verified=false
attempt=1
while [ "$attempt" -le "$WAIT_ATTEMPTS" ]; do
rm -f "$dst_file"
echo "downloading $object_name from $site"
if mc cp "$site/$BUCKET/$object" "$dst_file" >/dev/null 2>&1; then
dst_checksum="$(checksum_file "$dst_file")"
dst_bytes="$(wc -c < "$dst_file" | tr -d ' ')"
if [ "$dst_checksum" = "$src_checksum" ] && [ "$dst_bytes" = "$src_bytes" ]; then
verified=true
break
fi
fi
sleep "$WAIT_SLEEP_SECONDS"
attempt=$((attempt + 1))
done
if [ "$verified" != "true" ]; then
echo "download verification failed for $site/$BUCKET/$object" >&2
echo "expected checksum: $src_checksum" >&2
echo "expected bytes: $src_bytes" >&2
exit 1
fi
done
echo "verified replicated downloads for $object_name"
done
echo "creating empty bucket for replicated delete check: $DELETE_BUCKET"
mc mb "site1/$DELETE_BUCKET" >/dev/null
for site in site1 site2 site3; do
wait_for_bucket "$site" "$DELETE_BUCKET"
done
echo "deleting empty bucket on site1: $DELETE_BUCKET"
mc rb "site1/$DELETE_BUCKET" >/dev/null
for site in site1 site2 site3; do
wait_for_bucket_delete "$site" "$DELETE_BUCKET"
done
echo "site replication object flow check passed"
echo "bucket: $BUCKET"
echo "prefix: $PREFIX"
+3
View File
@@ -1 +1,4 @@
target
.docker/compat/data
.docker/compat/kms
target/compat
+24 -28
View File
@@ -1,36 +1,32 @@
# GitHub Workflow Instructions
Applies to `.github/` and repository pull-request operations.
Applies to `.github/`.
## Pull Requests
- PR titles and descriptions must be in English.
- Use `.github/pull_request_template.md` for every PR body.
- Keep all template section headings.
- Use `N/A` for non-applicable sections.
- Include verification commands in the PR details.
- For `gh pr create` and `gh pr edit`, always write markdown body to a file and pass `--body-file`.
- Do not use multiline inline `--body`; backticks and shell expansion can corrupt content or trigger unintended commands.
- Recommended pattern:
- `cat > /tmp/pr_body.md <<'EOF'`
- `...markdown...`
- `EOF`
- `gh pr create ... --body-file /tmp/pr_body.md`
PR conventions (English title/body, template usage, `--body-file` with the
heredoc pattern, review-thread etiquette) live in the root `AGENTS.md` under
"Git and PR Baseline" — that section is the single normative home; do not
duplicate its rules here.
## Workflow Changes
- Any change touching `.github/workflows/` must pass `actionlint` (run from
the repo root) with zero findings before commit/PR. Install via
`brew install actionlint`, or the download script in the actionlint repo
(rhysd/actionlint); `make pre-pr` does not cover workflow files, so this is
the required check for them.
- Custom self-hosted runner labels (`sm-standard-*`, `dind-*`) are declared
in `.github/actionlint.yaml`. When introducing a new `runs-on:` label,
declare it there in the same change. Never use the config or `-ignore` to
silence real findings — fix the workflow (root `AGENTS.md`: make checks
pass by fixing the cause, not by weakening the gate).
- If `shellcheck` is installed, actionlint also lints `run:` scripts; treat
those findings as part of the gate.
## CI Alignment
When changing CI-sensitive behavior, keep local validation aligned with `.github/workflows/ci.yml`.
Current `test-and-lint` gate includes:
- `cargo nextest run --all --exclude e2e_test`
- `cargo test --all --doc`
- `cargo test -p rustfs get_object_chunk_fast_path`
- `cargo test -p rustfs materialize_chunk_stream_before_commit`
- `touch rustfs/build.rs`
- `cargo build -p rustfs --bins --jobs 2`
- `cargo test -p e2e_test archive_multipart_roundtrip_preserves_bytes`
- `cargo test -p e2e_test presigned_get_and_reverse_proxy_preserve_multipart_bytes_with_fast_path`
- `cargo fmt --all --check`
- `cargo clippy --all-targets --all-features -- -D warnings`
- `./scripts/check_layer_dependencies.sh`
When changing CI-sensitive behavior, keep local validation aligned with
`.github/workflows/ci.yml`. Read the workflow file directly for the current
gate steps — do not rely on (or add) a copied command list here; copies go
stale. `make pre-pr` is the local equivalent of the main gate.
+7
View File
@@ -0,0 +1,7 @@
self-hosted-runner:
# Custom labels of this repository's self-hosted runners. Keep in sync with
# the labels actually used in `runs-on:` across .github/workflows/.
labels:
- sm-standard-2
- sm-standard-4
- dind-sm-standard-2
@@ -0,0 +1,74 @@
# schedule-failure-issue
Opens (or updates) a GitHub issue when a **scheduled** workflow run fails.
This is the single alerting mechanism for all timed pipelines
(rustfs/backlog#1149 ci-8, arbitration G3): scheduled workflows must consume
this action instead of inventing their own notification paths.
## Behavior
- Issue title: `[scheduled-failure] <workflow name>` — the workflow name is
the dedupe key.
- If an **open** issue with that exact title exists, the failure is appended
as a comment; otherwise a new issue is created (labeled `infrastructure` by
default). Closing the issue resets the cycle: the next failure opens a
fresh one.
- The issue body / comment includes the run URL, run attempt, event, ref and
the names of the failed (or timed-out/cancelled) jobs of the current run
attempt.
- Requires `gh` and `jq` on the runner (both preinstalled on GitHub-hosted
runners such as `ubuntu-latest`).
## Usage
Add a final job to the workflow. `always()` is required — without it the job
is skipped when a needed job fails; `contains(needs.*.result, 'failure')`
makes it act only when something actually failed. Scope `issues: write` to
this job only; no other job may gain permissions.
```yaml
alert-on-failure:
name: Alert on scheduled failure
needs: [<the jobs to watch>]
if: always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
```
Notes:
- Upstream jobs that are *cancelled* (e.g. a manually cancelled run) do not
trigger the alert — a human was already looking. Job `timeout-minutes`
expiry surfaces as `failure` and does alert.
- Manual `workflow_dispatch` runs never alert; a human is watching those.
## Current consumers
- `.github/workflows/e2e-s3tests.yml` (weekly full s3-tests sweep)
- `.github/workflows/mint.yml` (weekly multi-SDK mint run)
- `.github/workflows/fuzz.yml` (nightly fuzz corpus jobs)
- `.github/workflows/performance-ab.yml` (nightly warp A/B gate)
- **Pending:** the ci-7 e2e nightly-full workflow does not exist yet
(backlog#1149 ci-7). When it lands, wire it up with the snippet above.
## Drill
`.github/workflows/schedule-failure-alert-drill.yml` is a manual-only
workflow that forces a job failure and runs this action through the exact
consumer wiring. Use it to verify the alert path end to end:
```bash
gh workflow run schedule-failure-alert-drill.yml -R rustfs/rustfs --ref <branch>
```
Run it twice to verify both paths (issue creation, then comment dedupe), and
close the `[scheduled-failure] Schedule Failure Alert Drill` issue afterwards.
@@ -0,0 +1,104 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: "Schedule Failure Issue"
description: >-
Open (or update) a tracking issue when a scheduled workflow run fails.
Dedupes by workflow name: if an open issue titled
"[scheduled-failure] <workflow name>" already exists, the failure is
appended as a comment; otherwise a new issue is created. This is the
single alerting mechanism for all scheduled pipelines (backlog#1149 ci-8).
inputs:
github-token:
description: >-
Token with issues:write on the repository. Pass secrets.GITHUB_TOKEN
from a job that declares `permissions: issues: write` (scope the
permission to the alert job only, never workflow-wide).
required: true
workflow-name:
description: "Workflow name used for the issue title (the dedupe key)."
required: false
default: ${{ github.workflow }}
label:
description: >-
Label applied when a new issue is created. Must already exist in the
repository; if applying it fails, the issue is created without a label.
Set to an empty string to skip labeling.
required: false
default: "infrastructure"
runs:
using: "composite"
steps:
- name: Open or update failure-tracking issue
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
WORKFLOW_NAME: ${{ inputs.workflow-name }}
ISSUE_LABEL: ${{ inputs.label }}
run: |
set -euo pipefail
title="[scheduled-failure] ${WORKFLOW_NAME}"
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
# Failed job names for this run attempt. The alert job runs while the
# run as a whole is still in progress, so inspect the jobs that have
# already completed with a non-success conclusion.
failed_jobs="$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs" \
--paginate \
--jq '.jobs[]
| select(.conclusion == "failure" or .conclusion == "timed_out" or .conclusion == "cancelled")
| "- `\(.name)` (\(.conclusion))"')"
if [ -z "${failed_jobs}" ]; then
failed_jobs="- (failed job not recorded yet — see the run page)"
fi
body="$(cat <<EOF
Scheduled run of **${WORKFLOW_NAME}** failed.
- Run: ${run_url} (attempt ${GITHUB_RUN_ATTEMPT})
- Event: \`${GITHUB_EVENT_NAME}\`
- Ref: \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA}\`
Failed jobs:
${failed_jobs}
EOF
)"
# Dedupe by exact title among OPEN issues (a closed issue means the
# earlier breakage was resolved; a new failure gets a fresh issue).
# GitHub search strips the [] characters, so search loosely and match
# the exact title with jq.
existing="$(gh issue list --repo "${GITHUB_REPOSITORY}" --state open --limit 100 \
--search "\"scheduled-failure\" in:title" --json number,title \
| jq -r --arg title "${title}" 'map(select(.title == $title)) | (.[0].number // empty)')"
if [ -n "${existing}" ]; then
echo "Appending comment to existing open issue #${existing}"
gh issue comment "${existing}" --repo "${GITHUB_REPOSITORY}" --body "${body}"
exit 0
fi
echo "Creating new issue: ${title}"
if [ -n "${ISSUE_LABEL}" ]; then
if gh issue create --repo "${GITHUB_REPOSITORY}" \
--title "${title}" --body "${body}" --label "${ISSUE_LABEL}"; then
exit 0
fi
echo "::warning::issue creation with label '${ISSUE_LABEL}' failed (missing label?); retrying without a label"
fi
gh issue create --repo "${GITHUB_REPOSITORY}" --title "${title}" --body "${body}"
+15 -11
View File
@@ -53,21 +53,25 @@ runs:
musl-tools \
build-essential \
pkg-config \
libssl-dev
libssl-dev \
ripgrep \
unzip \
zip \
protobuf-compiler
- name: Install protoc
uses: arduino/setup-protoc@v3
uses: rustfs/setup-protoc@a3705324d8f9bf5b6c3573fb6cf8ae421db55dd6 # v3.0.1
with:
version: "33.1"
repo-token: ${{ inputs.github-token }}
version: "34.1"
repo-token: ${{ github.token }}
- name: Install flatc
uses: Nugine/setup-flatc@v1.2.4
uses: Nugine/setup-flatc@e7855e994773ce90094a3f1626d4afc9080c23ae # v1
with:
version: "25.9.23"
version: "25.12.19"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: ${{ inputs.rust-version }}
targets: ${{ inputs.target }}
@@ -75,17 +79,17 @@ runs:
- name: Install Zig
if: inputs.install-cross-tools == 'true'
uses: mlugg/setup-zig@v2
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
- name: Install cargo-zigbuild
if: inputs.install-cross-tools == 'true'
uses: taiki-e/install-action@cargo-zigbuild
uses: taiki-e/install-action@a21ae4029b089b9ddc45704028756f51ab8abe48 # cargo-zigbuild
- name: Install cargo-nextest
uses: taiki-e/install-action@cargo-nextest
uses: taiki-e/install-action@96c7780c1d8a2b8723e12031def873a434d39d8d # nextest
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-all-crates: true
cache-on-failure: true
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+16 -2
View File
@@ -1,8 +1,22 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
enabled: true
document:
version: v1
url: https://github.com/rustfs/cla/blob/main/cla/v1.md
version: v2
url: https://github.com/rustfs/cla/blob/main/cla/v2.md
signing:
mode: comment
+5 -2
View File
@@ -27,9 +27,8 @@ updates:
timezone: "Asia/Shanghai"
time: "08:00"
assignees:
- "heihutu"
reviewers:
- "houseme"
reviewers:
- "overtrue"
- "majinghe"
ignore:
@@ -39,6 +38,10 @@ updates:
versions: [ "0.23.x" ]
- dependency-name: "ratelimit"
versions: [ "1.x" ]
- dependency-name: "ratelimit"
versions: [ "2.x" ]
- dependency-name: "pyroscope"
versions: [ "2.x" ]
groups:
s3s:
update-types:
+21 -22
View File
@@ -2,36 +2,35 @@
Pull Request Template for RustFS
-->
## Type of Change
- [ ] New Feature
- [ ] Bug Fix
- [ ] Documentation
- [ ] Performance Improvement
- [ ] Test/CI
- [ ] Refactor
- [ ] Other:
## Related Issues
<!-- List related Issue numbers, e.g. #123 -->
<!--
List related issues, e.g. Fixes #123.
Use N/A when there is no related issue.
-->
## Summary of Changes
<!-- Briefly describe the main changes and motivation for this PR -->
<!--
Briefly explain what changed and why reviewers should accept it.
Focus on behavior, compatibility, and review-relevant context.
-->
## Checklist
- [ ] I have read and followed the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
- [ ] Passed `make pre-commit`
- [ ] Added/updated necessary tests
- [ ] Documentation updated (if needed)
- [ ] CI/CD passed (if applicable)
## Verification
<!--
List the commands or checks you ran, for example:
- `make pre-commit`
Use N/A only when verification is not applicable.
-->
## Impact
- [ ] Breaking change (compatibility)
- [ ] Requires doc/config/deployment update
- [ ] Other impact:
<!--
Describe user-facing, compatibility, API, deployment, configuration, or
documentation impact. Use N/A when there is no expected impact.
-->
## Additional Notes
<!-- Any extra information for reviewers -->
<!-- Any extra information for reviewers, or N/A. -->
---
Thank you for your contribution! Please ensure your PR follows the community standards ([CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)). If this is your first contribution, review the [CLA document](https://github.com/rustfs/cla/blob/main/cla/v1.md) and sign it by commenting `I have read and agree to the CLA.` on the PR.
Thank you for your contribution! Please ensure your PR follows the community standards ([CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)). If this is your first contribution, review the [CLA document](https://github.com/rustfs/cla/blob/main/cla/v2.md) and sign it by commenting `I have read and agree to the CLA.` on the PR.
+51 -73
View File
@@ -1,103 +1,81 @@
# S3 Compatibility Tests Configuration
This directory contains the configuration for running [Ceph S3 compatibility tests](https://github.com/ceph/s3-tests) against RustFS.
This directory contains the configuration template for running
[Ceph S3 compatibility tests](https://github.com/ceph/s3-tests) against RustFS.
The test runner itself lives in [`scripts/s3-tests/`](../../scripts/s3-tests/)
— see its README for how tests are selected, executed, and reported. This
directory only holds the shared config template.
## Configuration File
The `s3tests.conf` file is based on the official `s3tests.conf.SAMPLE` from the ceph/s3-tests repository. It uses environment variable substitution via `envsubst` to configure the endpoint and credentials.
`s3tests.conf` is based on the official `s3tests.conf.SAMPLE` from the
ceph/s3-tests repository. It is a template: `scripts/s3-tests/run.sh` renders
it with `envsubst` before every run.
### Key Configuration Points
Required environment variables (all have defaults in `run.sh`):
- **Host**: Set via `${S3_HOST}` environment variable (e.g., `rustfs-single` for single-node, `lb` for multi-node)
- **Port**: 9000 (standard RustFS port)
- **Credentials**: Uses `${S3_ACCESS_KEY}` and `${S3_SECRET_KEY}` from workflow environment
- **TLS**: Disabled (`is_secure = False`)
| Variable | Meaning |
|----------|---------|
| `S3_HOST` | RustFS endpoint host |
| `S3_PORT` | RustFS endpoint port |
| `S3_ACCESS_KEY` / `S3_SECRET_KEY` | main user credentials |
| `S3_ALT_ACCESS_KEY` / `S3_ALT_SECRET_KEY` | alt user credentials (must differ from main) |
## Test Execution Strategy
TLS is disabled (`is_secure = False`).
### Network Connectivity Fix
## Test Selection Strategy
Tests run inside a Docker container on the `rustfs-net` network, which allows them to resolve and connect to the RustFS container hostnames. This fixes the "Temporary failure in name resolution" error that occurred when tests ran on the GitHub runner host.
Tests are selected by exact pytest node ids from classification list files in
`scripts/s3-tests/`**not** by pytest markers:
### Performance Optimizations
- `implemented_tests.txt` — expected to pass; run as the per-PR gate
(`ci.yml`, job `s3-implemented-tests`)
- `unimplemented_tests.txt` — standard S3 features not yet implemented
- `excluded_tests.txt` — intentionally excluded (vendor-specific behavior or
product decisions)
1. **Parallel Execution**: Uses `pytest-xdist` with `-n 4` to run tests in parallel across 4 workers
2. **Load Distribution**: Uses `--dist=loadgroup` to distribute test groups across workers
3. **Fail-Fast**: Uses `--maxfail=50` to stop after 50 failures, saving time on catastrophic failures
The upstream s3-tests repository is pinned to a fixed commit (`S3TESTS_REV` in
`run.sh`) so results are reproducible; bump it deliberately and reclassify the
lists when upstream changes.
### Feature Filtering
## Where Tests Run
Tests are filtered using pytest markers (`-m`) to skip features not yet supported by RustFS:
- `lifecycle` - Bucket lifecycle policies
- `versioning` - Object versioning
- `s3website` - Static website hosting
- `bucket_logging` - Bucket logging
- `encryption` / `sse_s3` - Server-side encryption
- `cloud_transition` / `cloud_restore` - Cloud storage transitions
- `lifecycle_expiration` / `lifecycle_transition` - Lifecycle operations
This filtering:
1. Reduces test execution time significantly (from 1+ hour to ~10-15 minutes)
2. Focuses on features RustFS currently supports
3. Avoids hundreds of expected failures
- **Per PR**: `.github/workflows/ci.yml` job `s3-implemented-tests` runs the
implemented whitelist against a single-node debug binary. Any failure blocks
the PR.
- **Weekly + manual**: `.github/workflows/e2e-s3tests.yml` runs the full
upstream suite (`TEST_SCOPE=all`) against a Docker deployment (single node
or a 4-node distributed cluster behind HAProxy). It fails only on
regressions in the implemented whitelist and publishes a classification
report (`compat-report.md`, also shown in the job summary) listing promotion
candidates and unclassified tests.
## Running Tests Locally
### Single-Node Test
```bash
# Set credentials
export S3_ACCESS_KEY=rustfsadmin
export S3_SECRET_KEY=rustfsadmin
# Whitelist run against a locally built binary (the same thing the PR gate does)
./scripts/s3-tests/run.sh
# Start RustFS container
docker run -d --name rustfs-single \
--network rustfs-net \
-e RUSTFS_ADDRESS=0.0.0.0:9000 \
-e RUSTFS_ACCESS_KEY=$S3_ACCESS_KEY \
-e RUSTFS_SECRET_KEY=$S3_SECRET_KEY \
-e RUSTFS_VOLUMES="/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3" \
rustfs-ci
# Full sweep, never stop on failures, 4 pytest workers
TEST_SCOPE=all MAXFAIL=0 XDIST=4 ./scripts/s3-tests/run.sh
# Generate config
export S3_HOST=rustfs-single
envsubst < .github/s3tests/s3tests.conf > /tmp/s3tests.conf
# Run tests
docker run --rm \
--network rustfs-net \
-v /tmp/s3tests.conf:/etc/s3tests.conf:ro \
python:3.12-slim \
bash -c '
apt-get update -qq && apt-get install -y -qq git
git clone --depth 1 https://github.com/ceph/s3-tests.git /s3-tests
cd /s3-tests
pip install -q -r requirements.txt pytest-xdist
S3TEST_CONF=/etc/s3tests.conf pytest -v -n 4 \
s3tests/functional/test_s3.py \
-m "not lifecycle and not versioning and not s3website and not bucket_logging and not encryption and not sse_s3"
'
# A specific test against an already-running server
DEPLOY_MODE=existing TESTEXPR="test_bucket_list_empty" ./scripts/s3-tests/run.sh
```
## Test Results Interpretation
- **PASSED**: Test succeeded, feature works correctly
- **FAILED**: Test failed, indicates a potential bug or incompatibility
- **ERROR**: Test setup failed (e.g., network issues, missing dependencies)
- **SKIPPED**: Test skipped due to marker filtering
Results land in `artifacts/s3tests-single/` (`junit.xml`, `pytest.log`,
`compat-report.md`).
## Adding New Feature Support
When adding support for a new S3 feature to RustFS:
1. Remove the corresponding marker from the filter in `.github/workflows/e2e-s3tests.yml`
2. Run the tests to verify compatibility
3. Fix any failing tests
4. Update this README to reflect the newly supported feature
Follow [`scripts/s3-tests/S3_COMPAT_WORKFLOW.md`](../../scripts/s3-tests/S3_COMPAT_WORKFLOW.md).
In short: fix the behavior, verify with `TESTEXPR=...`, then move the test
from `unimplemented_tests.txt` to `implemented_tests.txt` so the PR gate locks
it in.
## References
- [Ceph S3 Tests Repository](https://github.com/ceph/s3-tests)
- [S3 API Compatibility](https://docs.aws.amazon.com/AmazonS3/latest/API/)
- [pytest-xdist Documentation](https://pytest-xdist.readthedocs.io/)
- [RustFS test runner](../../scripts/s3-tests/README.md)
+2 -2
View File
@@ -11,8 +11,8 @@
# host set for RustFS - will be substituted via envsubst
host = ${S3_HOST}
# port for RustFS
port = 9000
# port for RustFS - will be substituted via envsubst
port = ${S3_PORT}
## say "False" to disable TLS
is_secure = False
+14
View File
@@ -0,0 +1,14 @@
# GitHub secret scanning path exclusions.
# Secrets detected under these paths are auto-closed as "ignored by
# configuration" and are not blocked by push protection.
#
# Deliberately NOT excluded: production source files (e.g. rustfs/src/**,
# crates/*/src/**) even when a hit sits inside a #[cfg(test)] module or a
# doc-comment example — excluding the file would also stop scanning the
# production code in it. Close those alerts manually as "used in tests".
paths-ignore:
- "crates/e2e_test/**" # dedicated e2e test crate, test credentials throughout
- "**/tests/**" # Rust integration-test dirs (crates/*/tests, rustfs/tests)
- "**/benches/**" # benchmark harnesses
- ".docker/**" # local docker-compose dev configs (e.g. mqtt vm.args cookie)
- ".vscode/**" # local editor launch configs
@@ -0,0 +1,57 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Architecture Migration Rules
on:
pull_request:
types: [ opened, synchronize, reopened, closed ]
branches: [ main ]
paths:
- "ARCHITECTURE.md"
- "docs/architecture/**"
- "scripts/check_architecture_migration_rules.sh"
- ".github/workflows/architecture-migration-rules.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
cancel-closed-pr-runs:
name: Cancel Closed PR Runs
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Explain cancellation run
run: echo "PR closed; this run only cancels older runs in the same concurrency group."
architecture-migration-rules:
name: Architecture Migration Rules
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install ripgrep
run: |
sudo apt-get update
sudo apt-get install -y ripgrep
- name: Check architecture migration rules
run: ./scripts/check_architecture_migration_rules.sh
+65 -26
View File
@@ -20,62 +20,101 @@ on:
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '.github/workflows/audit.yml'
- 'deny.toml'
- '.github/actions/**'
- '.github/workflows/**'
- 'scripts/security/check_workflow_pins.sh'
pull_request:
types: [ opened, synchronize, reopened, closed ]
branches: [ main ]
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '.github/workflows/audit.yml'
- 'deny.toml'
- '.github/actions/**'
- '.github/workflows/**'
- 'scripts/security/check_workflow_pins.sh'
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC
- cron: '0 3 * * 0' # Weekly on Sunday 03:00 UTC (staggered after the midnight ci/build crons)
workflow_dispatch:
permissions:
contents: read
# Event-scoped groups: merges to main must not cancel the weekly scheduled
# run (same rationale as ci.yml).
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name != 'schedule' }}
env:
CARGO_TERM_COLOR: always
jobs:
security-audit:
name: Security Audit
runs-on: ubicloud-standard-2
timeout-minutes: 15
cancel-closed-pr-runs:
name: Cancel Closed PR Runs
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Explain cancellation run
run: echo "PR closed; this run only cancels older runs in the same concurrency group."
# RustSec advisory scanning is covered by the `advisories` check of
# cargo-deny below; a separate cargo-audit job would duplicate the same
# database lookup with a second ignore list to maintain.
cargo-deny:
name: Cargo Deny
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install cargo-audit
uses: taiki-e/install-action@v2
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
tool: cargo-audit
cache-shared-key: rustfs-cargo-deny
- name: Run security audit
run: |
cargo audit -D warnings --json | tee audit-results.json
- name: Upload audit results
if: always()
uses: actions/upload-artifact@v6
- name: Install cargo-deny
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
with:
name: security-audit-results-${{ github.run_number }}
path: audit-results.json
retention-days: 30
tool: cargo-deny
- name: Run cargo-deny
run: cargo deny check --hide-inclusion-graph advisories sources bans licenses
workflow-pin-report:
name: Workflow Pin Report
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Report unpinned GitHub Actions
run: ./scripts/security/check_workflow_pins.sh --enforce
dependency-review:
name: Dependency Review
runs-on: ubicloud-standard-2
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.action != 'closed'
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Dependency Review
uses: actions/dependency-review-action@v4
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5
with:
fail-on-severity: moderate
comment-summary-in-pr: true
allow-ghsas: GHSA-2f9f-gq7v-9h6m
# rustfs-uring is a first-party Apache-2.0 crate, now published on
# crates.io (backlog#1104) rather than pulled as a git dependency.
# Scope the allow to the exact pinned version so a version bump forces a
# conscious re-review of the license/provenance claim (backlog#1181).
allow-dependencies-licenses: pkg:cargo/rustfs-uring@0.1.0
comment-summary-in-pr: always
+304 -143
View File
@@ -21,6 +21,11 @@
# 2. Upload binaries to OSS storage
# 3. Trigger docker.yml to build and push images using the uploaded binaries
#
# Platform scope:
# - Pushes to main (development builds) build the Linux targets only — they
# feed the dev download channel and Docker dev images. Tags, the weekly
# schedule and manual dispatch build the full platform matrix.
#
# Manual Parameters:
# - build_docker: Build and push Docker images (default: true)
# - platforms: Comma-separated platform IDs or 'all' (default: all)
@@ -46,7 +51,7 @@ on:
- ".gitignore"
- ".dockerignore"
schedule:
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
- cron: "0 1 * * 0" # Weekly on Sunday 01:00 UTC (staggered after the ci.yml midnight cron)
workflow_dispatch:
inputs:
build_docker:
@@ -63,6 +68,10 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'push' }}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
@@ -73,7 +82,7 @@ jobs:
# Build strategy check - determine build type based on trigger
build-check:
name: Build Strategy Check
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
build_type: ${{ steps.check.outputs.build_type }}
@@ -82,9 +91,7 @@ jobs:
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Determine build strategy
id: check
@@ -121,8 +128,7 @@ jobs:
version="dev-${short_sha}"
echo "🛠️ Development build detected"
elif [[ "${{ github.event_name }}" == "schedule" ]] || \
[[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \
[[ "${{ contains(github.event.head_commit.message, '--build') }}" == "true" ]]; then
[[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
# Scheduled or manual build
should_build=true
build_type="development"
@@ -130,11 +136,13 @@ jobs:
echo "⚡ Manual/scheduled build detected"
fi
echo "should_build=$should_build" >> $GITHUB_OUTPUT
echo "build_type=$build_type" >> $GITHUB_OUTPUT
echo "version=$version" >> $GITHUB_OUTPUT
echo "short_sha=$short_sha" >> $GITHUB_OUTPUT
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
{
echo "should_build=$should_build"
echo "build_type=$build_type"
echo "version=$version"
echo "short_sha=$short_sha"
echo "is_prerelease=$is_prerelease"
} >> "$GITHUB_OUTPUT"
echo "📊 Build Summary:"
echo " - Should build: $should_build"
@@ -146,7 +154,8 @@ jobs:
# Build RustFS binaries
prepare-platform-matrix:
name: Prepare Platform Matrix
runs-on: ubicloud-standard-2
needs: build-check
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.select.outputs.matrix }}
selected: ${{ steps.select.outputs.selected }}
@@ -163,13 +172,24 @@ jobs:
selected="all"
fi
# Per-merge development builds only feed the dev download channel
# and the Docker dev images, which consume the Linux binaries; at
# the usual merge cadence most per-merge builds are cancelled by the
# next merge anyway. macOS and Windows stay covered by tag builds,
# the weekly scheduled build and manual dispatch.
if [[ "${selected}" == "all" \
&& "${{ github.event_name }}" == "push" \
&& "${{ needs.build-check.outputs.build_type }}" == "development" ]]; then
selected="linux-x86_64-musl,linux-aarch64-musl,linux-x86_64-gnu,linux-aarch64-gnu"
echo "Development (main push) build: restricting to Linux targets"
fi
all='{"include":[
{"target_id":"linux-x86_64-musl","os":"ubicloud-standard-2","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":""},
{"target_id":"linux-aarch64-musl","os":"ubicloud-standard-2","target":"aarch64-unknown-linux-musl","cross":true,"platform":"linux","rustflags":""},
{"target_id":"linux-x86_64-gnu","os":"ubicloud-standard-2","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":""},
{"target_id":"linux-aarch64-gnu","os":"ubicloud-standard-2","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":""},
{"target_id":"linux-x86_64-musl","os":"sm-standard-2","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":""},
{"target_id":"linux-aarch64-musl","os":"sm-standard-2","target":"aarch64-unknown-linux-musl","cross":true,"platform":"linux","rustflags":""},
{"target_id":"linux-x86_64-gnu","os":"sm-standard-2","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":""},
{"target_id":"linux-aarch64-gnu","os":"sm-standard-2","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":""},
{"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
{"target_id":"macos-x86_64","os":"macos-latest","target":"x86_64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
{"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":""}
]}'
@@ -202,20 +222,20 @@ jobs:
needs: [ build-check, prepare-platform-matrix ]
if: needs.build-check.outputs.should_build == 'true' && needs.prepare-platform-matrix.result == 'success'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
timeout-minutes: 150
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# Always enable Tokio unstable features (required by dial9-tokio-telemetry).
# The RUSTFLAGS env var takes precedence over .cargo/config.toml [build] rustflags,
# so we must include --cfg tokio_unstable here explicitly; otherwise an empty
# RUSTFLAGS value would shadow the config-file flag and silently break tracing.
RUSTFLAGS: "--cfg tokio_unstable ${{ matrix.rustflags }}"
# Release binaries ship without dial9 telemetry and therefore do not need
# --cfg tokio_unstable. Telemetry builds opt in explicitly (see
# `make build-profiling`); crates/obs/build.rs fails the compile if the
# `dial9` feature is enabled without the flag.
RUSTFLAGS: "${{ matrix.rustflags }}"
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare-platform-matrix.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
@@ -224,37 +244,131 @@ jobs:
with:
rust-version: stable
target: ${{ matrix.target }}
cache-shared-key: build-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
cache-shared-key: build-${{ matrix.target }}
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
install-cross-tools: ${{ matrix.cross }}
- name: Download static console assets
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
mkdir -p ./rustfs/static
if [[ "${{ matrix.platform }}" == "windows" ]]; then
curl.exe -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" -o console.zip --retry 3 --retry-delay 5 --max-time 300
if [[ $? -eq 0 ]]; then
unzip -o console.zip -d ./rustfs/static
rm console.zip
verify_sha256() {
local expected="$1"
local file="$2"
local actual=""
local actual_line=""
if command -v sha256sum >/dev/null 2>&1; then
actual_line=$(sha256sum -- "$file") || return 1
elif command -v shasum >/dev/null 2>&1; then
actual_line=$(shasum -a 256 -- "$file") || return 1
else
echo "Warning: Failed to download console assets, continuing without them"
echo "// Static assets not available" > ./rustfs/static/empty.txt
echo "No SHA-256 verification tool found" >&2
return 1
fi
actual="${actual_line%%[[:space:]]*}"
if [ "$actual" = "$expected" ]; then
echo "SHA256 verified OK: $actual"
return 0
else
echo "SHA256 mismatch: expected=$expected actual=$actual" >&2
return 1
fi
}
download_console_assets() {
local curl_bin="curl"
local python_bin="python3"
local console_api="https://api.github.com/repos/rustfs/console/releases/latest"
local console_json="console-release.json"
local console_url
local console_sha256
local curl_auth_args=()
console_tag=""
if [[ "${{ matrix.platform }}" == "windows" ]]; then
curl_bin="curl.exe"
else
chmod +w ./rustfs/static/LICENSE || true
fi
if ! command -v "$python_bin" >/dev/null 2>&1; then
python_bin="python"
fi
if ! command -v "$python_bin" >/dev/null 2>&1; then
echo "No Python interpreter found for release metadata parsing" >&2
return 1
fi
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
curl_auth_args=(-H "Authorization: Bearer ${GITHUB_TOKEN}" -H "X-GitHub-Api-Version: 2022-11-28")
fi
"$curl_bin" "${curl_auth_args[@]}" --fail -L "$console_api" \
-o "$console_json" --retry 3 --retry-delay 5 --max-time 300 || return 1
read -r console_tag console_url console_sha256 < <("$python_bin" - "$console_json" <<'PY'
import json
import re
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
release = json.load(handle)
tag = release.get("tag_name", "") or "unknown"
for asset in release.get("assets", []):
name = asset.get("name", "")
digest = asset.get("digest", "")
url = asset.get("browser_download_url", "")
if name.startswith("rustfs-console-") and name.endswith(".zip") and digest.startswith("sha256:") and url:
sha256 = digest.split(":", 1)[1]
if not re.fullmatch(r"[0-9a-fA-F]{64}", sha256):
raise SystemExit(f"console zip asset has invalid sha256 digest: {sha256}")
sys.stdout.buffer.write(f"{tag} {url} {sha256}\n".encode("utf-8"))
break
else:
raise SystemExit("no console zip asset with sha256 digest found")
PY
) || return 1
if [[ -z "$console_url" || -z "$console_sha256" ]]; then
echo "Console release metadata is missing URL or SHA-256 digest" >&2
return 1
fi
echo "Console release: ${console_tag}"
echo "Downloading console asset: ${console_url}"
"$curl_bin" --fail -L "$console_url" -o console.zip --retry 3 --retry-delay 5 --max-time 300 || return 1
verify_sha256 "$console_sha256" console.zip || return 2
unzip -o console.zip -d ./rustfs/static || return 2
}
if download_console_assets; then
rm -f console.zip console-release.json
else
chmod +w ./rustfs/static/LICENSE || true
curl -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" \
-o console.zip --retry 3 --retry-delay 5 --max-time 300
if [[ $? -eq 0 ]]; then
unzip -o console.zip -d ./rustfs/static
rm console.zip
else
echo "Warning: Failed to download console assets, continuing without them"
echo "// Static assets not available" > ./rustfs/static/empty.txt
status=$?
rm -f console.zip console-release.json
if [[ "$status" -eq 2 ]]; then
echo "Console asset integrity verification failed" >&2
fi
echo "Failed to download verified console assets" >&2
exit 1
fi
if [[ ! -s ./rustfs/static/index.html ]]; then
echo "Console asset archive is missing static/index.html" >&2
exit 1
fi
asset_count=$(find ./rustfs/static -type f | wc -l | tr -d '[:space:]')
echo "Console assets ready: version=${console_tag:-unknown}, ${asset_count} files extracted to ./rustfs/static"
- name: Build RustFS
shell: bash
run: |
@@ -262,14 +376,8 @@ jobs:
touch rustfs/build.rs
if [[ "${{ matrix.cross }}" == "true" ]]; then
if [[ "${{ matrix.platform }}" == "windows" ]]; then
# Use cross for Windows ARM64
cargo install cross --git https://github.com/cross-rs/cross
cross build --release --target ${{ matrix.target }} -p rustfs --bins
else
# Use zigbuild for other cross-compilation
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins
fi
# All cross targets in the matrix are Linux; zigbuild handles them.
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins
else
cargo build --release --target ${{ matrix.target }} -p rustfs --bins
fi
@@ -342,11 +450,11 @@ jobs:
# Release/Prerelease build: rustfs-${platform}-${arch}-${variant}-v${version}.zip
PACKAGE_NAME="${PACKAGE_BASENAME}-v${PACKAGE_VERSION}"
fi
# Create zip packages for all platforms
# Ensure zip is available
if ! command -v zip &> /dev/null; then
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
if [[ "${{ matrix.os }}" == "ubuntu-latest" || "${{ matrix.platform }}" == "linux" ]]; then
sudo apt-get update && sudo apt-get install -y zip
fi
fi
@@ -398,7 +506,7 @@ jobs:
if [[ "${{ matrix.platform }}" == "windows" ]]; then
dir
else
ls -lh ${PACKAGE_NAME}.zip
ls -lh "${PACKAGE_NAME}.zip"
fi
else
echo "❌ Failed to create package: ${PACKAGE_NAME}.zip"
@@ -447,11 +555,13 @@ jobs:
fi
fi
echo "package_name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT
echo "package_file=${PACKAGE_NAME}.zip" >> $GITHUB_OUTPUT
echo "latest_files=${LATEST_FILES}" >> $GITHUB_OUTPUT
echo "build_type=${BUILD_TYPE}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
{
echo "package_name=${PACKAGE_NAME}"
echo "package_file=${PACKAGE_NAME}.zip"
echo "latest_files=${LATEST_FILES}"
echo "build_type=${BUILD_TYPE}"
echo "version=${VERSION}"
} >> "$GITHUB_OUTPUT"
echo "📦 Package created: ${PACKAGE_NAME}.zip"
if [[ -n "$LATEST_FILES" ]]; then
@@ -460,86 +570,118 @@ jobs:
echo "🔧 Build type: ${BUILD_TYPE}"
echo "📊 Version: ${VERSION}"
- name: Verify packaged console
if: matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
run: |
set -euo pipefail
find_free_port() {
python3 - <<'PY'
import socket
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1])
PY
}
package_dir="$(mktemp -d)"
data_dir="$(mktemp -d)"
log_file="${RUNNER_TEMP}/rustfs-console-smoke.log"
server_pid=""
trap '
if [[ -n "${server_pid:-}" ]]; then
kill "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
fi
rm -rf "$package_dir" "$data_dir"
' EXIT
unzip -q "${{ steps.package.outputs.package_file }}" -d "$package_dir"
api_port="$(find_free_port)"
console_port="$(find_free_port)"
console_url="http://127.0.0.1:${console_port}/rustfs/console/"
"$package_dir/rustfs" server \
--address "127.0.0.1:${api_port}" \
--console-enable \
--console-address "127.0.0.1:${console_port}" \
--access-key console-smoke \
--secret-key console-smoke-secret \
"$data_dir" >"$log_file" 2>&1 &
server_pid=$!
for _ in {1..40}; do
response="$(curl --silent --output /dev/null --write-out '%{http_code} %{content_type}' "$console_url" || true)"
if [[ "$response" == 200\ text/html* ]]; then
exit 0
fi
sleep 0.25
done
echo "Console endpoint did not return 200 text/html: $response" >&2
cat "$log_file"
exit 1
- name: Upload to GitHub artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: ${{ steps.package.outputs.package_name }}
path: "rustfs-*.zip"
retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 30 || 7 }}
- name: Upload to Aliyun OSS
if: env.OSS_ACCESS_KEY_ID != '' && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease' || needs.build-check.outputs.build_type == 'development')
- name: Upload to Cloudflare R2
if: env.R2_ACCESS_KEY_ID != '' && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease' || needs.build-check.outputs.build_type == 'development')
env:
OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }}
OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }}
OSS_REGION: cn-beijing
OSS_ENDPOINT: https://oss-accelerate.aliyuncs.com
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
AWS_EC2_METADATA_DISABLED: true
shell: bash
run: |
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
# Install ossutil (platform-specific)
OSSUTIL_VERSION="2.1.1"
case "${{ matrix.platform }}" in
linux)
if [[ "$(uname -m)" == "arm64" ]]; then
ARCH="arm64"
else
ARCH="amd64"
fi
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-linux-${ARCH}.zip"
OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-linux-${ARCH}"
if [[ -z "$R2_ACCESS_KEY_ID" || -z "$R2_SECRET_ACCESS_KEY" || -z "$R2_ENDPOINT" || -z "$R2_BUCKET" ]]; then
echo "⚠️ R2 credentials or endpoint missing, skipping artifact upload"
exit 0
fi
curl -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
unzip "$OSSUTIL_ZIP"
mv "${OSSUTIL_DIR}/ossutil" /usr/local/bin/
rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP"
chmod +x /usr/local/bin/ossutil
OSSUTIL_BIN=ossutil
;;
macos)
if [[ "$(uname -m)" == "arm64" ]]; then
ARCH="arm64"
else
ARCH="amd64"
fi
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-mac-${ARCH}.zip"
OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-mac-${ARCH}"
# The self-hosted Linux runners do not ship the aws CLI (GitHub-hosted
# images did). Install it on demand so R2 uploads survive a fresh runner
# instead of hard-failing here.
if ! command -v aws >/dev/null 2>&1; then
echo "aws CLI not found on runner; installing..."
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y awscli
elif command -v brew >/dev/null 2>&1; then
brew install awscli
else
echo "❌ aws CLI missing and no apt-get/brew to install it; cannot upload to R2"
exit 1
fi
fi
curl -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
unzip "$OSSUTIL_ZIP"
mv "${OSSUTIL_DIR}/ossutil" /usr/local/bin/
rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP"
chmod +x /usr/local/bin/ossutil
OSSUTIL_BIN=ossutil
;;
windows)
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-windows-amd64.zip"
OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-windows-amd64"
curl -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
unzip "$OSSUTIL_ZIP"
mv "${OSSUTIL_DIR}/ossutil.exe" ./ossutil.exe
rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP"
OSSUTIL_BIN=./ossutil.exe
;;
esac
export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
export AWS_DEFAULT_REGION="auto"
# Determine upload path based on build type
if [[ "$BUILD_TYPE" == "development" ]]; then
OSS_PATH="oss://rustfs-artifacts/artifacts/rustfs/dev/"
echo "📤 Uploading development build to OSS dev directory"
R2_PATH="s3://${R2_BUCKET}/artifacts/rustfs/dev/"
echo "📤 Uploading development build to R2 dev directory"
else
OSS_PATH="oss://rustfs-artifacts/artifacts/rustfs/release/"
echo "📤 Uploading release build to OSS release directory"
R2_PATH="s3://${R2_BUCKET}/artifacts/rustfs/release/"
echo "📤 Uploading release build to R2 release directory"
fi
# Upload all rustfs zip files to OSS using glob pattern
echo "📤 Uploading all rustfs-*.zip files to $OSS_PATH..."
# Upload all rustfs zip files to R2 using S3-compatible API
echo "📤 Uploading all rustfs-*.zip files to $R2_PATH..."
for zip_file in rustfs-*.zip; do
if [[ -f "$zip_file" ]]; then
echo "Uploading: $zip_file to $OSS_PATH..."
$OSSUTIL_BIN cp "$zip_file" "$OSS_PATH" --force
echo "Uploading: $zip_file to $R2_PATH..."
aws s3 cp "$zip_file" "$R2_PATH" --endpoint-url "$R2_ENDPOINT" --only-show-errors
echo "✅ Uploaded: $zip_file"
fi
done
@@ -551,7 +693,7 @@ jobs:
name: Build Summary
needs: [ build-check, build-rustfs ]
if: always() && needs.build-check.outputs.should_build == 'true'
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
steps:
- name: Build completion summary
shell: bash
@@ -603,7 +745,7 @@ jobs:
name: Create GitHub Release
needs: [ build-check, build-rustfs ]
if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development'
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
@@ -611,7 +753,7 @@ jobs:
release_url: ${{ steps.create.outputs.release_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
@@ -680,8 +822,8 @@ jobs:
RELEASE_URL=$(gh release view "$TAG" --json url --jq '.url')
fi
echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT
echo "release_url=$RELEASE_URL" >> $GITHUB_OUTPUT
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
echo "release_url=$RELEASE_URL" >> "$GITHUB_OUTPUT"
echo "Created release: $RELEASE_URL"
# Prepare and upload release assets
@@ -689,16 +831,16 @@ jobs:
name: Upload Release Assets
needs: [ build-check, build-rustfs, create-release ]
if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development'
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Download all build artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: ./artifacts
pattern: rustfs-*
@@ -730,16 +872,23 @@ jobs:
cd ./release-assets
# Generate checksums for all files (including latest versions)
if ls *.zip >/dev/null 2>&1; then
sha256sum *.zip > SHA256SUMS
sha512sum *.zip > SHA512SUMS
if compgen -G "*.zip" >/dev/null; then
sha256sum -- *.zip > SHA256SUMS
sha512sum -- *.zip > SHA512SUMS
fi
# Create signature placeholder files
for file in *.zip; do
echo "# Signature for $file" > "${file}.asc"
echo "# GPG signature will be added in future versions" >> "${file}.asc"
done
cd ..
python3 scripts/security/generate_release_supply_chain_assets.py \
--asset-dir ./release-assets \
--version "$VERSION" \
--tag "$TAG" \
--build-type "${{ needs.build-check.outputs.build_type }}" \
--repository "${{ github.repository }}" \
--ref "${{ github.ref }}" \
--sha "${{ github.sha }}" \
--run-id "${{ github.run_id }}" \
--run-attempt "${{ github.run_attempt }}"
cd ./release-assets
echo "📦 Prepared assets:"
ls -la
@@ -765,12 +914,15 @@ jobs:
echo "✅ All assets uploaded successfully"
# Update latest.json for stable releases only
# Update latest.json for every release tag (stable and prerelease): the
# project currently ships prerelease tags only, so gating this to stable
# left the version pointer permanently stale. release_type records whether
# the pointed-to version is a prerelease.
update-latest-version:
name: Update Latest Version
needs: [ build-check, upload-release-assets ]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
steps:
- name: Update latest.json
env:
@@ -788,16 +940,25 @@ jobs:
VERSION="${{ needs.build-check.outputs.version }}"
TAG="${{ needs.build-check.outputs.version }}"
if [[ "${{ needs.build-check.outputs.build_type }}" == "prerelease" ]]; then
RELEASE_TYPE="prerelease"
else
RELEASE_TYPE="stable"
fi
# Install ossutil
OSSUTIL_VERSION="2.1.1"
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-linux-amd64.zip"
OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-linux-amd64"
OSSUTIL_SHA256="60f3a808cbbdfd4b5269b8f967c6a66f564c9dacff52d93a5d21a588976ab5a2"
curl -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
curl --fail -L -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
printf '%s %s\n' "$OSSUTIL_SHA256" "$OSSUTIL_ZIP" | sha256sum -c -
unzip "$OSSUTIL_ZIP"
mv "${OSSUTIL_DIR}/ossutil" /usr/local/bin/
OSSUTIL_BIN="${RUNNER_TEMP}/ossutil"
mv "${OSSUTIL_DIR}/ossutil" "$OSSUTIL_BIN"
rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP"
chmod +x /usr/local/bin/ossutil
chmod +x "$OSSUTIL_BIN"
# Create latest.json
cat > latest.json << EOF
@@ -805,27 +966,27 @@ jobs:
"version": "${VERSION}",
"tag": "${TAG}",
"release_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"release_type": "stable",
"release_type": "${RELEASE_TYPE}",
"download_url": "https://github.com/${{ github.repository }}/releases/tag/${TAG}"
}
EOF
# Upload to OSS
ossutil cp latest.json oss://rustfs-version/latest.json --force
"$OSSUTIL_BIN" cp latest.json oss://rustfs-version/latest.json --force
echo "✅ Updated latest.json for stable release $VERSION"
echo "✅ Updated latest.json for ${RELEASE_TYPE} release $VERSION"
# Publish release (remove draft status)
publish-release:
name: Publish Release
needs: [ build-check, create-release, upload-release-assets ]
if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development'
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Update release notes and publish
env:
+70
View File
@@ -0,0 +1,70 @@
# Copyright 2026 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Companion to ci.yml for the required "Test and Lint" status check.
#
# ci.yml skips docs-only pull requests via paths-ignore, but the branch
# ruleset requires a check named "Test and Lint" — without this workflow a
# docs-only PR would wait on that check forever. This workflow triggers on
# exactly the paths ci.yml ignores and reports an instant success under the
# same job name. Mixed PRs trigger both workflows and the real check still
# gates: a required check with any failing run blocks the merge.
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
#
# Keep the paths list below in sync with the pull_request paths-ignore list
# in ci.yml.
name: Continuous Integration (docs only)
on:
pull_request:
types: [ opened, synchronize, reopened ]
branches: [ main ]
paths:
- "**.md"
- "docs/**"
- "deploy/**"
- "scripts/dev_*.sh"
- "scripts/probe.sh"
- "LICENSE*"
- ".gitignore"
- ".dockerignore"
- "README*"
- "**/*.png"
- "**/*.jpg"
- "**/*.svg"
- ".github/workflows/build.yml"
- ".github/workflows/docker.yml"
- ".github/workflows/audit.yml"
permissions:
contents: read
jobs:
test-and-lint:
name: Test and Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# Docs-only PRs skip the full code CI, but they are exactly where a
# planning-type document could be slipped in (git add -f bypasses
# .gitignore). Run the guard here so the required "Test and Lint" check
# stays meaningful for docs-only changes.
- name: Check no planning docs committed
run: ./scripts/check_no_planning_docs.sh
- name: Satisfy required check for docs-only changes
run: echo "Docs-only change — code CI is skipped by paths-ignore; planning-docs guard passed, reporting success for the required 'Test and Lint' check."
+536 -72
View File
@@ -33,9 +33,11 @@ on:
- ".github/workflows/build.yml"
- ".github/workflows/docker.yml"
- ".github/workflows/audit.yml"
- ".github/workflows/performance.yml"
pull_request:
types: [ opened, synchronize, reopened, closed ]
branches: [ main ]
# Keep this list in sync with the `paths` list in ci-docs-only.yml, which
# reports the required "Test and Lint" check for PRs skipped here.
paths-ignore:
- "**.md"
- "docs/**"
@@ -52,9 +54,8 @@ on:
- ".github/workflows/build.yml"
- ".github/workflows/docker.yml"
- ".github/workflows/audit.yml"
- ".github/workflows/performance.yml"
merge_group:
types: [checks_requested]
types: [ checks_requested ]
schedule:
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
workflow_dispatch:
@@ -62,130 +63,386 @@ on:
permissions:
contents: read
# Concurrency groups are scoped per event so different triggers never cancel
# each other: PR pushes cancel the previous run of that PR, main pushes keep
# latest-wins semantics among themselves, and scheduled runs always complete
# (a shared group used to let every merge kill the weekly scheduled run).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name != 'schedule' }}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
CARGO_BUILD_JOBS: 2
jobs:
skip-check:
name: Skip Duplicate Actions
permissions:
actions: write
contents: read
runs-on: ubicloud-standard-2
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
cancel-closed-pr-runs:
name: Cancel Closed PR Runs
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Skip duplicate actions
id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
cancel_others: true
paths_ignore: '["*.md", "docs/**", "deploy/**"]'
do_not_skip: '["workflow_dispatch", "schedule", "merge_group", "release", "push"]'
- name: Explain cancellation run
run: echo "PR closed; this run only cancels older runs in the same concurrency group."
typos:
name: Typos
needs: skip-check
if: needs.skip-check.outputs.should_skip != 'true'
runs-on: ubicloud-standard-2
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Typos check with custom config file
uses: crate-ci/typos@master
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master
# Fast, compile-free checks that fail early so contributors get feedback in
# ~1 minute instead of waiting for the full test job.
quick-checks:
name: Quick Checks
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install ripgrep
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt
- name: Check code formatting
run: cargo fmt --all --check
- name: Check unsafe code allowances
run: ./scripts/check_unsafe_code_allowances.sh
- name: Check layered dependencies
run: ./scripts/check_layer_dependencies.sh
- name: Check architecture migration rules
run: ./scripts/check_architecture_migration_rules.sh
- name: Check tokio io-uring feature guard
run: ./scripts/check_no_tokio_io_uring.sh
- name: Check extension schema boundaries
run: ./scripts/check_extension_schema_boundaries.sh
- name: Check body-cache whitelist guard
run: ./scripts/check_body_cache_whitelist.sh
- name: Check no planning docs committed
run: ./scripts/check_no_planning_docs.sh
test-and-lint:
name: Test and Lint
needs: skip-check
if: needs.skip-check.outputs.should_skip != 'true'
runs-on: ubicloud-standard-4
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: sm-standard-4
timeout-minutes: 60
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-test-${{ hashFiles('**/Cargo.lock') }}
cache-shared-key: ci-test
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
# Clippy runs before the test pass: lint failures are the most common
# CI-only breakage and should surface in minutes, not after 20+ minutes
# of tests.
- name: Run clippy lints
run: cargo clippy --all-targets -- -D warnings
- name: Run tests
run: |
cargo nextest run --all --exclude e2e_test
cargo nextest run --profile ci --all --exclude e2e_test
cargo test --all --doc
- name: Check code formatting
run: cargo fmt --all --check
# rustfs/backlog#1289: fail if a seed rule's log anchor no longer exists
# verbatim in the source tree (log message drifted without updating the
# rule). Placed here where the workspace — including the la-dump-anchors
# bin — is already built by the clippy/test steps above.
- name: Check log-analyzer rule anchors
run: ./scripts/check_log_analyzer_rules.sh
- name: Run clippy lints
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Upload test junit report
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: junit-test-and-lint-${{ github.run_number }}
path: target/nextest/ci/junit.xml
retention-days: 3
if-no-files-found: ignore
- name: Check layered dependencies
run: ./scripts/check_layer_dependencies.sh
# Explicit gate for migration-critical suites. These tests already ran in
# the full nextest pass above; a single filtered nextest invocation keeps
# the named gate without rebuilding or re-running them one package at a time.
#
# The gate selects tests by name substring (data_movement / rebalance /
# decommission / source_cleanup / delete_marker), so renames can silently
# thin it. The script owns the filter expression and first verifies the
# selected-test count against the committed floor in
# .config/migration-gate-floor.txt before running the gate; renames or
# removals must update that file consciously (see the script header).
# Kept on the default profile (no --profile ci): a second --profile ci run
# would clobber target/nextest/ci/junit.xml, and none of these tests are
# quarantined so they gain nothing from the ci profile's retry overrides.
- name: Run rebalance/decommission migration proofs
run: ./scripts/check_migration_gate_count.sh
# Dedicated serial lane for the ILM / lifecycle integration tests. These tests
# drive the object layer through process-global singletons (the GLOBAL_ENV
# ECStore, the global tier-config manager, background-expiry workers) and bind
# fixed ports (9002 for the scanner suite, 9003 for the app suite), so they are
# marked #[ignore] to keep them out of the default parallel `cargo nextest run
# --all` pass. Run them here explicitly with `--run-ignored ignored-only` and
# `-j1` so no two of them share global state or race for a port at once.
# serial_test's #[serial] does NOT serialize across nextest's process-per-test
# boundary (see .config/nextest.toml); `-j1` is what actually serializes them.
# See rustfs/backlog#1148 (ilm-1) and #1155.
test-ilm-integration-serial:
name: ILM Integration (serial)
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: sm-standard-4
timeout-minutes: 45
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-ilm-serial
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
# test_transition_and_restore_flows was re-enabled by rustfs/backlog#1303:
# its "missing xl.meta on disk2" was a test-util bug (open_disk hardcoded
# disk_index 0), not an EC metadata-distribution issue.
# restore_object_usecase_reports_ongoing_conflict_and_completion was
# re-enabled by backlog#1304 (restore accepts serialize on a short CAS
# guard; the copy-back no longer holds the #4877 whole-copy-back lock,
# so the mid-restore ongoing read and fast 409 rejection it asserts are
# the implemented contract). The remaining exclusions each hit a
# DIFFERENT, independent issue (all tracked under rustfs/backlog#1148;
# they keep #[ignore] with a backlog reference):
# - test_noncurrent_{expiry,transition}_still_works_after_immediate_compensation_transition:
# noncurrent transition/expiry after an immediate compensation transition.
- name: Run ignored ILM integration tests serially
run: |
cargo nextest run -j1 --run-ignored ignored-only \
-p rustfs-scanner -p rustfs \
-E '(binary(lifecycle_integration_test) or (package(rustfs) and test(lifecycle_transition_api_test))) and not (test(test_noncurrent_expiry_still_works_after_immediate_compensation_transition) or test(test_noncurrent_transition_still_works_after_immediate_compensation_transition))'
test-and-lint-rio-v2:
name: Test and Lint (rio-v2)
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: sm-standard-4
timeout-minutes: 60
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-test-rio-v2
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run rio-v2 clippy lints
run: cargo clippy -p rustfs -p rustfs-ecstore --all-targets --features rio-v2 -- -D warnings
- name: Run rio-v2 feature tests
run: |
cargo nextest run -p rustfs -p rustfs-ecstore --features rio-v2
cargo test -p rustfs --doc --features rio-v2
test-and-lint-protocols:
name: "Test and Lint (${{ matrix.features.name }})"
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: sm-standard-4
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
features:
- name: swift
flags: "--features swift"
- name: sftp
flags: "--features sftp"
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-test-${{ matrix.features.name }}
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run clippy with ${{ matrix.features.name }}
run: |
cargo clippy -p rustfs -p rustfs-protocols --all-targets ${{ matrix.features.flags }} -- -D warnings
- name: Run tests with ${{ matrix.features.name }}
run: |
cargo nextest run -p rustfs -p rustfs-protocols ${{ matrix.features.flags }}
build-rustfs-debug-binary:
name: Build RustFS Debug Binary
needs: skip-check
if: needs.skip-check.outputs.should_skip != 'true'
runs-on: ubicloud-standard-4
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: sm-standard-4
timeout-minutes: 30
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-rustfs-debug-binary-${{ hashFiles('**/Cargo.lock') }}
cache-shared-key: ci-rustfs-debug-binary
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build debug binary
run: |
touch rustfs/build.rs
cargo build -p rustfs --bins --jobs 2
run: cargo build -p rustfs --bins
- name: Upload debug binary
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-debug-binary
path: target/debug/rustfs
if-no-files-found: error
retention-days: 1
e2e-tests:
name: End-to-End Tests
needs: [ skip-check, build-rustfs-debug-binary ]
if: needs.skip-check.outputs.should_skip != 'true'
runs-on: ubicloud-standard-2
build-rustfs-debug-binary-rio-v2:
name: Build RustFS Debug Binary (rio-v2)
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: sm-standard-4
timeout-minutes: 30
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-rustfs-debug-binary-rio-v2
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build debug binary with rio-v2
run: cargo build -p rustfs --bins --features rio-v2
- name: Upload debug binary
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: rustfs-debug-binary-rio-v2
path: target/debug/rustfs
if-no-files-found: error
retention-days: 1
uring-integration:
name: io_uring Integration (real)
# GitHub-hosted ubuntu-latest runs a recent kernel with io_uring and, unlike
# a container, applies no seccomp filter that would block io_uring_setup — so
# the probe succeeds and the tests exercise the real UringBackend/FdCache/
# latch paths instead of the StdBackend fallback (rustfs/backlog#1179). The
# self-hosted sm-standard runners cannot guarantee this.
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Clean up previous test run
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-uring
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install build dependencies
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
# ext4 supports O_DIRECT; the runner's default TMPDIR may sit on tmpfs or
# overlayfs, where open(O_DIRECT) returns EINVAL/EOPNOTSUPP and the native
# read_at_direct path silently latches off to the aligned StdBackend
# fallback. A dedicated ext4 loopback guarantees the native O_DIRECT read
# path is actually exercised, not just the fallback (rustfs/backlog#1220).
- name: Prepare ext4 loopback for O_DIRECT coverage
run: |
rm -rf /tmp/rustfs
rm -f /tmp/rustfs.log
set -euo pipefail
dd if=/dev/zero of=/tmp/rustfs-odirect.img bs=1M count=1024
mkfs.ext4 -q -F /tmp/rustfs-odirect.img
sudo mkdir -p /mnt/rustfs-odirect
sudo mount -o loop /tmp/rustfs-odirect.img /mnt/rustfs-odirect
sudo chmod 1777 /mnt/rustfs-odirect
mount | grep rustfs-odirect
# RUSTFS_URING_TESTS_MUST_RUN=1 makes the io_uring tests fail instead of
# silently skipping if io_uring is unavailable, so this leg can never pass
# vacuously. RUSTFS_IO_URING_READ_ENABLE=true selects the UringBackend.
# TMPDIR points every tempfile-based test fixture at the ext4 loopback so
# eligible reads keep O_DIRECT and the native path is covered end to end.
- name: Run ecstore io_uring integration tests on real io_uring
env:
RUSTFS_IO_URING_READ_ENABLE: "true"
RUSTFS_URING_TESTS_MUST_RUN: "1"
TMPDIR: /mnt/rustfs-odirect
run: cargo test -p rustfs-ecstore uring_ -- --test-threads=1 --nocapture
e2e-tests:
name: End-to-End Tests
needs: [ build-rustfs-debug-binary ]
runs-on: sm-standard-2
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# Full setup with dependency caching: the smoke-suite step below
# compiles the e2e_test crate, which pulls in most of the workspace.
# Without a restored cache this was a cold multi-GB build on every run.
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-e2e
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
# Download after the cache restore so the freshly built binary from the
# build job always wins over anything restored into target/debug.
- name: Download debug binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: rustfs-debug-binary
path: target/debug
@@ -193,15 +450,142 @@ jobs:
- name: Make binary executable
run: chmod +x ./target/debug/rustfs
- name: Setup Rust toolchain for s3s-e2e installation
uses: dtolnay/rust-toolchain@stable
# Guard the security negative-auth smoke subset (backlog#1151 sec-5)
# against a rename or deletion silently dropping it out of the e2e-smoke
# filter. The script lists what the profile selects and fails if the count
# of security auth-rejection tests falls below the committed floor in
# .config/security-smoke-floor.txt (infra-12 count-floor mechanism). Run
# before the smoke suite so a thinned gate fails fast; the `nextest list`
# here compiles the e2e_test binaries the run below reuses.
- name: Check security smoke subset count floor
run: ./scripts/check_security_smoke_count.sh check
# PR smoke subset of the in-repo e2e suite (backlog#1149 ci-4). The
# profile.e2e-smoke default-filter in .config/nextest.toml is the single
# wiring mechanism for e2e tests in CI — extend that filter instead of
# adding new e2e jobs here. Each test spawns its own rustfs server on a
# random port and reuses the downloaded debug binary above.
- name: Run e2e smoke suite
run: cargo nextest run --profile e2e-smoke -p e2e_test
- name: Install s3s-e2e test tool
uses: taiki-e/cache-cargo-install-action@v2
uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # v2
with:
tool: s3s-e2e
git: https://github.com/s3s-project/s3s.git
rev: 4a04a670cf41274d9be9ab65dc36f4aa3f92fbad
rev: 62cb4a71dd759a6ec56b64c4c42fcc183a2c6a52
- name: Run end-to-end tests
run: |
s3s-e2e --version
RUN_ROOT="${RUNNER_TEMP}/rustfs-e2e-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}"
mkdir -p "${RUN_ROOT}"
RUSTFS_TEST_PORT="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')"
RUSTFS_TEST_PORT="${RUSTFS_TEST_PORT}" \
RUSTFS_TEST_LOG="${RUN_ROOT}/rustfs.log" \
./scripts/e2e-run.sh ./target/debug/rustfs "${RUN_ROOT}/data"
- name: Upload test logs
if: failure()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: e2e-test-logs-${{ github.run_number }}
path: ${{ runner.temp }}/rustfs-e2e-*/rustfs.log
retention-days: 3
e2e-full:
name: End-to-End Tests (full merge gate)
# Merge gate only (backlog#1149 ci-5): the never-automated user-visible
# suites — KMS, object_lock, multipart_auth, quota, checksum, encryption,
# security-boundary, ... — via the e2e-full nextest profile. Too heavy for
# every PR, so it is gated to main pushes, the merge queue, and manual
# dispatch. protocols / the 6 cluster suites / replication / #[ignore] are
# owned by other lanes (see .config/nextest.toml profile.e2e-full).
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'merge_group' ||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
needs: [ build-rustfs-debug-binary ]
runs-on: sm-standard-2
timeout-minutes: 55
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-e2e
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
# Download after the cache restore so the freshly built binary from the
# build job always wins over anything restored into target/debug.
- name: Download debug binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: rustfs-debug-binary
path: target/debug
- name: Make binary executable
run: chmod +x ./target/debug/rustfs
# Full single-node e2e lane (backlog#1149 ci-5). The e2e-full
# default-filter in .config/nextest.toml is the single wiring mechanism —
# extend that filter, never add ad-hoc e2e jobs here. Reuses the downloaded
# debug binary; each test spawns its own rustfs server on a random port.
- name: Run e2e full suite
run: cargo nextest run --profile e2e-full -p e2e_test
- name: Upload junit
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: e2e-full-junit-${{ github.run_number }}
path: target/nextest/e2e-full/junit.xml
retention-days: 7
e2e-tests-rio-v2:
name: End-to-End Tests (rio-v2)
needs: [ build-rustfs-debug-binary-rio-v2 ]
runs-on: sm-standard-2
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Clean up previous test run
run: |
rm -rf /tmp/rustfs
rm -f /tmp/rustfs.log
- name: Download debug binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: rustfs-debug-binary-rio-v2
path: target/debug
- name: Make binary executable
run: chmod +x ./target/debug/rustfs
- name: Setup Rust toolchain for s3s-e2e installation
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
# The sm-standard-* custom runner images (introduced in #4884) ship no C
# toolchain, unlike GitHub-hosted ubuntu-latest. Installing s3s-e2e below
# compiles it from source on a cache miss, and build scripts need cc.
- name: Install build tools for s3s-e2e compilation
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libssl-dev
- name: Install s3s-e2e test tool
uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # v2
with:
tool: s3s-e2e
git: https://github.com/s3s-project/s3s.git
rev: 62cb4a71dd759a6ec56b64c4c42fcc183a2c6a52
- name: Run end-to-end tests
run: |
@@ -210,24 +594,23 @@ jobs:
- name: Upload test logs
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: e2e-test-logs-${{ github.run_number }}
name: e2e-test-logs-rio-v2-${{ github.run_number }}
path: /tmp/rustfs.log
retention-days: 3
s3-implemented-tests:
name: S3 Implemented Tests
needs: [ skip-check, build-rustfs-debug-binary ]
if: needs.skip-check.outputs.should_skip != 'true'
runs-on: ubicloud-standard-4
needs: [ build-rustfs-debug-binary, e2e-tests ]
runs-on: sm-standard-4
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Download debug binary
uses: actions/download-artifact@v7
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: rustfs-debug-binary
path: target/debug
@@ -237,17 +620,98 @@ jobs:
- name: Run implemented s3-tests
run: |
RUN_ROOT="${RUNNER_TEMP}/rustfs-s3tests-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}"
mkdir -p "${RUN_ROOT}"
S3_PORT="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')"
DEPLOY_MODE=binary \
RUSTFS_BINARY=./target/debug/rustfs \
TEST_MODE=single \
MAXFAIL=1 \
MAXFAIL=0 \
XDIST=4 \
S3_PORT="${S3_PORT}" \
DATA_ROOT="${RUN_ROOT}" \
S3TESTS_CONF=artifacts/s3tests-single/s3tests.conf \
./scripts/s3-tests/run.sh
- name: Upload s3 test artifacts
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: s3tests-implemented-${{ github.run_number }}
path: artifacts/s3tests-single/**
if-no-files-found: ignore
retention-days: 3
# Dedicated lifecycle behavior lane (backlog#1148 ilm-10, master plan #1155).
#
# These ceph/s3-tests cases assert that objects/versions/uploads are actually
# removed by the background scanner and the stale-multipart cleanup loop, so
# they need a debug-accelerated day plus an enabled scanner. That configuration
# cannot be applied to the s3-implemented-tests lane above because a global
# RUSTFS_ILM_DEBUG_DAY_SECS also shrinks the x-amz-expiration header those tests
# assert on (test_lifecycle_expiration_header_*). Hence a separate server here.
#
# RUSTFS_ILM_DEBUG_DAY_SECS MUST stay equal to the s3-tests lc_debug_interval
# default (10s). test_lifecycle_expiration polls the Days=1 rule with a
# 4*lc_interval window while the Days=5 rule fires at 5*debug_day; observing
# the intermediate 4-object plateau requires 4*lc_interval < 5*debug_day, i.e.
# debug_day > 8. A smaller value lets the Days=5 rule fire inside the Days=1
# poll window so the count collapses straight to 2 and the test fails (#4764).
#
# RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES=1 is also required. A compacted directory
# is only re-descended (and its objects re-evaluated for ILM) once every
# DATA_USAGE_UPDATE_DIR_CYCLES scanner cycles (default 16). At the accelerated
# RUSTFS_SCANNER_CYCLE=2 that is ~32s between evaluations, so a Days=1 object
# that becomes due at debug_day (10s) is not actually expired until the next
# ~32s boundary (~42s), landing just past the 4*lc_interval (40s) poll window
# and making the count stall at 6 (#4767). Forcing every-cycle re-descent
# evaluates ILM within ~2s of the due time, well inside the poll window.
s3-lifecycle-behavior-tests:
name: S3 Lifecycle Behavior Tests
needs: [ build-rustfs-debug-binary ]
runs-on: sm-standard-4
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Download debug binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: rustfs-debug-binary
path: target/debug
- name: Make binary executable
run: chmod +x ./target/debug/rustfs
- name: Run lifecycle behavior s3-tests
run: |
RUN_ROOT="${RUNNER_TEMP}/rustfs-s3tests-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}"
mkdir -p "${RUN_ROOT}"
S3_PORT="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')"
DEPLOY_MODE=binary \
RUSTFS_BINARY=./target/debug/rustfs \
TEST_MODE=single \
MAXFAIL=0 \
XDIST=0 \
IMPLEMENTED_TESTS_FILE=scripts/s3-tests/lifecycle_behavior_tests.txt \
RUSTFS_ILM_DEBUG_DAY_SECS=10 \
RUSTFS_ILM_PROCESS_TIME=1 \
RUSTFS_SCANNER_ENABLED=true \
RUSTFS_SCANNER_CYCLE=2 \
RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES=1 \
RUSTFS_SCANNER_START_DELAY_SECS=0 \
RUSTFS_API_STALE_UPLOADS_CLEANUP_INTERVAL=2s \
S3_PORT="${S3_PORT}" \
DATA_ROOT="${RUN_ROOT}" \
S3TESTS_CONF=artifacts/s3tests-single/s3tests.conf \
./scripts/s3-tests/run.sh
- name: Upload s3 test artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: s3tests-lifecycle-behavior-${{ github.run_number }}
path: artifacts/s3tests-single/**
if-no-files-found: ignore
retention-days: 3
+17 -5
View File
@@ -16,7 +16,7 @@ name: CLA Check
on:
pull_request_target:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, closed]
merge_group:
types: [checks_requested]
issue_comment:
@@ -28,14 +28,26 @@ permissions:
issues: write
checks: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
cancel-in-progress: true
jobs:
cancel-closed-pr-runs:
name: Cancel Closed PR Runs
if: github.event_name == 'pull_request_target' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Explain cancellation run
run: echo "PR closed; this run only cancels older runs in the same concurrency group."
cla:
if: ${{ github.event_name != 'issue_comment' || github.event.issue.pull_request }}
if: ${{ (github.event_name != 'issue_comment' || github.event.issue.pull_request) && (github.event_name != 'pull_request_target' || github.event.action != 'closed') }}
runs-on: ubuntu-latest
steps:
- name: Report CLA result for merge queue
if: github.event_name == 'merge_group'
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await github.rest.checks.create({
@@ -54,7 +66,7 @@ jobs:
- name: Create token for rustfs/cla
if: github.event_name != 'merge_group'
id: registry-token
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ vars.CLA_BOT_APP_ID }}
private-key: ${{ secrets.CLA_BOT_APP_PRIVATE_KEY }}
@@ -64,7 +76,7 @@ jobs:
- name: Run CLA Bot
if: github.event_name != 'merge_group'
uses: overtrue/cla-bot@v0.0.9
uses: overtrue/cla-bot@5e5929846a60cfb42b16d0d38e135ef67d94fc88 # v0.0.9
with:
github-token: ${{ github.token }}
registry-token: ${{ steps.registry-token.outputs.token }}
+122
View File
@@ -0,0 +1,122 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Weekly workspace line-coverage baseline (backlog#1153 infra-5).
#
# NON-BLOCKING by design: this workflow only runs on schedule and manual
# dispatch, so it never attaches a status to a PR and must never be made a
# required check. It exists to give coverage a visible baseline and trend
# (per-crate table in the job summary, lcov artifact kept 90 days) — the
# per-crate ratchet for the security-critical crates builds on it later
# (backlog#1153 infra-6, report-only first per the ci-11 ladder).
#
# Measurement scope matches the PR test gate (ci.yml "Run tests"):
# `--workspace --exclude e2e_test` with the `ci` nextest profile. Doctests are
# NOT measured (ci.yml runs them uninstrumented; `cargo llvm-cov` needs a
# nightly toolchain to cover doctests). Trend-comparison workflow:
# docs/testing/README.md "Coverage" section. `make coverage` is the local
# equivalent. Scheduled failures alert via the ci-8 composite action.
name: coverage
on:
workflow_dispatch:
schedule:
# 07:00 UTC Sunday — staggered clear of the other Sunday crons: ci (00:00),
# build (01:00), e2e-s3tests (02:00), audit (03:00), nix-flake-update
# (05:00), mint (06:00), and the daily fuzz (02:00), minio-interop (03:17),
# e2e-replication-nightly (04:00) and performance-ab (06:00) lanes.
- cron: "0 7 * * 0"
# Only alert-on-failure needs more than read access; it declares its own
# job-level `issues: write`.
permissions:
contents: read
jobs:
coverage:
name: Workspace coverage (weekly)
runs-on: sm-standard-4
# The instrumented build cannot reuse the regular CI cache (different
# RUSTFLAGS), so a cold week rebuilds the workspace before running the
# full suite; give it double the test job's 60-minute budget.
timeout-minutes: 120
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# Match the PR gate's nextest semantics (ci.yml runs `--profile ci`):
# retries=0 plus the quarantine list and the ecstore-serial-flaky
# serialization. Set via env because `cargo llvm-cov`'s own --profile
# flag selects the *cargo build* profile, not the nextest profile.
NEXTEST_PROFILE: ci
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-coverage
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
with:
tool: cargo-llvm-cov
- name: Install llvm-tools component
run: rustup component add llvm-tools-preview
- name: Run instrumented test suite
run: cargo llvm-cov nextest --workspace --exclude e2e_test --no-report
- name: Generate lcov and JSON reports
run: |
mkdir -p target/llvm-cov
cargo llvm-cov report --lcov --output-path target/llvm-cov/lcov.info
cargo llvm-cov report --json --output-path target/llvm-cov/coverage.json
- name: Write per-crate summary
run: python3 scripts/coverage_per_crate.py target/llvm-cov/coverage.json >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: coverage-lcov-${{ github.run_number }}
path: |
target/llvm-cov/lcov.info
target/llvm-cov/coverage.json
retention-days: 90
if-no-files-found: ignore
alert-on-failure:
name: Alert on scheduled failure
needs: [coverage]
# Only scheduled runs open/append the tracking issue (backlog#1149 ci-8);
# manual workflow_dispatch runs stay quiet so a debugging run never files a
# spurious alert.
if: always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+110 -52
View File
@@ -70,10 +70,20 @@ env:
DOCKER_PLATFORMS: linux/amd64,linux/arm64
jobs:
# Check if we should build Docker images
# Check if we should build Docker images.
# Short-circuit at job level so per-merge development builds (workflow_run
# from a push to main) skip the whole run without spawning a checkout job:
# images are only published for tag builds — build.yml push triggers cover
# only main and tags, so a non-main head_branch is a tag name — and for
# manual dispatch.
build-check:
name: Docker Build Check
runs-on: ubicloud-standard-2
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch != 'main')
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
should_push: ${{ steps.check.outputs.should_push }}
@@ -84,9 +94,8 @@ jobs:
create_latest: ${{ steps.check.outputs.create_latest }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
# For workflow_run events, checkout the specific commit that triggered the workflow
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
@@ -163,11 +172,10 @@ jobs:
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
build_type="prerelease"
is_prerelease=true
# TODO: Temporary change - currently allows alpha versions to also create latest tags
# After the version is stable, you need to remove the following line and restore the original logic (latest is created only for stable versions)
if [[ "$version" == *"alpha"* ]]; then
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
create_latest=true
echo "🧪 Building Docker image for prerelease: $version (temporarily allowing creation of latest tag)"
echo "🧪 Building Docker image for prerelease: $version (creating latest tag)"
else
echo "🧪 Building Docker image for prerelease: $version"
fi
@@ -216,11 +224,10 @@ jobs:
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
build_type="prerelease"
is_prerelease=true
# TODO: Temporary change - currently allows alpha versions to also create latest tags
# After the version is stable, you need to remove the if block below and restore the original logic.
if [[ "$input_version" == *"alpha"* ]]; then
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
create_latest=true
echo "🧪 Building with prerelease version: $input_version (temporarily allowing creation of latest tag)"
echo "🧪 Building with prerelease version: $input_version (creating latest tag)"
else
echo "🧪 Building with prerelease version: $input_version"
fi
@@ -240,13 +247,15 @@ jobs:
esac
fi
echo "should_build=$should_build" >> $GITHUB_OUTPUT
echo "should_push=$should_push" >> $GITHUB_OUTPUT
echo "build_type=$build_type" >> $GITHUB_OUTPUT
echo "version=$version" >> $GITHUB_OUTPUT
echo "short_sha=$short_sha" >> $GITHUB_OUTPUT
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
echo "create_latest=$create_latest" >> $GITHUB_OUTPUT
{
echo "should_build=$should_build"
echo "should_push=$should_push"
echo "build_type=$build_type"
echo "version=$version"
echo "short_sha=$short_sha"
echo "is_prerelease=$is_prerelease"
echo "create_latest=$create_latest"
} >> "$GITHUB_OUTPUT"
echo "🐳 Docker Build Summary:"
echo " - Should build: $should_build"
@@ -265,55 +274,54 @@ jobs:
name: Build Docker Images
needs: build-check
if: needs.build-check.outputs.should_build == 'true'
runs-on: ubicloud-standard-2
runs-on: dind-sm-standard-2
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- variant: musl
file: Dockerfile
suffix: ""
- variant: glibc
file: Dockerfile.glibc
suffix: "-glibc"
fail-fast: false
matrix:
include:
- variant: musl
file: Dockerfile
suffix: ""
- variant: glibc
file: Dockerfile.glibc
suffix: "-glibc"
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_PASSWORD }}
- name: Login to Quay.io
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Extract metadata and generate tags
id: meta
run: |
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
VERSION="${{ needs.build-check.outputs.version }}"
SHORT_SHA="${{ needs.build-check.outputs.short_sha }}"
CREATE_LATEST="${{ needs.build-check.outputs.create_latest }}"
VARIANT_SUFFIX="${{ matrix.suffix }}"
@@ -336,8 +344,8 @@ jobs:
;;
esac
echo "docker_release=$DOCKER_RELEASE" >> $GITHUB_OUTPUT
echo "docker_channel=$DOCKER_CHANNEL" >> $GITHUB_OUTPUT
echo "docker_release=$DOCKER_RELEASE" >> "$GITHUB_OUTPUT"
echo "docker_channel=$DOCKER_CHANNEL" >> "$GITHUB_OUTPUT"
echo "🐳 Docker build parameters:"
echo " - Original version: $VERSION"
@@ -351,9 +359,7 @@ jobs:
# Add channel tags for prereleases and latest for stable
if [[ "$CREATE_LATEST" == "true" ]]; then
# TODO: Temporary change - the current alpha version will also create the latest tag
# After the version is stabilized, the logic here remains unchanged, but the upstream CREATE_LATEST setting needs to be restored.
# Stable release (and temporary alpha versions)
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_GHCR }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_QUAY }}:latest${VARIANT_SUFFIX}"
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
# Prerelease channel tags (alpha, beta, rc)
@@ -371,7 +377,7 @@ jobs:
fi
# Output tags
echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
# Generate labels
LABELS="org.opencontainers.image.title=RustFS"
@@ -382,7 +388,7 @@ jobs:
LABELS="$LABELS,org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
LABELS="$LABELS,org.opencontainers.image.build-type=$BUILD_TYPE"
echo "labels=$LABELS" >> $GITHUB_OUTPUT
echo "labels=$LABELS" >> "$GITHUB_OUTPUT"
echo "🐳 Generated Docker tags:"
echo "$TAGS" | tr ',' '\n' | sed 's/^/ - /'
@@ -390,7 +396,7 @@ jobs:
echo "🔖 Version: $VERSION"
- name: Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: ${{ matrix.file }}
@@ -410,9 +416,8 @@ jobs:
RELEASE=${{ steps.meta.outputs.docker_release }}
CHANNEL=${{ steps.meta.outputs.docker_channel }}
BUILDKIT_INLINE_CACHE=1
# Enable advanced BuildKit features for better performance
provenance: false
sbom: false
provenance: true
sbom: true
# Add retry mechanism by splitting the build process
no-cache: false
pull: true
@@ -420,12 +425,66 @@ jobs:
# Note: Manifest creation is no longer needed as we only build one variant
# Multi-arch manifests are automatically created by docker/build-push-action
# Report-only container image vulnerability scan
scan-docker-image:
name: Scan Docker Images
needs: [ build-check, build-docker ]
if: needs.build-check.outputs.should_build == 'true' && needs.build-check.outputs.should_push == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
include:
- variant: musl
suffix: ""
- variant: glibc
suffix: "-glibc"
steps:
- name: Login to GitHub Container Registry
# docker/login-action v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_PASSWORD }}
- name: Scan image with Trivy
# aquasecurity/trivy-action v0.36.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25
with:
image-ref: ${{ env.REGISTRY_GHCR }}:${{ needs.build-check.outputs.version }}${{ matrix.suffix }}
format: sarif
output: trivy-${{ matrix.variant }}.sarif
ignore-unfixed: true
vuln-type: os,library
severity: CRITICAL,HIGH
exit-code: "0"
# Surface findings in the Security tab; an artifact alone is write-only
# reporting nobody reviews.
- name: Upload scan results to code scanning
uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
sarif_file: trivy-${{ matrix.variant }}.sarif
category: container-image-${{ matrix.variant }}
- name: Upload container scan report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: container-image-scan-${{ matrix.variant }}
path: trivy-${{ matrix.variant }}.sarif
if-no-files-found: error
retention-days: 30
# Docker build summary
docker-summary:
name: Docker Build Summary
needs: [ build-check, build-docker ]
if: always() && needs.build-check.outputs.should_build == 'true'
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
steps:
- name: Docker build completion summary
run: |
@@ -450,10 +509,9 @@ jobs:
"prerelease")
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
echo "⚠️ This is a prerelease image - use with caution"
# TODO: Temporary change - alpha versions currently create the latest tag
# After the version is stable, you need to restore the following prompt information
if [[ "$VERSION" == *"alpha"* ]] && [[ "$CREATE_LATEST" == "true" ]]; then
echo "🏷️ Latest tag has been created for alpha version (temporary measures)"
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
if [[ "$CREATE_LATEST" == "true" ]]; then
echo "🏷️ Latest tag has been created for prerelease: $VERSION"
else
echo "🚫 Latest tag NOT created for prerelease"
fi
@@ -0,0 +1,130 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Nightly full replication e2e lane (backlog#1147 repl-1, deps: ci-4).
#
# The per-PR gate (ci.yml `e2e-tests` job, `--profile e2e-smoke`) runs the 20
# FAST replication tests. This scheduled lane runs the remaining 27
# heavier replication e2e tests that are unfit for a per-PR gate:
#
# * 2 remote-target TLS validation tests.
# * 12 bucket-replication data-plane/helper tests (PUT/delete + poll for
# convergence; two replicate over HTTPS, two pin active SSE failure
# contracts, and one guards event/history observers). The SSE-S3 contract
# remains ignored under backlog#1291.
# * 11 `_real_dual_node` site-replication tests (each spawns TWO rustfs
# servers and drives the cross-process site-replication control plane).
# * 1 `_real_three_node` site-replication test.
# * 1 `_real_single_node` service-account round-trip test.
#
# The selection is the [profile.e2e-repl-nightly] default-filter in
# .config/nextest.toml — the single wiring mechanism (repl-1 / ci-4). Do NOT
# add ad-hoc cargo-test steps here; change the filterset instead.
#
# Explicit division of labor: these 27 tests run ONLY here, never double-run
# in ci-5's future e2e-full merge gate. TODO(ci-7): once the ci domain's
# consolidated scheduled e2e workflow exists, fold this interim repl-owned lane
# into it rather than growing a second scheduled entrypoint.
name: e2e-replication-nightly
on:
workflow_dispatch:
schedule:
# 04:00 UTC nightly — staggered clear of fuzz/e2e-s3tests (02:00),
# stale (01:30) and performance-ab (06:00).
- cron: "0 4 * * *"
# Only alert-on-failure needs more than read access; it declares its own
# job-level `issues: write`.
permissions:
contents: read
jobs:
repl-nightly:
name: Replication e2e (nightly)
# dual-node tests spawn two full rustfs servers each; the extra headroom
# over the per-PR sm-standard-2 e2e job keeps the parallel process fan-out
# from starving the runner. Mirrors the ILM serial lane's runner class.
runs-on: sm-standard-4
timeout-minutes: 45
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-e2e-repl
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
# awscurl lets the STS dual-node test actually exercise its path. Without
# it the test skips gracefully with a visible log line
# (`awscurl_available()` in crates/e2e_test/src/common.rs), so the lane
# still passes — installing it just upgrades that one test from skip to
# real coverage.
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- name: Install awscurl
run: |
python3 -m pip install --user --upgrade pip awscurl
echo "AWSCURL_PATH=$HOME/.local/bin/awscurl" >> "$GITHUB_ENV"
- name: Verify awscurl
run: test -x "$AWSCURL_PATH"
# Build the rustfs binary once up front. The e2e tests spawn it as a
# child process (crates/e2e_test/src/common.rs) and will build it on
# demand otherwise, but a single explicit build avoids several parallel
# nextest test processes racing to build it at once.
- name: Build rustfs binary
run: cargo build -p rustfs --bins
- name: Run replication e2e nightly suite
run: cargo nextest run --profile e2e-repl-nightly -p e2e_test
- name: Upload nextest junit report
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: e2e-replication-nightly-junit-${{ github.run_number }}
path: target/nextest/e2e-repl-nightly/junit.xml
retention-days: 7
if-no-files-found: ignore
alert-on-failure:
name: Alert on scheduled failure
needs: [repl-nightly]
# Only scheduled runs open/append the tracking issue (backlog#1149 ci-8);
# manual workflow_dispatch runs stay quiet so a debugging run never files a
# spurious alert.
if: always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+210 -293
View File
@@ -12,64 +12,140 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Full ceph/s3-tests sweep against RustFS.
#
# The per-PR compatibility gate lives in ci.yml (job s3-implemented-tests) and
# runs only the implemented_tests.txt whitelist. This workflow complements it:
#
# - Scheduled weekly full sweep (TEST_SCOPE=all): runs the ENTIRE upstream
# suite and reports promotion candidates (tests that newly pass) and
# unclassified tests. The job fails only on regressions in the implemented
# whitelist or on infrastructure errors — expected failures from
# not-yet-implemented features do not turn the run red.
# - Manual runs (workflow_dispatch): same, with configurable mode/scope.
#
# All test execution is delegated to scripts/s3-tests/run.sh (single source of
# truth, also used locally and by the PR gate). This workflow only provisions
# the server topology: a single node, or a real 4-node distributed cluster
# (erasure-coded across nodes) behind an HAProxy round-robin load balancer.
#
# Runner infrastructure (ci-1, backlog#1149): this sweep needs BOTH a working
# Docker daemon (to build the source image and run the single/multi topologies)
# AND a Python toolchain with pip (for awscurl/tox and the pytest harness).
# It previously ran on the self-hosted `sm-standard-4` label, which is served
# by heterogeneous runners: some scheduled runs landed on minimal pods that
# had neither `pip` (bare python3, `No module named pip`) nor `docker.sock`,
# so "Install Python tools" died before any test executed (all 15 recorded
# runs failed). Two changes make the infra assumptions explicit instead of
# trusting drifting runner state:
# 1. Pin to GitHub-hosted `ubuntu-latest`, which reliably ships Docker,
# docker compose, python3 and pip (no self-hosted fleet drift).
# 2. Set up Python explicitly (actions/setup-python) before any pip usage,
# so the workflow stays correct even if the runner image changes.
# The PR gate (ci.yml s3-implemented-tests) is unaffected: it avoids Docker
# via DEPLOY_MODE=binary and defers all pip setup to run.sh's self-bootstrap.
name: e2e-s3tests
on:
workflow_dispatch:
inputs:
test-mode:
description: "Test mode to run"
description: "Server topology"
required: true
type: choice
default: "single"
options:
- single
- multi
scope:
description: "Test scope: full upstream suite or implemented whitelist"
required: true
type: choice
default: "all"
options:
- all
- implemented
xdist:
description: "Enable pytest-xdist (parallel). '0' to disable."
description: "pytest-xdist workers. '0' to disable."
required: false
default: "4"
maxfail:
description: "Stop after N failures. '0' to run everything."
required: false
default: "0"
maxfail:
description: "Stop after N failures (debug friendly)"
required: false
default: "1"
markexpr:
description: "pytest -m expression (feature filters)"
description: "Optional pytest -m expression"
required: false
default: "not lifecycle and not versioning and not s3website and not bucket_logging and not encryption"
default: ""
schedule:
# Weekly full sweep (Sunday 02:00 UTC): full suite, run against BOTH the
# single-node and the 4-node distributed topologies (matrix below).
- cron: "0 2 * * 0"
env:
# main user
S3_ACCESS_KEY: rustfsadmin
S3_SECRET_KEY: rustfsadmin
S3_ACCESS_KEY: rustfsadmin-ci
S3_SECRET_KEY: rustfssecret-ci
# alt user (must be different from main for many s3-tests)
S3_ALT_ACCESS_KEY: rustfsalt
S3_ALT_SECRET_KEY: rustfsalt
S3_REGION: us-east-1
S3_HOST: 127.0.0.1
S3_PORT: "9000"
RUST_LOG: info
PLATFORM: linux/amd64
BUILDX_CACHE_SCOPE: rustfs-e2e-s3tests-source
# Scheduled runs have no inputs; default to a full sweep.
TEST_SCOPE: ${{ github.event.inputs.scope || 'all' }}
XDIST: ${{ github.event.inputs.xdist || '4' }}
MAXFAIL: ${{ github.event.inputs.maxfail || '0' }}
MARKEXPR: ${{ github.event.inputs.markexpr || '' }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs['test-mode'] || 'single' }}
cancel-in-progress: true
# Only alert-on-failure needs more than read access; it declares its own
# job-level `issues: write`.
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
s3tests-single:
if: github.event.inputs['test-mode'] == 'single'
runs-on: ubicloud-standard-2
timeout-minutes: 120
s3tests:
# GitHub-hosted: reliably provides Docker + docker compose + python3/pip.
# See the header note (ci-1) for why the self-hosted sm-standard-4 label
# was abandoned. TODO(ci-8): scheduled-failure alerting (auto-open issue)
# is added by the ci-8 composite action; do not implement it here.
runs-on: ubuntu-latest
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
# Scheduled sweeps cover both topologies; manual runs use the input.
test-mode: ${{ github.event_name == 'schedule' && fromJSON('["single", "multi"]') || fromJSON(format('["{0}"]', github.event.inputs.test-mode || 'single')) }}
env:
TEST_MODE: ${{ matrix.test-mode }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# Provision Python explicitly rather than trusting the runner image to
# ship a working pip (ci-1: a bare python3 without pip is what broke the
# scheduled sweep). Keeps the workflow correct across runner drift.
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- name: Cache pip downloads
uses: actions/cache@v4
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-e2e-s3tests-${{ hashFiles('.github/workflows/e2e-s3tests.yml') }}
@@ -82,233 +158,81 @@ jobs:
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Enable buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Build RustFS image (source, cached)
run: |
DOCKER_BUILDKIT=1 docker buildx build --load \
--platform ${PLATFORM} \
--cache-from type=gha,scope=${BUILDX_CACHE_SCOPE} \
--cache-to type=gha,mode=max,scope=${BUILDX_CACHE_SCOPE} \
--platform "${PLATFORM}" \
--cache-from "type=gha,scope=${BUILDX_CACHE_SCOPE}" \
--cache-to "type=gha,mode=max,scope=${BUILDX_CACHE_SCOPE}" \
-t rustfs-ci \
-f Dockerfile.source .
- name: Create network
run: docker network inspect rustfs-net >/dev/null 2>&1 || docker network create rustfs-net
- name: Remove existing rustfs-single (if any)
run: docker rm -f rustfs-single >/dev/null 2>&1 || true
- name: Start single RustFS
if: env.TEST_MODE == 'single'
run: |
docker network inspect rustfs-net >/dev/null 2>&1 || docker network create rustfs-net
docker rm -f rustfs-single >/dev/null 2>&1 || true
# The four disks share one physical device on the runner (a single
# loopback filesystem), so the local physical-disk-independence guard
# would refuse to start. Bypass it — this is the CI use case the guard
# explicitly sanctions via RUSTFS_UNSAFE_BYPASS_DISK_CHECK.
docker run -d --name rustfs-single \
--network rustfs-net \
-p 9000:9000 \
-p "${S3_PORT}:9000" \
-e RUSTFS_ADDRESS=0.0.0.0:9000 \
-e RUSTFS_ACCESS_KEY=$S3_ACCESS_KEY \
-e RUSTFS_SECRET_KEY=$S3_SECRET_KEY \
-e RUSTFS_VOLUMES="/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3" \
-e RUSTFS_ACCESS_KEY="${S3_ACCESS_KEY}" \
-e RUSTFS_SECRET_KEY="${S3_SECRET_KEY}" \
-e RUSTFS_VOLUMES="/data/rustfs{0...3}" \
-e RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true \
-v /tmp/rustfs-single:/data \
rustfs-ci
- name: Wait for RustFS ready
- name: Start 4-node distributed cluster
if: env.TEST_MODE == 'multi'
run: |
for i in {1..60}; do
if curl -sf http://127.0.0.1:9000/health >/dev/null 2>&1; then
echo "RustFS is ready"
exit 0
fi
# A real distributed deployment: every node lists all endpoints in
# RUSTFS_VOLUMES so data is erasure-coded ACROSS nodes. Do not use
# node-local volume paths here — that would create four independent
# single-node stores behind the load balancer.
cat > compose.yml <<EOF
x-rustfs-node: &rustfs-node
image: rustfs-ci
networks: [rustfs-net]
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY}
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
# Each node's four disks share one physical device inside its
# container, so bypass the local physical-disk-independence guard
# (the CI use case it explicitly sanctions).
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
if [ "$(docker inspect -f '{{.State.Running}}' rustfs-single 2>/dev/null)" != "true" ]; then
echo "RustFS container not running" >&2
docker logs rustfs-single || true
exit 1
fi
sleep 2
done
echo "Health check timed out" >&2
docker logs rustfs-single || true
exit 1
- name: Generate s3tests config
run: |
export S3_HOST=127.0.0.1
envsubst < .github/s3tests/s3tests.conf > s3tests.conf
- name: Provision s3-tests alt user (required by suite)
run: |
# Admin API requires AWS SigV4 signing. awscurl is used by RustFS codebase as well.
awscurl \
--service s3 \
--region "${S3_REGION}" \
--access_key "${S3_ACCESS_KEY}" \
--secret_key "${S3_SECRET_KEY}" \
-X PUT \
-H 'Content-Type: application/json' \
-d '{"secretKey":"'"${S3_ALT_SECRET_KEY}"'","status":"enabled","policy":"readwrite"}' \
"http://127.0.0.1:9000/rustfs/admin/v3/add-user?accessKey=${S3_ALT_ACCESS_KEY}"
# Explicitly attach built-in policy via policy mapping.
# s3-tests relies on alt client being able to ListBuckets during setup cleanup.
awscurl \
--service s3 \
--region "${S3_REGION}" \
--access_key "${S3_ACCESS_KEY}" \
--secret_key "${S3_SECRET_KEY}" \
-X PUT \
"http://127.0.0.1:9000/rustfs/admin/v3/set-user-or-group-policy?policyName=readwrite&userOrGroup=${S3_ALT_ACCESS_KEY}&isGroup=false"
# Sanity check: alt user can list buckets (should not be AccessDenied).
awscurl \
--service s3 \
--region "${S3_REGION}" \
--access_key "${S3_ALT_ACCESS_KEY}" \
--secret_key "${S3_ALT_SECRET_KEY}" \
-X GET \
"http://127.0.0.1:9000/" >/dev/null
- name: Prepare s3-tests
run: |
git clone --depth 1 https://github.com/ceph/s3-tests.git s3-tests
- name: Run ceph s3-tests (debug friendly)
run: |
export PATH="$HOME/.local/bin:$PATH"
mkdir -p artifacts/s3tests-single
cd s3-tests
set -o pipefail
MAXFAIL="${{ github.event.inputs.maxfail }}"
if [ -z "$MAXFAIL" ]; then MAXFAIL="1"; fi
MARKEXPR="${{ github.event.inputs.markexpr }}"
if [ -z "$MARKEXPR" ]; then MARKEXPR="not lifecycle and not versioning and not s3website and not bucket_logging and not encryption"; fi
XDIST="${{ github.event.inputs.xdist }}"
if [ -z "$XDIST" ]; then XDIST="0"; fi
XDIST_ARGS=""
if [ "$XDIST" != "0" ]; then
# Add pytest-xdist to requirements.txt so tox installs it inside
# its virtualenv. Installing outside tox does NOT work.
echo "pytest-xdist" >> requirements.txt
XDIST_ARGS="-n $XDIST --dist=loadgroup"
fi
# Run tests from s3tests/functional (boto2+boto3 combined directory).
S3TEST_CONF=${GITHUB_WORKSPACE}/s3tests.conf \
tox -- \
-vv -ra --showlocals --tb=long \
--maxfail="$MAXFAIL" \
--junitxml=${GITHUB_WORKSPACE}/artifacts/s3tests-single/junit.xml \
$XDIST_ARGS \
s3tests/functional/test_s3.py \
-m "$MARKEXPR" \
2>&1 | tee ${GITHUB_WORKSPACE}/artifacts/s3tests-single/pytest.log
- name: Collect RustFS logs
if: always()
run: |
mkdir -p artifacts/rustfs-single
docker logs rustfs-single > artifacts/rustfs-single/rustfs.log 2>&1 || true
docker inspect rustfs-single > artifacts/rustfs-single/inspect.json || true
- name: Upload artifacts
if: always() && env.ACT != 'true'
uses: actions/upload-artifact@v6
with:
name: s3tests-single
path: artifacts/**
s3tests-multi:
if: github.event_name == 'workflow_dispatch' && github.event.inputs['test-mode'] == 'multi'
runs-on: ubicloud-standard-2
timeout-minutes: 150
steps:
- uses: actions/checkout@v6
- name: Cache pip downloads
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-e2e-s3tests-${{ hashFiles('.github/workflows/e2e-s3tests.yml') }}
restore-keys: |
${{ runner.os }}-pip-e2e-s3tests-
- name: Install Python tools
run: |
python3 -m pip install --user --upgrade pip awscurl tox
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Enable buildx
uses: docker/setup-buildx-action@v3
- name: Build RustFS image (source, cached)
run: |
DOCKER_BUILDKIT=1 docker buildx build --load \
--platform ${PLATFORM} \
--cache-from type=gha,scope=${BUILDX_CACHE_SCOPE} \
--cache-to type=gha,mode=max,scope=${BUILDX_CACHE_SCOPE} \
-t rustfs-ci \
-f Dockerfile.source .
- name: Prepare cluster compose
run: |
cat > compose.yml <<'EOF'
services:
rustfs1:
image: rustfs-ci
<<: *rustfs-node
hostname: rustfs1
networks: [rustfs-net]
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY}
RUSTFS_VOLUMES: "/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3"
volumes:
- rustfs1-data:/data
volumes: [rustfs1-data:/data]
rustfs2:
image: rustfs-ci
<<: *rustfs-node
hostname: rustfs2
networks: [rustfs-net]
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY}
RUSTFS_VOLUMES: "/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3"
volumes:
- rustfs2-data:/data
volumes: [rustfs2-data:/data]
rustfs3:
image: rustfs-ci
<<: *rustfs-node
hostname: rustfs3
networks: [rustfs-net]
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY}
RUSTFS_VOLUMES: "/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3"
volumes:
- rustfs3-data:/data
volumes: [rustfs3-data:/data]
rustfs4:
image: rustfs-ci
<<: *rustfs-node
hostname: rustfs4
networks: [rustfs-net]
environment:
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY}
RUSTFS_VOLUMES: "/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3"
volumes:
- rustfs4-data:/data
volumes: [rustfs4-data:/data]
lb:
image: haproxy:2.9
hostname: lb
networks: [rustfs-net]
ports:
- "9000:9000"
- "${S3_PORT}:9000"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
networks:
@@ -340,104 +264,97 @@ jobs:
server s4 rustfs4:9000 check
EOF
- name: Launch cluster
run: docker compose -f compose.yml up -d
docker compose -f compose.yml up -d
- name: Wait for LB ready
- name: Wait for RustFS ready
run: |
for i in {1..90}; do
if curl -sf http://127.0.0.1:9000/health >/dev/null 2>&1; then
echo "Load balancer is ready"
for _ in {1..120}; do
if curl -sf "http://${S3_HOST}:${S3_PORT}/health" >/dev/null 2>&1; then
echo "RustFS is ready"
exit 0
fi
sleep 2
done
echo "LB or backend not ready" >&2
docker compose -f compose.yml logs --tail=200 || true
echo "Health check timed out" >&2
if [ "${TEST_MODE}" = "single" ]; then
docker logs rustfs-single || true
else
docker compose -f compose.yml logs --tail=200 || true
fi
exit 1
- name: Generate s3tests config
- name: Run ceph s3-tests
run: |
export S3_HOST=127.0.0.1
envsubst < .github/s3tests/s3tests.conf > s3tests.conf
set +e
DEPLOY_MODE=existing \
TEST_MODE="${TEST_MODE}" \
TEST_SCOPE="${TEST_SCOPE}" \
XDIST="${XDIST}" \
MAXFAIL="${MAXFAIL}" \
MARKEXPR="${MARKEXPR}" \
./scripts/s3-tests/run.sh
RC=$?
set -e
- name: Provision s3-tests alt user (required by suite)
run: |
awscurl \
--service s3 \
--region "${S3_REGION}" \
--access_key "${S3_ACCESS_KEY}" \
--secret_key "${S3_SECRET_KEY}" \
-X PUT \
-H 'Content-Type: application/json' \
-d '{"secretKey":"'"${S3_ALT_SECRET_KEY}"'","status":"enabled","policy":"readwrite"}' \
"http://127.0.0.1:9000/rustfs/admin/v3/add-user?accessKey=${S3_ALT_ACCESS_KEY}"
awscurl \
--service s3 \
--region "${S3_REGION}" \
--access_key "${S3_ACCESS_KEY}" \
--secret_key "${S3_SECRET_KEY}" \
-X PUT \
"http://127.0.0.1:9000/rustfs/admin/v3/set-user-or-group-policy?policyName=readwrite&userOrGroup=${S3_ALT_ACCESS_KEY}&isGroup=false"
awscurl \
--service s3 \
--region "${S3_REGION}" \
--access_key "${S3_ALT_ACCESS_KEY}" \
--secret_key "${S3_ALT_SECRET_KEY}" \
-X GET \
"http://127.0.0.1:9000/" >/dev/null
- name: Prepare s3-tests
run: |
git clone --depth 1 https://github.com/ceph/s3-tests.git s3-tests
- name: Run ceph s3-tests (multi, debug friendly)
run: |
export PATH="$HOME/.local/bin:$PATH"
mkdir -p artifacts/s3tests-multi
cd s3-tests
set -o pipefail
MAXFAIL="${{ github.event.inputs.maxfail }}"
if [ -z "$MAXFAIL" ]; then MAXFAIL="1"; fi
MARKEXPR="${{ github.event.inputs.markexpr }}"
if [ -z "$MARKEXPR" ]; then MARKEXPR="not lifecycle and not versioning and not s3website and not bucket_logging and not encryption"; fi
XDIST="${{ github.event.inputs.xdist }}"
if [ -z "$XDIST" ]; then XDIST="0"; fi
XDIST_ARGS=""
if [ "$XDIST" != "0" ]; then
# Add pytest-xdist to requirements.txt so tox installs it inside
# its virtualenv. Installing outside tox does NOT work.
echo "pytest-xdist" >> requirements.txt
XDIST_ARGS="-n $XDIST --dist=loadgroup"
if [ "${TEST_SCOPE}" = "implemented" ]; then
# Whitelist run: every failure is a regression.
exit "${RC}"
fi
# Run tests from s3tests/functional (boto2+boto3 combined directory).
S3TEST_CONF=${GITHUB_WORKSPACE}/s3tests.conf \
tox -- \
-vv -ra --showlocals --tb=long \
--maxfail="$MAXFAIL" \
--junitxml=${GITHUB_WORKSPACE}/artifacts/s3tests-multi/junit.xml \
$XDIST_ARGS \
s3tests/functional/test_s3.py \
-m "$MARKEXPR" \
2>&1 | tee ${GITHUB_WORKSPACE}/artifacts/s3tests-multi/pytest.log
# Full sweep: failures outside the implemented whitelist are
# inventory (promotion candidates / unimplemented features), not a
# gate. Fail only on whitelist regressions or infrastructure errors.
JUNIT="artifacts/s3tests-${TEST_MODE}/junit.xml"
if [ ! -f "${JUNIT}" ]; then
echo "No junit.xml produced — infrastructure failure (exit ${RC})" >&2
exit "${RC}"
fi
python3 scripts/s3-tests/report_compat.py \
--junit "${JUNIT}" \
--lists-dir scripts/s3-tests \
--fail-on-regression
- name: Collect logs
- name: Publish compatibility report
if: always()
run: |
mkdir -p artifacts/cluster
docker compose -f compose.yml logs --no-color > artifacts/cluster/cluster.log 2>&1 || true
REPORT="artifacts/s3tests-${TEST_MODE}/compat-report.md"
if [ -f "${REPORT}" ]; then
cat "${REPORT}" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Collect RustFS logs
if: always()
run: |
mkdir -p "artifacts/rustfs-${TEST_MODE}"
if [ "${TEST_MODE}" = "single" ]; then
docker logs rustfs-single > "artifacts/rustfs-${TEST_MODE}/rustfs.log" 2>&1 || true
docker inspect rustfs-single > "artifacts/rustfs-${TEST_MODE}/inspect.json" || true
else
docker compose -f compose.yml logs --no-color > "artifacts/rustfs-${TEST_MODE}/cluster.log" 2>&1 || true
fi
- name: Upload artifacts
if: always() && env.ACT != 'true'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: s3tests-multi
name: s3tests-${{ env.TEST_MODE }}
path: artifacts/**
alert-on-failure:
name: Alert on scheduled failure
needs: [s3tests]
# `always()` is required: without it this job is skipped when a needed
# job fails. Alerts only for scheduled runs (backlog#1149 ci-8) — manual
# dispatch failures are already watched by a human.
if: always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+253
View File
@@ -0,0 +1,253 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Fuzz
on:
pull_request:
types: [ opened, synchronize, reopened, closed ]
# PR trigger is intentionally narrow: only changes to the fuzz harness
# itself gate a PR. Broad crate paths (ecstore/filemeta/utils/policy/…)
# are covered by the nightly `schedule` run below, which fuzzes against
# whatever landed on main. Widening these paths previously queued a
# ~45min fuzz-build on nearly every PR and is why this workflow was
# disabled; do not re-add crate paths here.
paths:
- "fuzz/**"
- "scripts/fuzz/**"
- ".github/workflows/fuzz.yml"
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
inputs:
profile:
description: "Which fuzz profile to run"
required: false
default: "both"
type: choice
options:
- smoke
- nightly
- both
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu
# Fuzz targets build without the dial9 feature, so tokio_unstable is not needed.
RUSTFLAGS: "-C target-feature=-crt-static"
jobs:
cancel-closed-pr-runs:
name: Cancel Closed PR Runs
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Explain cancellation run
run: echo "PR closed; this run only cancels older runs in the same concurrency group."
# ──────────────────────────────────────────────────────────────
# Phase 1: Build all fuzz harness binaries once.
# ──────────────────────────────────────────────────────────────
fuzz-build:
name: Build Fuzz Harness
if: >
(github.event_name == 'pull_request' && github.event.action != 'closed') ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
runs-on: sm-standard-4
timeout-minutes: 45
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: nightly
cache-shared-key: fuzz-${{ hashFiles('fuzz/Cargo.lock') }}
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'schedule' }}
- name: Install cargo-fuzz
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
with:
tool: cargo-fuzz
- name: Build all fuzz targets
run: BUILD_ONLY=1 ./scripts/fuzz/run.sh
- name: Stage prebuilt fuzz binaries
run: |
binary_root="fuzz/prebuilt/${CARGO_BUILD_TARGET}/release"
mkdir -p "${binary_root}"
# Keep this list in sync with the smoke/nightly matrices and
# scripts/fuzz/run.sh default target set (all 5 buildable bins in
# fuzz/Cargo.toml). The *_storage_api.rs files are `mod` submodules
# of their parent targets, not standalone bins.
for target in archive_extract bucket_validation local_metadata path_containment policy_ingress; do
install -Dm755 "fuzz/target/${CARGO_BUILD_TARGET}/release/${target}" "${binary_root}/${target}"
done
- name: Upload prebuilt fuzz binaries
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: fuzz-prebuilt-binaries-${{ github.run_number }}
path: |
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/archive_extract
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/bucket_validation
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/local_metadata
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/path_containment
fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release/policy_ingress
if-no-files-found: error
retention-days: 1
compression-level: 0
# ──────────────────────────────────────────────────────────────
# Phase 2 (PR): Run smoke targets in parallel via matrix.
# ──────────────────────────────────────────────────────────────
pr-fuzz-smoke:
name: "Smoke / ${{ matrix.target }}"
needs: fuzz-build
if: >
(github.event_name == 'pull_request' && github.event.action != 'closed') ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.profile == 'smoke' || github.event.inputs.profile == 'both'))
runs-on: sm-standard-4
timeout-minutes: 30
strategy:
fail-fast: false
# One job per target runs in parallel, so wall-clock time is per-target
# (~60s fuzz + download/chmod overhead), not the sum across the matrix.
matrix:
target: [archive_extract, bucket_validation, local_metadata, path_containment, policy_ingress]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Download prebuilt fuzz binaries
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: fuzz-prebuilt-binaries-${{ github.run_number }}
path: fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
- name: Restore executable bit
run: chmod +x "fuzz/prebuilt/${CARGO_BUILD_TARGET}/release/${{ matrix.target }}"
- name: Run ${{ matrix.target }}
env:
FUZZ_TARGET: ${{ matrix.target }}
MAX_TOTAL_TIME: 60
SKIP_BUILD: 1
USE_PREBUILT_BINARY: 1
PREBUILT_BINARY_DIR: ${{ github.workspace }}/fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
run: ./scripts/fuzz/run.sh
- name: Upload fuzz smoke artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: fuzz-smoke-${{ matrix.target }}-${{ github.run_number }}
path: |
fuzz/artifacts/**
fuzz/corpus/${{ matrix.target }}/**
if-no-files-found: ignore
retention-days: 7
# ──────────────────────────────────────────────────────────────
# Phase 2 (Nightly): Run all targets in parallel via matrix.
# ──────────────────────────────────────────────────────────────
nightly-fuzz-corpus:
name: "Nightly / ${{ matrix.target }}"
needs: fuzz-build
# TODO(ci-8): when the schedule-failure-issue composite action lands,
# add a step here (or a dependent job) that opens/updates a GitHub issue
# on nightly failure. ci-8 is the single alerting mechanism for all
# scheduled workflows; do not self-roll alerting in this workflow.
if: >
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.profile == 'nightly' || github.event.inputs.profile == 'both'))
runs-on: sm-standard-4
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
target: [archive_extract, bucket_validation, local_metadata, path_containment, policy_ingress]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Download prebuilt fuzz binaries
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: fuzz-prebuilt-binaries-${{ github.run_number }}
path: fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
- name: Restore executable bit
run: chmod +x "fuzz/prebuilt/${CARGO_BUILD_TARGET}/release/${{ matrix.target }}"
- name: Run ${{ matrix.target }}
env:
FUZZ_TARGET: ${{ matrix.target }}
MAX_TOTAL_TIME: 300
SKIP_BUILD: 1
USE_PREBUILT_BINARY: 1
PREBUILT_BINARY_DIR: ${{ github.workspace }}/fuzz/prebuilt/${{ env.CARGO_BUILD_TARGET }}/release
run: ./scripts/fuzz/run.sh
- name: Upload nightly fuzz artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: fuzz-nightly-${{ matrix.target }}-${{ github.run_number }}
path: |
fuzz/artifacts/**
fuzz/corpus/${{ matrix.target }}/**
if-no-files-found: ignore
retention-days: 30
# ──────────────────────────────────────────────────────────────
# Nightly alerting: open/update a tracking issue on failure.
# ──────────────────────────────────────────────────────────────
alert-on-failure:
name: Alert on scheduled failure
needs: [fuzz-build, nightly-fuzz-corpus]
# `always()` is required: without it this job is skipped when a needed
# job fails. Alerts only for scheduled (nightly) runs (backlog#1149
# ci-8); PR and manual dispatch failures are already watched by a human.
if: always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+62 -24
View File
@@ -18,77 +18,115 @@ on:
workflow_run:
workflows: [ "Build and Release" ]
types: [ completed ]
workflow_dispatch:
inputs:
version:
description: "Release version to publish, e.g. 1.0.0-beta.1 or v1.0.0-beta.1"
required: true
default: "1.0.0-beta.1"
type: string
permissions:
contents: read
env:
new_version: ${{ github.event.workflow_run.head_branch }}
jobs:
build-helm-package:
runs-on: ubicloud-standard-2
# Only run on successful builds triggered by tag pushes (version format: x.y.z or x.y.z-suffix)
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
contains(github.event.workflow_run.head_branch, '.')
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
contains(github.event.workflow_run.head_branch, '.')
)
outputs:
raw_tag: ${{ steps.version.outputs.raw_tag }}
app_version: ${{ steps.version.outputs.app_version }}
chart_version: ${{ steps.version.outputs.chart_version }}
steps:
- name: Checkout helm chart repo
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Replace chart app version
- name: Normalize release version
id: version
run: |
set -e
set -x
old_version=$(grep "^appVersion:" helm/rustfs/Chart.yaml | awk '{print $2}')
sed -i "s/$old_version/$new_version/g" helm/rustfs/Chart.yaml
set -eux
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
RAW="${{ github.event.inputs.version }}"
else
RAW="${{ github.event.workflow_run.head_branch }}"
fi
case "$RAW" in
refs/tags/*)
RAW_TAG="${RAW#refs/tags/}"
;;
*)
RAW_TAG="$RAW"
;;
esac
./scripts/helm_chart_version.sh "$RAW_TAG"
- name: Replace chart version and app version
run: |
set -eux
sed -i -E 's/^version:.*/version: "${{ steps.version.outputs.chart_version }}"/' helm/rustfs/Chart.yaml
sed -i -E 's/^appVersion:.*/appVersion: "${{ steps.version.outputs.app_version }}"/' helm/rustfs/Chart.yaml
- name: Set up Helm
uses: azure/setup-helm@v4.3.0
uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
- name: Test Helm Chart Templates
run: ./scripts/test_helm_templates.sh
- name: Package Helm Chart
run: |
set -eux
cp helm/README.md helm/rustfs/
package_version=$(echo $new_version | awk -F '-' '{print $2}' | awk -F '.' '{print $NF}')
helm package ./helm/rustfs --destination helm/rustfs/ --version "0.0.$package_version"
helm package ./helm/rustfs \
--destination helm/rustfs/ \
--version "${{ steps.version.outputs.chart_version }}"
- name: Upload helm package as artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: helm-package
path: helm/rustfs/*.tgz
retention-days: 1
publish-helm-package:
runs-on: ubicloud-standard-2
runs-on: ubuntu-latest
needs: [ build-helm-package ]
if: needs.build-helm-package.result == 'success'
steps:
- name: Checkout helm package repo
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
repository: rustfs/helm
token: ${{ secrets.RUSTFS_HELM_PACKAGE }}
- name: Download helm package
uses: actions/download-artifact@v7
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: helm-package
path: ./
- name: Set up helm
uses: azure/setup-helm@v4.3.0
uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
- name: Generate index
run: helm repo index . --url https://charts.rustfs.com
- name: Push helm package and index file
run: |
set -eux
git config --global user.name "${{ secrets.USERNAME }}"
git config --global user.email "${{ secrets.EMAIL_ADDRESS }}"
git status .
git add .
git commit -m "Update rustfs helm package with $new_version."
git commit -m "Update rustfs helm package with ${{ needs.build-helm-package.outputs.app_version }}." || echo "No changes to commit"
git push origin main
+3 -3
View File
@@ -25,12 +25,12 @@ permissions:
jobs:
build:
runs-on: ubicloud-standard-4
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
- uses: usthe/issues-translate-action@b41f55ddc81d7d54bd542a4f289fe28ec081898e # v2.7
with:
IS_MODIFY_TITLE: false
# not require, default false, . Decide whether to modify the issue title
# not require, default false. Decide whether to modify the issue title
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.
# not require. Customize the translation robot prefix message.
+92
View File
@@ -0,0 +1,92 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# MinIO on-disk interop: prove RustFS reads MinIO-written erasure-coded SSE
# objects with byte-identical data and correct logical size.
#
# The fixtures are real MinIO backend trees generated on the fly (they are
# gitignored, never committed), so the job regenerates them each run with
# Docker and then runs the `#[ignore]` reader tests in
# crates/ecstore/tests/minio_generated_read_test.rs. SSE/RIO changes run this
# as a path-filtered PR gate; nightly and manual runs retain broader coverage.
#
# Runner: GitHub-hosted `ubuntu-latest`. It reliably ships Docker + Python,
# unlike the self-hosted fleet, whose pods drift in Docker/pip availability
# (see the infra note in e2e-s3tests.yml).
name: minio-interop
on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/minio-interop.yml"
- "Cargo.lock"
- "Cargo.toml"
- "crates/ecstore/Cargo.toml"
- "crates/ecstore/src/io_support/rio.rs"
- "crates/ecstore/src/client/api_put_object_streaming.rs"
- "crates/ecstore/src/object_api/readers.rs"
- "crates/ecstore/src/set_disk/ops/object.rs"
- "crates/ecstore/src/sse/**"
- "crates/ecstore/src/store/object.rs"
- "crates/ecstore/tests/minio_generated_read_test.rs"
- "crates/kms/**"
- "crates/rio-v2/**"
- "rustfs/src/app/object_usecase.rs"
- "rustfs/Cargo.toml"
- "rustfs/src/storage/sse.rs"
schedule:
# Nightly at 03:17 UTC (offset from other nightly jobs).
- cron: "17 3 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
minio-interop:
name: MinIO interop (EC + SSE read parity)
# Skip on forks because this job depends on the repository's CI setup.
if: github.repository == 'rustfs/rustfs'
runs-on: ubuntu-latest
timeout-minutes: 40
env:
# Fixed 32-byte test KMS key baked into the fixture lab; not a secret.
RUSTFS_MINIO_STATIC_KMS_KEY: minio-default-key:IyqsU3kMFloCNup4BsZtf/rmfHVcTgznO2F25CkEH1g=
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: ci-minio-interop
github-token: ${{ secrets.GITHUB_TOKEN }}
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Generate real MinIO fixtures via Docker
run: bash crates/rio-v2/tests/minio_fixture_lab/capture_via_docker.sh all
- name: Generate real RustFS beta.5 KMS fixture
run: uv run python crates/rio-v2/tests/minio_fixture_lab/capture_rustfs_beta5.py
- name: Run MinIO interop reader tests
run: |
cargo nextest run --run-ignored ignored-only \
-p rustfs-ecstore --features rio-v2 \
-E 'binary(minio_generated_read_test)'
+269
View File
@@ -0,0 +1,269 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# MinIO Mint against RustFS: multi-SDK client compatibility.
#
# ceph/s3-tests (see e2e-s3tests.yml and the ci.yml PR gate) exercises the S3
# API semantics through boto3 only. Mint complements it by running the
# functional test suites of many REAL client SDKs and tools (awscli, mc,
# aws-sdk-java/go/php/ruby, minio-go/java/js/py, s3cmd, ...), which catches
# client-specific signing, encoding, and streaming edge cases a single client
# cannot.
#
# This job is REPORT-ONLY for test failures: RustFS does not pass every mint
# suite yet, so failures are inventoried in the job summary instead of turning
# the run red. The job fails only when mint produces no results at all
# (infrastructure error). Once a suite passes reliably, tightening this into a
# baseline gate (like implemented_tests.txt for s3-tests) is the natural next
# step. Tightening this into a per-suite baseline gate is tracked as ci-3
# (backlog#1149) and is intentionally NOT done here.
#
# Runner infrastructure (ci-2, backlog#1149): this job needs a working Docker
# daemon to (a) build the RustFS source image with buildx and (b) run both the
# RustFS server and the mint image. It previously ran on the self-hosted
# `sm-standard-4` label, which is served by heterogeneous runners; its single
# recorded run (28730530597, 2026-07-05) landed on a minimal pod with no
# `/var/run/docker.sock`, so "Enable buildx" died at `docker buildx create`
# ("failed to connect to the docker API at unix:///var/run/docker.sock") before
# any test executed -- the same root cause ci-1 fixed for the weekly s3-tests
# sweep. The fix is to pin to GitHub-hosted `ubuntu-latest`, which reliably
# ships a running Docker daemon + buildx + python3, instead of trusting the
# drifting self-hosted fleet. Tradeoff: the source Rust build is heavier on a
# GitHub-hosted 4-vCPU runner than on a warm self-hosted host, but it stays
# well inside the 120-minute budget and buys deterministic infra. The
# docker-capable self-hosted `dind-sm-standard-2` label was the alternative but
# has fewer cores and reintroduces fleet-state risk for no reliability gain.
name: mint
on:
workflow_dispatch:
inputs:
suites:
description: "Space-separated mint suites to run (empty = all, e.g. 'awscli mc minio-go')"
required: false
default: ""
mode:
description: "Mint mode: core (quick) or full"
required: true
type: choice
default: "core"
options:
- core
- full
mint-image:
description: "Mint image reference"
required: false
default: "minio/mint:edge"
schedule:
# Weekly, after the Sunday s3-tests full sweep (starts 02:00 UTC, up to
# 3h) has finished, so the two never contend for the same runner pool.
- cron: "0 6 * * 0"
env:
S3_ACCESS_KEY: rustfsadmin-ci
S3_SECRET_KEY: rustfssecret-ci
S3_REGION: us-east-1
RUST_LOG: info
PLATFORM: linux/amd64
BUILDX_CACHE_SCOPE: rustfs-e2e-s3tests-source
MINT_SUITES: ${{ github.event.inputs.suites || '' }}
MINT_MODE: ${{ github.event.inputs.mode || 'core' }}
# minio/mint:edge is a rolling tag, so an unpinned run is not reproducible and
# can go red purely from an upstream mint change (ci-2, backlog#1149). Pin the
# default to the linux/amd64 digest resolved on 2026-07-10. workflow_dispatch
# can still override via the `mint-image` input (e.g. to test a newer edge).
# Refresh: docker manifest inspect minio/mint:edge --verbose | grep digest
# (or, without a docker daemon:)
# TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:minio/mint:pull" | jq -r .token)
# curl -sI -H "Authorization: Bearer $TOKEN" \
# -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
# https://registry-1.docker.io/v2/minio/mint/manifests/edge | grep -i docker-content-digest
MINT_IMAGE: ${{ github.event.inputs.mint-image || 'minio/mint:edge@sha256:08a05e68893c68be2a83b6f79556853ed6aa3c6c9e64c823a00853e4e55d2200' }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Only alert-on-failure needs more than read access; it declares its own
# job-level `issues: write`.
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
mint:
# GitHub-hosted: reliably provides a running Docker daemon + buildx +
# python3. See the header note (ci-2) for why the self-hosted sm-standard-4
# label was abandoned (no docker.sock on the pod its only run landed on).
# TODO(ci-8): scheduled-failure alerting (auto-open issue on a red weekly
# run) is added by the ci-8 composite action; do not implement it here.
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Enable buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Build RustFS image (source, cached)
run: |
DOCKER_BUILDKIT=1 docker buildx build --load \
--platform "${PLATFORM}" \
--cache-from "type=gha,scope=${BUILDX_CACHE_SCOPE}" \
--cache-to "type=gha,mode=max,scope=${BUILDX_CACHE_SCOPE}" \
-t rustfs-ci \
-f Dockerfile.source .
- name: Start RustFS
run: |
docker network inspect rustfs-net >/dev/null 2>&1 || docker network create rustfs-net
docker rm -f rustfs-mint >/dev/null 2>&1 || true
# The four disks share one physical device on the runner (a single
# loopback filesystem), so the local physical-disk-independence guard
# would refuse to start. Bypass it — this is the CI use case the guard
# explicitly sanctions via RUSTFS_UNSAFE_BYPASS_DISK_CHECK.
docker run -d --name rustfs-mint \
--network rustfs-net \
-p 9000:9000 \
-e RUSTFS_ADDRESS=0.0.0.0:9000 \
-e RUSTFS_ACCESS_KEY="${S3_ACCESS_KEY}" \
-e RUSTFS_SECRET_KEY="${S3_SECRET_KEY}" \
-e RUSTFS_VOLUMES="/data/rustfs{0...3}" \
-e RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true \
-v /tmp/rustfs-mint:/data \
rustfs-ci
- name: Wait for RustFS ready
run: |
for _ in {1..60}; do
if curl -sf http://127.0.0.1:9000/health >/dev/null 2>&1; then
echo "RustFS is ready"
exit 0
fi
if [ "$(docker inspect -f '{{.State.Running}}' rustfs-mint 2>/dev/null)" != "true" ]; then
echo "RustFS container not running" >&2
docker logs rustfs-mint || true
exit 1
fi
sleep 2
done
echo "Health check timed out" >&2
docker logs rustfs-mint || true
exit 1
- name: Run mint
run: |
mkdir -p artifacts/mint-log
# Mint exits non-zero when any test fails; that is inventory here,
# not a gate (see header comment). Capture the exit code and let the
# summary step decide.
set +e
# shellcheck disable=SC2086 # MINT_SUITES is intentionally word-split
docker run --rm --network rustfs-net \
-e SERVER_ENDPOINT=rustfs-mint:9000 \
-e ACCESS_KEY=${S3_ACCESS_KEY} \
-e SECRET_KEY=${S3_SECRET_KEY} \
-e ENABLE_HTTPS=0 \
-e SERVER_REGION=${S3_REGION} \
-e MINT_MODE=${MINT_MODE} \
-v "${PWD}/artifacts/mint-log:/mint/log" \
"${MINT_IMAGE}" ${MINT_SUITES}
echo "MINT_RC=$?" >> "$GITHUB_ENV"
- name: Summarize results
run: |
python3 - <<'EOF'
import json, os, pathlib, sys
from collections import Counter
log = pathlib.Path("artifacts/mint-log/log.json")
if not log.is_file():
print("mint produced no log.json - infrastructure failure", file=sys.stderr)
sys.exit(int(os.environ.get("MINT_RC", "1")) or 1)
per_suite: dict[str, Counter] = {}
failures: list[tuple[str, str, str]] = []
for line in log.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
suite = entry.get("name", "unknown")
status = entry.get("status", "unknown").upper()
per_suite.setdefault(suite, Counter())[status] += 1
if status == "FAIL":
failures.append((suite, entry.get("function", ""), entry.get("error") or entry.get("message") or ""))
lines = ["# Mint results", "", "| Suite | Pass | Fail | N/A |", "|-------|------|------|-----|"]
for suite in sorted(per_suite):
c = per_suite[suite]
lines.append(f"| {suite} | {c.get('PASS', 0)} | {c.get('FAIL', 0)} | {c.get('NA', 0)} |")
lines.append("")
if failures:
lines.append(f"## Failures ({len(failures)})")
lines.append("")
for suite, func, err in failures:
first = err.strip().splitlines()[0] if err.strip() else ""
lines.append(f"- `{suite}` / `{func}` {first}")
lines.append("")
lines.append("_Report-only: failures are inventory, not a gate (see workflow header)._")
report = "\n".join(lines)
print(report)
summary = os.environ.get("GITHUB_STEP_SUMMARY")
if summary:
with open(summary, "a", encoding="utf-8") as fh:
fh.write(report + "\n")
EOF
- name: Collect RustFS logs
if: always()
run: |
mkdir -p artifacts/rustfs-mint
docker logs rustfs-mint > artifacts/rustfs-mint/rustfs.log 2>&1 || true
- name: Upload artifacts
if: always() && env.ACT != 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: mint
path: artifacts/**
alert-on-failure:
name: Alert on scheduled failure
needs: [mint]
# `always()` is required: without it this job is skipped when a needed
# job fails. Alerts only for scheduled runs (backlog#1149 ci-8) — manual
# dispatch failures are already watched by a human.
if: always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Open or update failure-tracking issue
uses: ./.github/actions/schedule-failure-issue
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+12 -7
View File
@@ -17,7 +17,7 @@ name: Update Nix Flake
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # Weekly on Sundays
- cron: '0 5 * * 0' # Weekly on Sunday 05:00 UTC (staggered after the midnight ci/build crons)
permissions:
contents: write
@@ -31,20 +31,25 @@ jobs:
update-flake:
name: Update flake.lock
runs-on: ubuntu-latest
timeout-minutes: 45
timeout-minutes: 90
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
uses: DeterminateSystems/determinate-nix-action@629b284231c2a82554b724e357e47fc6020833c8 # v3
- name: Cache Nix
uses: DeterminateSystems/flakehub-cache-action@1f9a51a2959d3e26c7838c6f3bf9f48acae525ea # v3.20.0
- name: Check Nix flake inputs
uses: DeterminateSystems/flake-checker-action@v12
uses: DeterminateSystems/flake-checker-action@3164002371bc90729c68af0e24d5aacf20d7c9f6 # v12
- name: Update flake.lock
id: update
uses: DeterminateSystems/update-flake-lock@main
uses: DeterminateSystems/update-flake-lock@fd9359ac79d0e912f1b4b947a48470b3e2799b56 # main
with:
git-author-name: houseme
git-author-email: housemecn@gmail.com
@@ -56,7 +61,7 @@ jobs:
nix
automated
commit-msg: "chore(deps): update flake.lock"
pr-reviewers: houseme, overtrue, majinghe
pr-reviewers: overtrue, majinghe
token: ${{ secrets.FLAKE_UPDATE_TOKEN }}
- name: Log PR details
+52 -13
View File
@@ -24,6 +24,7 @@ on:
- 'Cargo.lock'
- '.github/workflows/nix.yml'
pull_request:
types: [ opened, synchronize, reopened, closed ]
branches: [ "main" ]
paths:
- 'flake.nix'
@@ -34,43 +35,81 @@ on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
jobs:
cancel-closed-pr-runs:
name: Cancel Closed PR Runs
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Explain cancellation run
run: echo "PR closed; this run only cancels older runs in the same concurrency group."
nix-validation:
name: Nix Build & Check
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 45
timeout-minutes: 60
permissions:
contents: read
id-token: write
env:
NIX_CURL_FLAGS: -A cargo/stable
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install Nix
uses: cachix/install-nix-action@v31
uses: DeterminateSystems/determinate-nix-action@4eea0b33e3d1f02ecfe37cf16e7204c424009606 # v3.21.0
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
extra_nix_config: |
experimental-features = nix-command flakes
github-token: ${{ secrets.GITHUB_TOKEN }}
extra-conf: |
experimental-features = nix-command flakes configurable-impure-env
impure-env = NIX_CURL_FLAGS
max-jobs = 1
- name: Setup Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@v13
- name: Cache Nix
uses: DeterminateSystems/flakehub-cache-action@c01e819d047464c3edf6ba778f075952af5a3aa7 # v3.21.0
- name: Setup Flake Checker
uses: DeterminateSystems/flake-checker-action@v12
- name: Check Nix Flake Inputs
uses: DeterminateSystems/flake-checker-action@3164002371bc90729c68af0e24d5aacf20d7c9f6 # v12
with:
fail-mode: true
ignore-missing-flake-lock: false
- name: Verify Flake
run: |
echo "Checking flake structure and evaluation..."
nix flake show
nix flake check --print-build-logs
for attempt in 1 2 3; do
if nix flake check --print-build-logs --fallback --option fallback true; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "nix flake check failed after 3 attempts."
exit 1
fi
sleep $((attempt * 15))
done
- name: Build RustFS
run: |
echo "Building the default package..."
nix build .#default --print-build-logs
for attempt in 1 2 3; do
if nix build .#default --print-build-logs --fallback --option fallback true; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "nix build failed after 3 attempts."
exit 1
fi
sleep $((attempt * 15))
done
- name: Test Binary
run: |

Some files were not shown because too many files have changed in this diff Show More