Files
rustfs/docs/architecture/s3-compatibility-matrix.md
T
唐小鸭 46907c05cf fix(replication): close the GA blocker set from backlog#2366 (#7503)
* fix(replication): close GA blockers from backlog#2366

Implements the P1 set from the pre-GA replication audit:

- Replication rule tag filters now require every And.Tag to match, replacing
  the s3s OR semantics with a local AND matcher that fails closed on a
  malformed tag.
- A replicated group membership change no longer writes the group status, so
  a membership update carrying the default Enabled status cannot silently
  re-enable a disabled group on the peer.
- A successful IAM import schedules one collapsed full-IAM snapshot per remote
  peer instead of leaving the imported entities local-only.
- A pending endpoint refresh is redriven by the heavyweight reconcile tick,
  carries its own ilm-expiry override, and no longer blocks a remove that
  drops every unacknowledged peer.
- Site metrics expose local replication failure totals and rolling windows;
  node-level counters no longer report a constructed zero.
- set/remove-remote-target notify peer metadata caches before returning, so a
  follow-up put-bucket-replication on another node sees the target.
- Adds the site-replication operations runbook, a docs index, a replication
  support boundary section, and the Replication changelog section.

* fix(site-replication): resume only a locally driven endpoint refresh

The peer-side edit handler journals a pending endpoint refresh with an empty
`remote_peers` map and commits it inside the same request through
`apply_internal_peer_edit`. The reconcile tick could not tell that journal
from the coordinator's own: with no required peers it reads as complete on
sight, so the tick committed it with `edit_state` - losing the local-name
sync - and cleared it under the request that owned it, whose commit then
reported the refresh as changed and denied the coordinator the peer
acknowledgement it was waiting for.

Resume now runs only for a journal that carries the fan-out topology. A
receiver's journal stays for the coordinator to redrive with the same
refresh id, which is the path that already recovers it.

* fix(site-replication): keep an explicit disabled group status on a snapshot

Skipping the group-status write whenever an item carries members stopped a
membership change from re-enabling a disabled group, but it also silenced the
full-IAM snapshot, which always sends members together with the sender's real
status. A peer that did not have the group yet created it through
`GroupInfo::new` - enabled - so a bootstrap, a repair, or the snapshot an IAM
import now schedules handed every member of a frozen group live access there.

The madmin wire maps an unset `groupStatus` to Enabled, so only Enabled can be
a default. Disabled is always explicit and is applied again.

* fix(site-replication): schedule the import snapshot without recording a failure

`import-iam` reused the failure-recording path to queue its full-IAM
snapshot. That raises `retry_count` on every call, so three imports - the
normal shape of a bulk migration done one archive at a time - escalated a
healthy peer to `retryStats.failed` with the scheduling note shown as
`lastError`, which is exactly the signal the runbook tells operators to
repair. A full retry queue also turned a completed import into a 503.

Scheduling now only ensures the collapsed entry exists, and a failure to
schedule is logged instead of failing the request: the entities are already
imported and the reconcile pass still closes the gap.

* fix(admin): stop reporting replication failures as retries

`retries` is the minio-go counter for redeliveries, and mc prints it as such.
Filling it with the failure count claimed a redelivery that never happens: a
failed object is not retried by an event today, it waits for the scanner heal
pass. `errors` keeps the failure counters; `retries` stays zero until there is
a real redelivery to count, and the runbook now says so.

* perf(site-replication): aggregate failure windows without cloning bucket stats

`site_metrics_snapshot` went through `get_all`, which clones every bucket's
stats, and then scanned each target's sample deque twice. That deque is
bounded only by the one-hour window, so an unreachable target under load -
the case an operator polls this endpoint for - made every
`mc admin replicate status` copy the whole backlog and hold the read lock
against the failure path while doing it.

It now folds under the read lock and takes both windows in one walk. The
`max` against the serialized `last_minute` / `last_hour` snapshots is dropped:
those are stamped onto per-bucket clones elsewhere and are always zero in this
node-local cache.

* fix(site-replication): reject a conflicting ilm-expiry override on a re-run

The commit now reads the ilm-expiry override back out of the pending refresh
journal, so a second edit that asks for a different value had it dropped while
the request still reported success. Re-running without the flag keeps pinning
the recorded value - that is the documented way to redrive a stuck refresh -
but an explicit different value is now rejected instead of ignored.

* fix(admin): do not fail a remote-target write on a peer reload error

set/remove-remote-target propagated the peer metadata reload error, so a
target that was already persisted and live on this node reported a 5xx to the
client whenever one peer could not be reached. Every S3 bucket-config write
path treats that reload as best effort and only warns; these two admin
handlers now do the same, and the reason is logged with the bucket and action.

