Commit Graph

263 Commits

Author SHA1 Message Date
houseme 5b54c4303d fix(ecstore): reconcile object cleanup receipts (#6077)
* fix(s3): keep multipart completion publication owned

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

* fix(s3): keep put publication owned

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

* chore(app): route multipart context through facade

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

* fix(ecstore): gate object transaction fencing

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

* fix(ecstore): fence object transaction epochs

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

* fix(ecstore): reconcile old data cleanup receipts

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-13 18:08:50 +00:00
houseme f704d015d6 fix(copy): keep copy commit owner alive (#6070)
Keep S3 CopyObject's real outer owner task alive across caller cancellation so the source/destination bucket guards, same-key copy guard, storage commit, and post-commit publication hooks complete as one request-owned transaction boundary.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-13 20:19:33 +08:00
houseme d2b1003612 perf(storage): converge Wave 2 hot-path optimizations (#6065)
* perf(get): share inline shards and lock clients

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

* perf(ecstore): converge PUT encoding on contiguous blocks

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

* perf(get): cache codec streaming gate config

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

* fix(sse): redact projected customer headers

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

* perf(ecstore): collapse GET metadata snapshots

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

* perf(ecstore): reuse decode stripe scratch

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

* refactor(ecstore): trim decode scratch adapters

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

* test(ecstore): adapt transition checks to metadata snapshots

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

* perf(get): release metadata snapshots at ownership boundary

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

* refactor(ecstore): close cumulative fast-path findings

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

* fix(storage): preserve lock and header invariants

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

* test(ecstore): adapt cumulative paths after rebase

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

* fix(rio-v2): adapt generated metadata fixture

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-13 16:34:28 +08:00
cxymds e11fcfbd08 fix(rebalance): converge multipart data movement retries (#6057)
* fix(rebalance): converge multipart data movement retries

* fix(rebalance): harden multipart retry replacement

* fix(rebalance): isolate internal multipart uploads

* test(ecstore): adapt metadata mutation fixtures

* fix(rebalance): preserve transition metadata semantics

* refactor(ecstore): reuse internal metadata matcher

* Revert "refactor(ecstore): reuse internal metadata matcher"

This reverts commit c87ca0328f.

* refactor(rebalance): reuse data movement log constants

* fix(rebalance): isolate migration-owned state

* fix(rebalance): preserve pre-gate retry compatibility
2026-08-13 06:12:26 +00:00
houseme 11eecdc888 perf(put): avoid eager body zero fill (#6063)
Use BytesMut spare capacity for direct and pooled small PUT body reads while preserving exact-length validation.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-13 05:30:00 +00:00
Zhengchao An e4da9bd718 refactor(rustfs): move layer-neutral shared types out of server (#6061)
RemoteAddr, the DependencyReadiness family (DependencyReadiness, ReadinessDegradedReason, DependencyReadinessReport), and convert_ecstore_object_info (with its offset_date_time_to_timestamp helper) are consumed across app, infra, and interface layers but lived under server, so every lower-layer import was an upward app->interface or infra->interface edge the layer guard had to baseline.

They now live in a new layer-neutral rustfs/src/shared_types.rs (classified infra by the guard, making all consumer imports downward or lateral). server::readiness and server::event re-export for their own internals; the eight consumer sites (admin_usecase, bucket_usecase, object_usecase, cluster_snapshot, storage/access, storage/helper, plus the admin handler tests) import from the new home. Pure move: no type, impl, or behavior change.

The regenerated layer-dependency baseline shrinks by exactly eight lines with zero additions — the ratchet's intended direction. The two remaining readiness entries (collect/snapshot fn imports) need the collection machinery itself extracted from server and are left for the issue's PR5 scope.

Ref rustfs/backlog#1834 (PR4).
2026-08-13 12:29:49 +08:00
houseme 3a0dbccc2e perf(ecstore): reduce inline PUT commit overhead (#6033)
* perf(metrics): attribute PUT stage costs

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

* perf(ecstore): move PUT metadata during shuffle

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

* perf(s3): reuse PUT object lock state

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

* perf(ecstore): trim PUT metadata fanout clones

Build per-disk PUT metadata only for committed writer slots, move the response metadata out of the fanout vector, and preserve fresh FileInfo shuffle semantics.

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

* perf(metrics): make PUT stage attribution opt-in

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

* perf(ecstore): commit inline PUT shards directly

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

* perf(ecstore): streamline rename staging cleanup

Use the directory-specific removal operation for rename_data staging parents. This avoids a guaranteed failed file-removal probe on Unix-like hosts and lets Windows remove the empty directory directly while preserving best-effort non-empty handling.

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

* test(ecstore): cover inline PUT rename failures

Cache the detailed stage metrics gate once per PUT and exercise exact-quorum and quorum-minus-one failures after inline shard encoding.

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-13 02:04:20 +00:00
Zhengchao An 3c78a56ab0 test: un-ignore the remaining seven global-state tests, drop one stale premise (#6048) 2026-08-13 08:10:28 +08:00
Zhengchao An e313276e49 fix(sse): align copy-path unknown-algorithm fallback with put path (#6022) 2026-08-13 03:42:03 +08:00
houseme 73bd5d9d95 perf(get): reduce request entry allocations (#6029)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-12 19:07:59 +00:00
houseme 848b330825 perf(ecstore): reduce inline GET fixed costs (#5985) 2026-08-12 19:29:46 +08:00
houseme b00b7ab8f1 feat: add GET stream failure observability (#5967)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-12 03:32:17 +00:00
houseme ba6a0f25d9 perf(get): tune body stream buffers (#5959) 2026-08-11 20:48:03 +08:00
cxymds 2aa0148454 fix: report stalled object traffic as unready (#5936)
* fix: report stalled object traffic as unready

* fix: track fully received PUT storage progress
2026-08-11 10:55:22 +08:00
houseme 10a1d6b6e6 perf(get): avoid zeroing response body chunks (#5905)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-10 06:43:35 +08:00
唐小鸭 6333f21a2e feat(replication): SSE-C ciphertext passthrough replication (#5898)
Complete the encrypted-object replication series (backlog#1783, PR-C of
3, after #5872 and #5885): SSE-C objects replicate as ciphertext
passthrough — the source holds no customer key, so the stored bytes and
their encryption metadata travel verbatim and the replica decrypts only
with the original customer key, single-part and multipart.

- Sender: SSE-C objects read raw (raw_data_movement_read), transfer at
  ciphertext size, and range multipart parts over stored part sizes.
- Receiver: authorized replication PUTs restore the stored SSE-C keys
  from the transport headers (exact lowercase forms - the read-path
  check is case-sensitive), set ObjectOptions.preserve_ciphertext, and
  skip compression, bucket-default SSE, and sse_encryption behind one
  restore-derived gate. Multipart uses an internal session marker to
  store parts verbatim and strips it on complete.
- Convergence: the replication HEAD sends
  x-rustfs-source-replication-check; the target authorizes it as
  ReplicateObjectAction and skips SSE-C read validation for that
  request only, so keyless convergence HEADs see etag/size/mtime
  instead of 400 and SSE-C replicas stop re-driving forever.
- e2e: SSE-C contract flips to a key-gated readable replica (no-key and
  wrong-key GETs fail - the direct silent-plaintext detector); new
  multipart passthrough contract with ETag/marker/stability assertions.
2026-08-09 23:53:04 +08:00
houseme 08de165358 perf(get): reduce response body chunk overhead (#5897) 2026-08-09 22:36:39 +08:00
houseme a71726ef49 perf(get): reduce response write allocations (#5890)
Avoid cloning cache-served GET bodies, preserve downstream vectored writes through the GET close-detection wrapper, and remove per-stripe EC decode sidecar allocations.

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-08-09 08:58:41 +00:00
houseme 2c7d0fb2ce feat: add hotpath observability for S3 data paths (#5860) 2026-08-09 08:36:58 +00:00
cxymds 02b4e082e8 fix(get): pin resume reads to resolved version (#5859) 2026-08-09 07:48:51 +08:00
DIO a5c8052163 fix(s3): resume in-flight GET streams after rebalance relocation (#5791)
* fix(s3): resume in-flight GET streams after rebalance relocation

* fix(s3): address GET resume review findings

---------

Co-authored-by: zhengsf <zhengsf@kaopucloud.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <cxymds@gmail.com>
Co-authored-by: 马登山 <cxymds@qq.com>
2026-08-07 17:59:44 +08:00
anthonymartin 706a8b6061 fix(scanner): publish bounded observational usage (#5742)
* fix(scanner): publish bounded observational usage

* test(ci): serialize embedded integration ports

* test(cache): isolate generation-change timeout

* fix(scanner): address observational usage review

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

---------

Co-authored-by: Anthony Martin <949506+anthonymartin@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-08-07 08:52:34 +08:00
Zhengchao An efd5481b35 fix(auth): log structured denial reasons for generic AccessDenied responses (#5761) 2026-08-06 02:45:54 +00:00
唐小鸭 dbf51117a1 fix(replication): schedule replication for CopyObject and snowball extracted objects (#5753)
* test(replication): expect CopyObject and snowball extract to schedule replication

Red-phase TDD tests for P0-6: CopyObject never consults the bucket
replication config (no pending stamp, no schedule, and the destination
inherits the source's stale replication status metadata wholesale), and
snowball auto-extract members are never scheduled either.

- usecase white-box: observe MUST_REPLICATE_OBJECT_CALLS for
  execute_copy_object (currently 0, must be 1) and
  execute_put_object_extract (currently 0, must be 2 for a two-member
  archive), plus stale replication-status metadata cleanup assertions
  (MinIO filterReplicationStatusMetadata parity).
- e2e: CopyObject destination and snowball-extracted members must appear
  on the remote replication target and reach COMPLETED on the source.

Red evidence (before fix):
  copy_object_computes_replication_decision_and_strips_stale_status
    assertion failed: left: 0, right: 1
  put_object_extract_computes_replication_decision_per_entry
    assertion failed: left: 0, right: 2

* fix(replication): schedule replication for CopyObject and snowball extracted objects

CopyObject and snowball auto-extract never consulted the bucket
replication config: no PENDING stamp, no post-commit schedule, and no
scanner-heal backstop (heal only re-drives Pending/Failed objects, and
these objects carried no status at all). Worse, the copy path cloned the
source metadata wholesale, so a destination object inherited the
source's replication bookkeeping and could present a fake
COMPLETED/REPLICA state.

Mirroring the PUT path (single immutable decision drives both the
pending metadata and the post-commit schedule, rustfs/backlog#1320):

- execute_copy_object: strip the source's replication status metadata
  (internal replication/replica status + timestamps under both
  compatibility prefixes, plus x-amz-replication-status) for
  non-inbound requests — MinIO filterReplicationStatusMetadata parity;
  the cleanup runs before the decision so an inherited REPLICA status
  cannot suppress it. Then compute must_replicate_object once, stamp
  PENDING when it replicates, and schedule after the copy commits and
  the self-copy lock guard is released. Inbound replica writes keep
  their authorized metadata and are declined inside
  must_replicate_object, so replicas are never re-scheduled outbound.

- execute_put_object_extract: same stamp + schedule per extracted
  member object (MinIO PutObjectExtract parity).

- execute_put_object dispatch: an authorized inbound replication PUT is
  stored verbatim instead of being re-dispatched into the extract path.
  Extracted members keep x-amz-meta-snowball-auto-extract in their user
  metadata and the replication client replays stored metadata as
  headers, so the target used to try to untar each member's own bytes,
  permanently failing replication for non-archive members (surfaced by
  the new snowball e2e test).

Green evidence:
- copy_object_computes_replication_decision_and_strips_stale_status,
  put_object_extract_computes_replication_decision_per_entry (red: 0
  decisions; green: 1 and 2), plus the existing PUT/object-lock
  decision-count tests stay green.
- e2e test_copy_object_replicates_to_target and
  test_snowball_extract_replicates_members_to_target pass against two
  live instances.
2026-08-06 08:46:39 +08:00
Zhengchao An 98d3619613 fix: address rc.1 release blockers (#5648)
* fix: address rc.1 release blockers

* fix: route release guards through architecture boundaries

* fix: close remaining rc.1 regression gaps

* refactor: group multipart listing options

* fix: resolve rc.1 CI regressions

* fix(ecstore): keep bucket-config writes off the caller's stack

A bucket-config write nests incarnation resolution (which can drive legacy
migration and a peer fan-out), a full metadata load, and `save` — itself an
object PUT that pulls in the whole erasure write path. Every request that
mutates bucket config is already several futures deep, so inlining all of
that into one state machine overflows the 2MiB worker stack in debug builds.

Two CI lanes aborted with SIGABRT on this:

  ILM Integration (serial)
    rustfs app::lifecycle_transition_api_test::
      compensation_driven_complete_multipart_upload_still_transitions
  Test and Lint (swift)
    rustfs-protocols::swift_metadata_persistence::
      swift_metadata_writes_are_durable

Neither test file is touched by this branch and both lanes are green on
main. Stack-pointer probing showed ~780KiB consumed between
`metadata_sys::update` and the config read alone, with single hops of
363KiB (`update` -> `acquire_config_write_guard_for_incarnation`), 125KiB
and 105KiB.

Box the deep sub-futures on both read-modify-write paths (`update` /
`update_checked` and `update_config_with` / `update_config_with_checked`)
so each guard's own state machine stays small. Behaviour is unchanged;
`update` -> guard drops to 253KiB and both tests pass on the default stack.

* fix(lifecycle): unbreak restore under the bucket generation fence

The ILM lane aborted on a stack overflow before reaching these, so they
were never reported; with that fixed, four restore tests fail. All four
are green on main and none of their test files are touched by this branch.

1. RestoreObject and ListMultipartUploads hard-required
   `opts.expected_bucket_incarnation_id`, but `apply_bucket_generation_guard`
   deliberately leaves it unset when no guard extension is present — only the
   S3 access layer installs one. Every direct caller therefore got
   `InternalError: ... bucket generation guard is missing`. Resolve the
   current generation instead, the way the copy path already does. The fence
   is unaffected: RestoreObject still re-reads the incarnation from disk and
   compares before admitting the restore, and the multipart listing is
   filtered by the value it resolves.

2. `restore_expiry_snapshot_matches` (new on this branch) rejected every
   restored-copy expiry whose `restore_expires` had not already elapsed.
   Whether the restored copy is due to expire is the ILM evaluator's
   decision, made when it emitted DeleteRestoredAction; re-deriving it in
   the set layer only adds a way for a legitimate action to be rejected.
   The stale-event risk it appears to guard is already covered by the
   surrounding snapshot match — a re-restore rewrites `restore_expires`,
   so a replayed event fails the equality check. Drop the clause; the
   fifteen identity clauses are unchanged.

Fixed:
  rustfs app::lifecycle_transition_api_test::
    restore_object_usecase_accepts_exactly_one_of_two_concurrent_restores
    restore_object_usecase_completes_suspended_null_version_in_place
    restore_object_usecase_reports_ongoing_conflict
  rustfs-scanner::lifecycle_integration_test serial_tests::
    test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore

Verification: the CI ILM lane filter now runs 53/53 green locally.

* chore: address review follow-ups on this branch

Four items from the adversarial review that were still open.

- Restore the assertion `test_bucket_replication_replayed_delete_marker_
  preserves_source_mtime_without_source_restart` is named for. The branch
  had replaced the backlog#867 mtime check with `assert_replication_
  converged`, which any successful replication satisfies, and deleted the
  two helpers it needed — so the regression the test exists to catch would
  now pass. This matters here specifically because the branch changes the
  flag feeding `replication_delete_remove_options` and routes replay
  through a new file and ordering.

- Drop `read_config_no_lock_preserve_empty`: zero production callers (the
  one real consumer calls the `_with_metadata` variant directly). Its test
  stanza now exercises that variant, so the coverage moves to live code
  rather than being deleted.

- Revert the `bytesize` bump. It is a no-op: `Cargo.lock` already pinned
  2.7.0 before this branch and is untouched, so the caret range already
  resolved there. Nothing in the diff uses the crate.

- Split the AGENTS.md "Adversarial Validation" policy change out of this
  branch. The edit is defensible on its own, but it relaxes the review gate
  that this branch has to pass, so it should land as its own PR reviewed on
  its own merits rather than bundled with the change that benefits from it.
  The reverted hunks are unchanged and ready to re-apply.

Not changed, deliberately: the missing-sidecar path still fails closed.
`missing_bucket_incarnation_sidecar_for_new_metadata_fails_closed` pins
that on purpose, and serving a non-authoritative Object Lock state would
be the wrong trade. The residual concern stands and is recorded in review
— a crash between the two writes in `persist_new_and_set` leaves the
bucket unloadable until DeleteBucket+CreateBucket, and the repair branches
in `migrate_legacy_metadata` and `make_bucket` are unreachable dead code
for that case. Resolving it needs the read path and the (transaction-lock
holding) repair path to be separated, which is more than a follow-up edit.

* test(ci): serialize the new bucket-incarnation tests

The five tests this branch adds around the incarnation / lifecycle fence
drive `init_bucket_metadata_sys` and `bucket_metadata_sys_of` — process-global
OnceLock state that `serial_test`'s `#[serial]` cannot protect across
nextest's process boundary — and they delete+recreate buckets, the shape that
raced into InsufficientWriteQuorum in backlog#937.

Add them to the `ecstore-serial-flaky` group in both the default and ci
profiles (nextest evaluates a named profile's own overrides list, so the
ci mirror is required). Preventive serialization only, no retries.

Not a full fix for the review comment: `bucket_delete_waits_for_config_
mutation_fence` still proves liveness with a fixed 200ms sleep plus
`assert!(!delete.is_finished())`. Turning that into readiness polling needs
a production-side signal to wait on — asserting "still blocked" is inherently
a negative. Serializing the group removes the parallel-load pressure that
makes the window fragile; the sleep itself is left for a follow-up.

* test(ecstore): pin that a drained bucket is actually deletable

`DeleteBucket`'s emptiness check is `has_xlmeta_files`, a raw scan of the
bucket directory on local disks — not an S3-level listing. So "the client
drained the bucket" and "the bucket is deletable" are two different
contracts, and only the first one was covered.

That gap is what the `S3 Implemented Tests` lane is failing on: 219 cases,
all `BucketNotEmpty` on `nuke_prefixed_buckets`, with every test body
passing. The first one is `test_versioning_obj_suspend_versions`, reported
by pytest as PASSED followed by ERROR at teardown.

Add the missing assertion for the unversioned path: PUT, client DELETE,
then assert no `xl.meta` survives and `DeleteBucket` succeeds. It passes —
which is itself a result: the plain delete path leaves no residue, so the
s3-tests failure is not there.

The versioning-suspended path is the remaining suspect (the client DELETE
leaves a null delete marker, and draining means purging it by
`versionId=null`). It is not covered here: `BucketVersioningSys` resolves
through the ambient `get_bucket_metadata_sys()` OnceLock, which this unit
env cannot set, so the bucket never actually reports as suspended. That
repro belongs at the e2e layer where a real server owns the versioning
state.

* fix(ecstore): let an explicit null-version delete purge its delete marker

Root cause of the `S3 Implemented Tests` lane: 219 cases, all
`BucketNotEmpty` on `nuke_prefixed_buckets`, every test body passing.

On a versioning-suspended bucket a client DELETE leaves a null delete
marker — correct S3 semantics, and an `xl.meta` on disk. Draining the
bucket therefore means purging that marker as `?versionId=null`, which is
what `nuke_bucket` does before `DeleteBucket`. That purge was rejected:

    explicit null-version purge of the null delete marker must succeed,
    got [Some(MethodNotAllowed)]

so the marker survived, and `DeleteBucket`'s emptiness check — a raw
`has_xlmeta_files` scan of the bucket directory, not an S3 listing — kept
reporting the bucket as non-empty.

The two sides of the version comparison in the batch delete loop are in
different namespaces. `goi.version_id` is the client-facing identity, where
`from_file_info` synthesizes `Some(Uuid::nil())` for a null version on a
versioned *or versioning-suspended* bucket. `version_id` is the storage
identity, where `delete_file_info_version_id` maps an explicit
`?versionId=null` to `None`. Comparing them raw makes the purge look like a
version mismatch, so `explicit_delete_marker` is false and the
`MethodNotAllowed` from the lookup is recorded as a delete failure.

This only became reachable on this branch: previously `check_opts` did not
carry `dobj.version_id`, so `set_disk_delete_creates_delete_marker` was
true, `object_lock_check_required` was false, and the lookup that produces
`MethodNotAllowed` never ran. Adding the version id to `check_opts` lit up
a comparison that was already wrong.

Normalize both sides through `delete_file_info_version_id`.

The regression test injects a real Suspended bucket-config snapshot — the
delete path reads versioned/suspended from that snapshot, not from `opts`,
so without it `from_file_info` never synthesizes the null version id and
the branch is not reached. Mutation-checked: restoring the raw comparison
fails the test with the exact `MethodNotAllowed` above.

* fix(app): drop the now-needless struct update

Reverting `crates/replication` to main removed the extra `MrfReplicateEntry`
fields, so this literal specifies every field again and `..Default::default()`
trips `clippy::needless_update` under `-D warnings`.

Caught by CI, not locally: I had run `cargo check --workspace --all-targets`,
which does not see clippy-only lints. Ran `cargo clippy --workspace
--all-targets -- -D warnings` here — clean.

* test(e2e): assert the fresh-volume classification

four_node_empty_legacy_volumes_start_as_fresh only started the cluster and
listed buckets — no assertion, so any classification path that still permits
startup left it green without proving the pre-created empty `.minio.sys`
directories were treated as fresh volumes.

Pin what that classification actually leaves behind: no buckets adopted into
the namespace, `.rustfs.sys/format.json` written on every drive, and the empty
legacy directory left untouched rather than migrated into.

* fix(bucket): apply the requested Object Lock to existing buckets

Site replication replays make-with-versioning against the destination,
carrying the source's `lockEnabled`. When the destination bucket already
exists it takes `force_create`, and the whole option-application block was
gated on `confirmed_missing` — so the call returned success while the replica
stayed unlocked. Replicated versions could then be deleted without the
retention the source enforces.

Object Lock enable is one-way, so applying it to an existing bucket is safe:
move it out of the creation-only gate, keeping `created` and versioning-only
options creation-scoped as before.

An existing authoritative bucket takes the `cache_bucket_metadata_in` branch,
which only caches, so the enable would have been dropped on restart. Persist
instead when the enable actually changed something.

Mutation-checked: restoring the creation-only gate fails the new
`force_create_enables_object_lock_on_an_existing_bucket` with "Object Lock
must be enabled on the existing bucket".

cargo nextest run -p rustfs-ecstore --lib: 3633 passed.

* fix(ecstore): box the generation-checked config mutation paths too

The earlier stack fix boxed `update` and `delete`, but an authorized
bucket-config mutation carrying an incarnation takes `update_if_incarnation`
/ `delete_if_incarnation` instead — which were still inlining the whole
resolve/load/save chain into an already-deep request future. Same overflow,
sibling path.

* fix(restore): keep the nil-version normalization the strip removed

Reverting the replication subsystem to main took `set_disk/replication.rs`
with it, but one line in that file was this branch's own fix rather than
replication work:

    -  self.version_id.filter(|v| !v.is_nil()) == fi.version_id.filter(|v| !v.is_nil())
    +  self.version_id == fi.version_id

For a versioning-suspended object the expected version is `Some(Uuid::nil())`
while the read-back `FileInfo` carries `None`, so the raw compare reports
every suspended restore as "restored object changed before restore metadata
finalization" and the copy-back never commits. Same nil-vs-None mismatch as
the null delete-marker purge fixed earlier on this branch.

Caught by `Test and Lint (rio-v2)`, not by my local runs: the test lives in
`transition_commit_failure_tests`, gated behind `feature = "test-util"`, so
the 3633-test suite I had been running never included it. Re-ran with
`--features rio-v2,test-util`: 3722 passed.
2026-08-03 19:25:43 +00:00
cxymds 380ec74ece fix(replication): persist force-delete handoff state (#5641)
* fix(replication): persist force-delete handoff state

* fix(arch): route force-delete config access through boundary

* style: format force-delete imports

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
2026-08-03 01:44:28 +00:00
cxymds 2ce670837c fix(ecstore): make transitioned deletes durable (#5644)
* fix(ecstore): make transitioned deletes durable

* fix(ecstore): journal force deletes

* fix(ecstore): journal force deletes
2026-08-02 18:00:11 +00:00
cxymds ec67884f8d fix(replication): preserve durable MRF delete admission (#5643) 2026-08-02 23:53:53 +08:00
cxymds 2cc7443067 feat(replication): bound DeleteObjects queue admission (#5637)
feat(replication): batch DeleteObjects queue admission
2026-08-02 21:06:54 +08:00
cxymds 2cdba03dee fix(authz): gate replication-only PUT headers (#5625) 2026-08-02 19:28:54 +08:00
Zhengchao An b1ddda3bb2 fix(sse): rewrite data when a same-key copy changes encryption (#5618)
A same-name CopyObject marks the operation `metadata_only`, which lets the
store layer rewrite `xl.meta` in place and leave the data blocks untouched.
The handler independently strips the source encryption metadata and calls
`sse_encryption`, which mints a *fresh* DEK. On an unversioned bucket both
happen at once, so the object ends up with a new DEK sitting beside ciphertext
sealed under the old one, and can never be decrypted again.

The mirror case is silent: an encrypted source copied without any destination
SSE keeps its ciphertext while losing the key metadata, so GET returns raw
ciphertext as if it were plaintext, with HTTP 200 and no error anywhere.

Keep `metadata_only` off whenever either side of the copy is encrypted, so the
store layer performs a full read/write rewrite through `put_object`. This is
the same resolution the versioned historical-restore path already uses for
this risk (issue #4238), and it matches MinIO's
`isSourceEncrypted || isTargetEncrypted -> metadataOnly = false` guard in
CopyObjectHandler.

The target half of the predicate deliberately tests `effective_sse` rather
than the request headers MinIO inspects: `effective_sse` also resolves the
bucket default-encryption rule, and `sse_encryption` mints a DEK from that
resolved value. A header-only check would miss a same-key copy performed under
a bucket default rule. The source half reuses `ObjectInfo::is_encrypted` so a
future encryption flavour is covered here as soon as it is recognised there.

Versioned buckets were already safe: that path falls through to `put_object`
regardless of `metadata_only`. RestoreObject also sets `metadata_only` but
only appends restore keys and never re-derives a DEK, so it is unaffected.
2026-08-02 11:00:52 +00:00
cxymds 7463655ae1 fix(authz): preserve direct delete usecase callers (#5614)
* fix(authz): preserve direct delete usecase callers

* test(ilm): provide owner context for forced deletes
2026-08-02 17:01:32 +08:00
cxymds f00f5fe776 fix(authz): restrict recursive force-delete scope (#5610) 2026-08-02 06:30:36 +00:00
cxymds c1955a8498 fix(replication): harden live delete admission (#5599) 2026-08-02 12:52:11 +08:00
唐小鸭 c8016cbcdb fix(replication): enforce bucket replication switches (#5449)
* fix(replication): enforce bucket replication switches

* fix(replication): satisfy delete admission clippy lint

* fix(replication): restore MinIO tag filter behavior

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
Co-authored-by: cxymds <cxymds@gmail.com>
2026-08-01 15:03:57 +00:00
Zhengchao An 782c78e0ef feat(sse): enforce per-key KMS authorization on the SSE-KMS data path (#5538) 2026-08-01 05:25:18 +00:00
houseme 04c5921850 fix(s3): allow CopyObject COPY request metadata (#5514)
* fix(s3): allow CopyObject COPY request metadata

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

* fix(kms): remove stale KmsClient import

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

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-31 17:52:00 +00:00
GatewayJ 2dea4a9acf fix(s3): correlate server-owned request IDs (#5433)
* fix(s3): correlate server-owned request IDs

* fix(server): preserve trace context and Swift routing

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-29 23:50:59 +08:00
houseme f7c1b13c0f refactor(deps): replace md5 crate with md-5 (#5432)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-29 11:29:37 +00:00
cxymds b65210b1db fix(ilm): preserve restore source version mode (#5406)
* fix(ilm): preserve restore source version mode

* test(ilm): cover suspended null-version restore

* fix(ilm): reconcile worker results from journal
2026-07-29 16:16:40 +08:00
cxymds 294c79c156 fix(tiering): gate remote version state safely (#5374)
* feat(tiering): model provider version capabilities

* feat(tiering): persist opaque remote versions

* fix(tiering): gate remote version state safely

* fix(tiering): preserve remote version state on delete

* fix(tiering): accept unversioned transition responses

* fix(tiering): replay exact cleanup journals

* test(tiering): pin empty exact cleanup guard

* test(tiering): accept strict missing journal errors

* test(tiering): exercise free-version identity guard

* test(tiering): reach destination identity guard

* test(tiering): persist version identity drift

* test(tiering): bind version drift fixture

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-29 02:43:28 +00:00
cxymds 6bc5fc77b5 fix(notify): emit noop event for missing deletes (#5381) 2026-07-28 17:05:10 +08:00
Henry Guo d5df66ac4f fix(scanner): require authoritative usage snapshots (#5333) 2026-07-28 07:29:57 +08:00
Zhengchao An 5a768d3a44 perf(s3): resolve bucket versioning once per DeleteObjects request (#5340)
DeleteObjects rebuilt ObjectOptions inside its per-key phase-1 loop, and
del_opts re-fetched the bucket versioning configuration on every call: a
1000-key request paid 1001 identical metadata-sys lookups, each taking the
global bucket-metadata read lock and cloning a VersioningConfiguration.

Split del_opts into the existing async entry point plus a synchronous
del_opts_with_versioning that takes an already-resolved configuration, and
reuse the request-level version_cfg the handler had already fetched.

This also removes a snapshot inconsistency. The skip-stat decision
(can_skip_delete_objects_pre_stat) and the post-delete accounting already
derived from the request-level snapshot while del_opts re-read the config
per key, so a PutBucketVersioning committing mid-request could give a key
opts.versioned=true while the batch delete still ran with versioned=false:
the object was deleted outright with replication state attached to a delete
marker that was never created, and the advisory object-lock pre-check was
skipped on a false premise. All three now derive from one snapshot.

GET, HEAD and DeleteObject additionally establish bucket existence before
building ObjectOptions, so a request naming a nonexistent bucket no longer
performs bucket-metadata work first. GET keeps its cheap request-shape
validations (key, range, partNumber) ahead of the existence check so
InvalidArgument still wins over NoSuchBucket for malformed requests.

Bucket validation moves to validate_bucket_exists, which takes an explicit
store and shares the existing 5s-TTL cache. GET, PUT, HEAD and DeleteObject
now resolve the store through the request-bound server context
(backlog#1052 S6) instead of the process-global handle: get_validated_store
resolves the first-published global AppContext, so in an embedded
multi-instance process a request to the second server operated on the first
server's store. Remaining global-handle call sites in bucket_usecase and
select_object are left for a follow-up.
2026-07-28 06:33:17 +08:00
Zhengchao An c984bc7251 fix(s3): return Cache-Control on GET object (#5241) 2026-07-26 04:12:11 +08:00
cxymds f52dde87d1 fix(object): make version undo preconditions atomic (#5225)
* fix(object): make version undo preconditions atomic

* fix(object): fence metadata-only undo copies

* fix(object): order versions by commit time
2026-07-25 19:26:50 +08:00
houseme 2dc4d0b651 refactor(getobject): scope downstream close tagging (#5224)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-25 09:36:19 +00:00
houseme 9ddb30139d fix(getobject): distinguish downstream output closure (#5220)
fix(getobject): preserve internal broken pipe failures

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-25 08:22:20 +00:00
houseme 44b1916103 fix(getobject): enrich stream error context (#5201)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-24 14:47:56 +00:00
cxymds f46ea6e14f fix(s3): enforce SSE-C copy key validation (#5185) 2026-07-24 19:11:36 +08:00