mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-06 03:59:14 +00:00
fix(ecstore): make transitioned cleanup crash-safe (#6978)
* fix(ecstore): fence transitioned object cleanup * fix(ecstore): address ILM recovery review findings * fix(ecstore): complete crash-safe tier cleanup recovery * test(ecstore): avoid typo false positive * fix(ecstore): stabilize decommission error buckets * fix(ecstore): stabilize transition delete validation * fix(ecstore): resume authorized tier delete dispatch * fix(ecstore): satisfy feature clippy
This commit is contained in:
@@ -39,6 +39,8 @@ for later deletion.
|
||||
- `rustfs-5063` pre-beta.9 Local KMS recovery: persisted Local KMS configs from beta.8 and earlier predate the explicit insecure-development flag, and encrypted key files use the legacy SHA-256 KDF. Remove the config fallback after supported upgrades have rewritten or explicitly resaved all pre-beta.9 configs with the development-default field, and remove the legacy KDF after supported upgrades have rewritten all pre-beta.9 Local KMS key files with explicit at-rest protection.
|
||||
- `sse-local-dek-json-v1` legacy local SSE DEK decoding: releases before the JSON envelope wrote wrapped DEKs as `base64(nonce):base64(ciphertext)`, so readers retain that decoder while all new writes use the versioned JSON envelope. Remove the colon decoder after the minimum supported direct-upgrade release writes JSON envelopes and migration tooling has rewritten every retained legacy object.
|
||||
- `not-initialized-error-code-v1` typed control-plane not-initialized wire code: control-plane RPC responses historically signaled an uninitialized peer only through the literal error_info string "errServerNotInitialized" (one drift site says "storage layer not initialized"). Responses now dual-carry a typed ControlPlaneErrorCode beside the legacy string, and clients prefer the code; the string stays populated and the client substring fallback (is_err_not_initialized, control_plane_failure) stays in place so mixed-version clusters keep classifying older peers' responses. Remove the substring fallback (and stop populating error_info for this case) after the minimum supported RustFS peer version always sends error_code.
|
||||
- `backlog-2097-tier-mutation-v4-error-text` tier-mutation Prepare rejection classification: a v3 server rejects a v4 request before store/runtime dispatch with the FailedPrecondition status and an authenticated, byte-exact unsupported-version message. A v4 coordinator recognizes only that exact code/message/requested-version tuple as definitely not persisted and fails the mutation without sending that peer an incompatible Abort; Unimplemented, near-text, missing/unknown failure classes, timeouts, and every other transport outcome remain ambiguous and stay in identity-bound Abort fanout. There is no automatic v3 retry. New servers retain v3 request/proof decoding for older coordinators, while operators must pause tier edit/remove/clear during a mixed v3/v4 rollout. Remove the text classifier after the minimum supported RustFS peer version returns the signed v4 PreDispatchRejected failure class.
|
||||
- `backlog-2097-tier-delete-journal-v6` tier-delete sole-owner recovery: v6 distinguishes transactions that may replace the xl.meta free-version owner and therefore require the all-pool live-source proof. v5-and-older readers reject and retain v6 records during rolling upgrades instead of performing an unsafe remote delete. Keep v1-v5 readers for upgrade recovery and keep the downgrade prohibition while any v6 record exists; retire the fence only after every supported rollback release understands and enforces v6 proof semantics.
|
||||
- `multipart-compression-default-off-window` staged multipart disk-compression rollout: releases before the resumable legacy decompressor fail transient reads of compressed objects under mid-payload suspension, so multipart uploads advertise the compression marker only when RUSTFS_COMPRESSION_MULTIPART_ENABLED is set in addition to RUSTFS_COMPRESSION_ENABLED, keeping rolling upgrades from creating new compressed multipart objects while pre-fix nodes may still serve reads. Flip the default to enabled (and retire the extra switch) after the minimum supported direct-upgrade release ships the resumable decompressor.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
@@ -165,16 +165,33 @@ and all listing/GET paths exclude them.
|
||||
|
||||
### Lifecycle And Consumers
|
||||
|
||||
Creation: any local delete that removes a version whose transition status is
|
||||
`complete` appends the record via `MetaObject::delete_version` →
|
||||
`init_free_version` (skipped only when `skip_tier_free_version` is set, as on
|
||||
data-movement copies). The same deletes also persist a durable tier-journal
|
||||
entry on every user-facing path: S3 single deletes (`execute_delete_object` →
|
||||
`delete_object_with_tier_delete_journal`), S3 batch deletes, lifecycle expiry,
|
||||
and lifecycle delete-all all prepare and commit a journal entry around the
|
||||
delete. A journal entry is omitted when the removed version's transition state
|
||||
decodes as `TransitionVersionState::Unknown`, or on internal journal-less
|
||||
delete paths that never touch transitioned user objects.
|
||||
Creation: a local delete that removes a version whose transition status is
|
||||
`complete` normally appends the record via `MetaObject::delete_version` →
|
||||
`init_free_version`. User-facing single and batch deletes always retain that
|
||||
historical owner when they actually remove a transitioned source; they do not
|
||||
create a tier journal, probe a fleet capability, or issue a peer mutation RPC.
|
||||
`TransitionVersionState::Unknown` and incomplete destination identities remain
|
||||
on the same conservative free-version path. Delete-marker creation on an Enabled
|
||||
bucket remains unchanged and does not schedule remote deletion.
|
||||
|
||||
Recursive prefix/delete-all cannot preserve per-object markers across its
|
||||
physical directory purge, so it requires a v6 recoverable journal for every
|
||||
transitioned visible source plus a durable dispatch manifest for the complete
|
||||
operation. It fails closed before mutation on legacy metadata or any existing
|
||||
hidden tier free-version under the prefix. Its internal streaming walk
|
||||
discovers logical keys, then exact-loads every key from its authoritative set in
|
||||
every pool, including free versions; the S3 listing merge is never treated as a
|
||||
complete physical-owner inventory. Tier-operation leases remain held from that
|
||||
preflight through journal prepare and physical deletion. Once physical deletion
|
||||
starts, any error is mutation-ambiguous: authorized/dispatched journals remain
|
||||
for recovery to commit owners only after all physical sets prove both the source
|
||||
and exact free-version identity absent; uncertain owners are retained.
|
||||
If a retry discovers a later transitioned source after the manifest reached
|
||||
`DispatchAuthorized`, it replays only the manifest's immutable predecessor set,
|
||||
completes that operation, and leaves the newcomer for a successor dispatch.
|
||||
Operators may retry after the legacy free-version worker has durably completed
|
||||
remote and local cleanup. Journal-less internal deletes and older nodes retain
|
||||
their established marker behavior.
|
||||
|
||||
Consumption while the record exists: the background recovery loop started by
|
||||
`init_background_expiry` (spawned by `spawn_tier_free_version_recovery_once`,
|
||||
@@ -209,16 +226,93 @@ only when its free-version identity matches; a conflicting ordinary version or
|
||||
different free record is an overwrite error. This makes retries idempotent and
|
||||
prevents a free record from replacing a user-visible version.
|
||||
|
||||
### Remote-Tuple Publication Fence
|
||||
|
||||
Cross-pool capability v3 includes a commit-late publication contract for every
|
||||
path that can copy an existing transition tuple to a new physical owner. This
|
||||
capability version is independent of the tier-mutation RPC protocol version.
|
||||
A mixed fleet whose minimum cross-pool capability is below v3 cannot authorize
|
||||
journal-v6 remote deletion.
|
||||
|
||||
Data movement captures a non-cloneable, process-local source capability before
|
||||
copying, but it does not hold a namespace write lock or tier-operation lease
|
||||
while reading a large body or uploading multipart parts. `NewMultipartUpload`
|
||||
and `UploadPart` are staging only. Immediately before single-PUT rename,
|
||||
Multipart Complete, or a pure-remote/free-version metadata quorum write, the
|
||||
final consumer acquires the exact tier generation (when a remote tuple exists),
|
||||
then fixed/source/target write domains in stable order. The fixed domain is used
|
||||
only for a real remote-tuple decommission publisher; an ordinary local object
|
||||
keeps the lighter source/target commit scope.
|
||||
|
||||
While that owned scope is held, the publisher re-reads the exact source pool and
|
||||
compares version, data directory, modification time, ETag, checksums, transition
|
||||
tuple, transition-version state, and destination identity. A missing or changed
|
||||
source, changed/revoked tier generation, bucket incarnation change, or lost lock
|
||||
fails before target rename. The scope remains owned through rename quorum and
|
||||
the existing rename-tail guard handoff. Consequently, recovery-first ordering
|
||||
cannot delete the remote object and then have a stale restored-transitioned
|
||||
rebalance recreate its tuple; publisher-first ordering makes recovery wait and
|
||||
rescan the newly committed owner.
|
||||
|
||||
Full cross-key S3 Copy is not an ownership-sharing operation: it materializes
|
||||
local data and strips transition, destination, transaction, and free-version
|
||||
keys. Same-key metadata/version-only updates preserve the existing protected
|
||||
state. Admin heal keeps the legacy `nolock` request field for wire compatibility
|
||||
but ignores it as lock authority; final heal writes enter the normal locked
|
||||
path. Restore similarly ignores ambient `ObjectOptions.no_lock`, acquires its
|
||||
own commit-late PUT/Complete lock, validates the restore operation id, and keeps
|
||||
an exact tier generation lease through the local commit.
|
||||
|
||||
### Reference-Audit Result
|
||||
|
||||
After migration, user-facing GET/list/transition/replication/restore paths still
|
||||
exclude the record. Recovery, usage scanning, lifecycle tier cleanup, and heal
|
||||
continue to see it when they request free versions, so an unresolved remote
|
||||
delete remains actionable on the target pool. The committed tier journal remains
|
||||
an independent retry source where one exists; it is not used as a reason to drop
|
||||
the xl.meta record. In particular, `Unknown` transition state records are
|
||||
migrated unchanged rather than discarded: the lifecycle worker retains them if
|
||||
remote identity validation cannot make a delete request.
|
||||
continue to see a legacy/fallback record when they request free versions, so an
|
||||
unresolved remote delete remains actionable on the target pool. Only an
|
||||
authorized recursive prefix/delete-all v6 transaction may instead use a
|
||||
per-source journal as the sole retry source; ordinary single/batch deletes never
|
||||
take that path. A journal discovered
|
||||
alongside an older or fallback free-version does not authorize dropping the
|
||||
record. In particular, `Unknown` transition state records are migrated unchanged
|
||||
rather than discarded: the lifecycle worker retains them if remote identity
|
||||
validation cannot make a delete request.
|
||||
|
||||
Tier edit/remove/clear reference proof uses the internal walk with
|
||||
`include_free_versions = true`, in addition to persisted journal and transition
|
||||
transaction checks. Protocol v3 peer Prepare blocks new reference creators and
|
||||
drains existing tier-operation leases before this proof; protocol v4 preserves
|
||||
that state machine and adds a signed failure classification. Abort carries the
|
||||
canonical Prepare intent, so a peer can create an identity-bound `Aborted`
|
||||
tombstone even when Abort overtakes Prepare. A delayed matching Prepare then
|
||||
converges on `Aborted` instead of reinstalling the block; a conflicting intent
|
||||
with the same mutation id fails closed. The tombstone remains durable until the
|
||||
intent expiry plus the configured clock-skew allowance, including across reload
|
||||
and coordinator-record cleanup. After
|
||||
expiry, a missing-record replay of the original signed Prepare is rejected and
|
||||
cannot recreate a peer-only runtime fence. Abort checks an existing same-identity
|
||||
terminal record before consulting mutable current-config proof, and recovery
|
||||
reconstructs the original Prepared revision for Abort fanout.
|
||||
|
||||
A new server accepts both v3 and v4 requests and selects the matching canonical
|
||||
response proof. During a mixed rollout, an older v3 server rejects a v4 request
|
||||
with an authenticated, byte-exact unsupported-version status before dispatch;
|
||||
the v4 coordinator treats only that exact rejection as definitely not installed,
|
||||
fails the admin mutation, and does not send the peer an incompatible Abort.
|
||||
There is deliberately no automatic v3 retry. `Unimplemented`, near-text,
|
||||
timeouts, missing/unknown failure classes, and other ambiguous outcomes still
|
||||
receive Abort and retain the coordinator retry record if Abort cannot be proven.
|
||||
Operators must pause and drain tier edit/remove/clear operations before starting
|
||||
the rolling upgrade, leave them disabled while any v3-only peer remains, and
|
||||
resume only after every topology member advertises the v4-capable release.
|
||||
Ordinary object I/O and free-version cleanup remain available; xl.meta is
|
||||
unchanged by the rejected mutation.
|
||||
|
||||
Sole-owner transactions use journal v6: v5-and-older readers reject and retain
|
||||
those records, so an old recovery worker cannot bypass the all-pool proof. Older
|
||||
nodes may continue to create fallback free-versions until the rollout is
|
||||
homogeneous. A deployment must not downgrade every v6-aware recovery worker
|
||||
while any v6 record remains; drain the journal first or keep at least one v6-aware
|
||||
worker until cleanup converges.
|
||||
|
||||
Each migrated record emits `state = "free_version_migrated"` with reason
|
||||
`tier_free_version_migrated`. A record consumed before migration emits
|
||||
|
||||
Reference in New Issue
Block a user