* fix(site-replication): undo every bucket a cut-short refresh rewrote

When a remove accepted on another node clears the refresh journal mid-pass,
only the bucket holding the lock at that moment had its restored target
undone. The buckets rewritten earlier in the same pass kept a target pointing
at the removed peer whenever the remove's own cleanup had already walked past
them. The undo now covers every bucket this pass rewrote, attempting all of
them so one failure does not strand the rest.

* fix(site-replication): keep replay running while an endpoint refresh is pending

A pending endpoint refresh took the whole heavyweight pass with it, so a peer
that never came back froze IAM and bucket replay to every healthy peer too -
the stall this journal's resume path was meant to end. The refresh arm now
drains the retry queue before returning; it replays per-peer deliveries
against the endpoints currently committed in state, so it is unaffected by the
edit in flight. Bucket wiring reconciliation still waits, because it rewrites
the very targets the refresh is changing, and the runbook now says so.

* test(e2e): cover the AND semantics of a two-tag replication filter

The acceptance matrix only had a single-tag rule, which matches under both AND
and OR semantics and therefore proved nothing about the filter this fix
changed. It now also carries a two-tag `And` rule - the shape
`mc replicate add --tags "k1=v1&k2=v2"` writes - and asserts that an object
with one of the two tags is not admitted while an object with both is.

No new test function, so the nightly selection digest is unchanged.

* refactor(site-replication): fold the refresh state-change error into one constructor

The endpoint-refresh work added three `s3_error!` invocation lines, which the
s3s footprint ratchet is meant to prevent. Five copies of the same
concurrent-change error now share one constructor, so the surface nets one
line smaller than main; the baseline is retightened to match.

* fix(site-replication): report a peer whose IAM snapshot waits for a repair

An escalated snapshot entry records a deletion a snapshot cannot replay, so
only a repair settles it and the marker must survive. Scheduling an import
snapshot therefore leaves that peer's entry alone - and now says so, instead
of returning success while nothing was scheduled for it.

* docs(operations): state the group-status and escalation convergence limits

Two boundaries the fixes in this branch make load-bearing: a membership change
never carries an enable, so a group disabled on one site only has to be
re-enabled there explicitly; and a peer holding an escalated IAM entry does
not receive a scheduled snapshot, including the one a bulk import schedules,
until a repair settles it.
2026-09-08 14:58:41 +00:00

8.1 KiB

S3 Compatibility Matrix

Use this when: writing or checking a user-facing S3 compatibility claim, or moving a Ceph s3tests case between lists. Source of truth: the test lists under scripts/s3-tests/ and the runner scripts/s3-tests/run.sh; counts are derived from those files and are not recorded here.

Current Claim

RustFS provides broad S3 API compatibility for supported features. It does not claim complete coverage of every standard or vendor-specific S3 behavior. The root README uses the same wording: supported S3-compatible clients and features are covered by the compatibility matrix and test lists.

Test List Sources

List Purpose Source
Implemented tests Standard S3 tests expected to pass; the default local s3tests run. scripts/s3-tests/implemented_tests.txt
Lifecycle behavior tests Days-based expiration cases gated by the s3-lifecycle-behavior-tests lane in .github/workflows/ci.yml. scripts/s3-tests/lifecycle_behavior_tests.txt
Unimplemented tests Standard S3 features not yet passing. scripts/s3-tests/unimplemented_tests.txt
Excluded tests Vendor-specific or intentionally unsupported behavior excluded from RustFS gating. scripts/s3-tests/excluded_tests.txt

Counts ignore blank lines and comments; compute them from the files. The lifecycle lane runs separately because its cases need RUSTFS_ILM_DEBUG_DAY_SECS and an enabled scanner, and a global debug day would also shrink the x-amz-expiration header asserted by test_lifecycle_expiration_header_*; see IMPLEMENTED_TESTS_FILE in scripts/s3-tests/run.sh.

Supported Coverage

Area Status Evidence
Bucket create/delete/list/head Supported implemented_tests.txt
Object put/get/delete/copy/head Supported implemented_tests.txt
CopyObject checksums (CRC32, CRC32C, CRC64NVME, SHA1, SHA256, MD5, SHA512, XXHASH3, XXHASH64, XXHASH128), including source preservation and explicit override Supported crates/e2e_test/src/copy_object_checksum_test.rs
ListObjects/ListObjectsV2 prefix, delimiter, marker, max-keys Supported implemented_tests.txt
Multipart upload create/upload/complete/abort and selected multipart copy/checksum/object-attribute behavior Supported implemented_tests.txt
Bucket and object tagging Supported implemented_tests.txt
Bucket policy put/get/delete Supported implemented_tests.txt
Public access block put/get/delete Supported implemented_tests.txt
Presigned GET and PUT URLs Supported implemented_tests.txt
Range and conditional reads Supported implemented_tests.txt
User metadata Supported implemented_tests.txt
SSE-C and selected SSE-KMS edge cases Supported implemented_tests.txt
Selected versioning, object-lock, checksum, CORS, raw request, and conditional write behavior Supported implemented_tests.txt

"Supported" for the SSE row means RustFS encrypts and decrypts its own objects. MinIO SSE objects (SSE-S3, SSE-KMS, SSE-C) are not readable in default builds; see minio-file-format-compat.md Part C for the rio-v2 migration build.

Replication Support Boundary

Site replication and bucket replication are not the same compatibility claim. Site replication requires RustFS-compatible peer admin APIs and coordinates IAM, topology, buckets, and metadata. A generic S3-compatible service can only be a bucket-replication data target.

For a generic S3 target, RustFS supports object PUT/HEAD/DELETE, multipart uploads, tags, version deletes, and Object Lock mutations when the target implements the corresponding S3 APIs and has versioning enabled. Targets that mint their own version IDs are supported through a per-target version ledger; pre-ledger replicas are adopted only when exact key and ETag identify one unambiguous target version. NoSuchVersion for an already absent addressed replica is treated as converged.

The following are capability boundaries, not universal S3 claims:

  • GET /BUCKET?replication-check must pass the phases required by the intended workload. VersionFidelity may report a minting target as mismatched even though ledger-addressed delete and Object Lock phases succeed.
  • A target that rejects standard multipart constraints, required Object Lock integrity headers, or the configured checksum framing is unsupported until its transport settings are made compatible.
  • SSE-S3 and SSE-KMS are decrypted at the source and re-encrypted by the destination's KMS. SSE-C uses ciphertext passthrough and requires target evidence. Unsupported or ambiguous encryption metadata fails closed.
  • ACL authorization is intentionally unsupported, and generic targets never receive RustFS IAM/site-control-plane state.
  • RustFS does not guess between multiple target versions with the same key and ETag. The mutation remains failed and retryable until repair establishes an unambiguous mapping.

See site replication operations for health, recovery, and upgrade rules and replication outbound transport for the tested target classes and knobs.

Not Yet Passing

Standard S3 areas that must not be described as complete:

Area Status Evidence
Bucket access logging Handlers exist (get_bucket_logging, put_bucket_logging in rustfs/src/storage/ecfs.rs); the test_*bucket_logging* s3tests cases are still listed as unimplemented unimplemented_tests.txt
POST Object form upload checksum handling Not yet passing unimplemented_tests.txt
Bucket ownership controls No handler unimplemented_tests.txt
Multipart upload listing and part lookup compatibility edge cases Not part of default gate excluded_tests.txt
IAM-account or multi-storage-class dependent cases Not part of default gate unimplemented_tests.txt
Tenanted bucket policy edge cases Needs investigation unimplemented_tests.txt

Intentional Exclusions

excluded_tests.txt holds tests that must not block the compatibility gate: vendor-specific or non-portable behavior, and intentionally unsupported product behavior such as ACL authorization.

Intentional Deviations From AWS S3

Object keys are stored as file-system paths under each drive ({drive}/{bucket}/{object}/xl.meta), the same layout MinIO uses. The rules below exist to keep that layout unambiguous and are not compatibility gaps to close; clients that need the AWS behavior must adapt on their side.

Behavior RustFS AWS S3 Why
Object key with a . or .. path segment, or an empty segment (//), such as a//b/./c/../d 400 InvalidArgument (check_object_args in crates/ecstore/src/bucket/utils.rs, mirroring MinIO IsValidObjectPrefix) Accepted as an opaque key A .. segment would resolve to a parent directory and ./// segments would alias other keys on disk; encoding them would change the MinIO-compatible on-disk format.
Directory marker (key ending in /, with or without a body) in a versioned bucket Stored as the null version: PutObject/HeadObject report version id 00000000-0000-0000-0000-000000000000, ListObjectVersions reports null, and a later PUT of the same key overwrites in place (put_opts in rustfs/src/storage/options.rs, mirroring MinIO putOpts: "for directory objects skip creating new versions") A real version id per PUT, with a version history The marker only exists to make an empty prefix listable; keeping a history for it would leave hidden versions behind every prefix delete. Replication still copies the marker as its null version (test_bucket_replication_replicates_directory_marker_in_versioned_bucket in crates/e2e_test/src/replication_extension_test.rs).

Update Rule

When a feature starts passing, move its test entries from unimplemented_tests.txt to implemented_tests.txt and update the row here in the same PR. Do not change README wording beyond the supported coverage. Handler-level status (missing, stubbed, or diverging endpoints) is tracked in minio-rustfs-router-compatibility.md.