Repository navigation
fix(ilm): keep tier data when expiry only adds a delete marker - #253
Merged
Vonng merged 1 commit intoOct 24, 2026
Merged
Conversation
Signed-off-by: Denis Babchuk <babchukdenis@icloud.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contribution Licensing (no CLA, inbound=outbound, DCO required)
This pull request contributes to PGSTY SILO (
pgsty/silo). Code contributionsare accepted under AGPL-3.0-or-later, the same license as the server.
This project does not use a CLA or require a separate Apache-2.0 license grant.
By submitting this pull request I represent that I have the right to contribute
the code changes under this repository's
GNU Affero General Public License v3.0 or later
and retain copyright in my original work. Existing copyright and license
notices remain intact; separately licensed material keeps its applicable terms.
Every commit must carry a DCO
Signed-off-bytrailer(
git commit -s) certifying theDeveloper Certificate of Origin — see
CONTRIBUTING.md.
Description
Fixes #252.
expireTransitionedObject(cmd/bucket-lifecycle.go:610) deletes the remote tier object of every transitioned version it expires, before it deletes the version:For
DeleteAction, the Expiration of the current version,opts.VersionIDis empty. In a versioned bucketDeleteObjectthen only adds a delete marker, and the expired version stays as a noncurrent version whose tier object is gone: it is listed, andGET ?versionId=returns 404NoSuchKey.opts.VersionSuspendedis never set either, so with versioning suspendedDeleteObjecttries to delete thenullversion. For a current version with a UUID and nonullversion below it, that fails withVersionNotFoundafter the tier object is already deleted, and the scanner repeats it every cycle. Anullcurrent version is removed without thenulldelete marker.With this change:
VersionSuspendedis set as inapplyExpiryOnNonTransitionedObjects(cmd/data-scanner.go:1236), so a transitioned version expires into the same delete marker as a version that is not transitioned.SkipFreeVersionstays false there. When the version itself is deleted later, byNoncurrentVersionExpirationorDELETE ?versionId=,DeleteVersionadds a free version and the tier object is removed through it. This is the rule the userDELETEpath already follows inobjSweeper.shouldRemoveRemoteObject(cmd/tier-sweeper.go:108-121).nullcurrent version with versioning suspended.VersionNotFound, no delete marker, the version returns 404nulldelete marker; tier object kept, the version reads backnullnulldelete marker replaces the version; tier object deletednullExpiry of a restored copy returns before this code and is unchanged.
Motivation and Context
This is silent data loss. The scanner expires current versions on its own, and a GET, HEAD or listing of an expired object queues the same expiry, so it needs no failure, decommission or rebalance. Pool decommission and rebalance move transitioned versions correctly; the versions they move are lost later by this expiry like any other. The expired version stays listed with the tier's storage class, so the loss shows only when someone reads that version.
ExpiredObjectAllVersionson a transitioned version also goes throughexpireTransitionedObjectwith an empty version ID and only adds a delete marker in a versioned bucket. That is a separate problem, described in the issue and not changed here. With this change, those versions at least keep their data.How to test this PR?
TestExpireTransitionedObjectKeepsNoncurrentData(cmd/bucket-lifecycle_test.go) runs on the real two-pool fixture (migrationTestPools). An in-memoryWarmBackendis put intoglobalTierConfigMgr.drivercache. Transitioned versions are written withDecomTieredObject, as version metadata with a completed transition, so reads go to the tier, and their data is put into the fake tier. The test evaluates an Expiration rule withevalActionFromLifecycleand callsexpireTransitionedObjectas the expiry worker does. It then checks the versions left in the namespace, whether the remote object of the expired version was removed, and that a kept version reads back. Subtests:versioned: v1, v2 transitioned; after the expiry[DM v2 v1], v2's remote object kept, v2 reads back.suspended: v1, v2 with UUIDs, versioning suspended;[DM v2 v1], v2 kept and readable.suspended-null: v2 is thenullversion;[DM v1], v2's remote object removed.unversioned: the object and its remote object are removed.On main (trimmed):
With the change all four pass.
unversionedis a guard and passes on both.Manual reproduction. The script from the issue: a hot server with 4 drives and a second SILO server as a
miniotier (mc admin tier add minio), a Transition rule withDays=0,MINIO_SCANNER_SPEED=fastest, then an Expiration rule. A wider run on the same setup also covers the rules that must keep deleting tier data. Results on mainb91e1c6f2and on this branch:WARM,GET ?versionId=404NoSuchKey, tier object goneDELETE ?versionId=v3removes its tier object through the free versionDELETEevery scanner cycle (8 in 2 min)nulldelete marker, v1 200nullcurrent version, Expirationnulldelete marker, tier object deletedNoncurrentVersionExpiration(NewerNoncurrentVersions=1;NoncurrentDays=1)ExpiredObjectAllVersions(separate problem)After every run with this change, each tier object is referenced by a version, and no version points to a missing tier object.
Overlap with #240. #240 (open) also edits
expireTransitionedObject. It addsopts.SetEvalMetadataFn(...)just before the// Delete remote object from warm-tiercomment and replaces theDeleteObjectcall. This change keeps that comment line and theDeleteObjectcall as they are, so the two do not touch the same lines. I applied the #240 diff to main and cherry-picked this commit on top, and also did it the other way round. Both orders apply without conflicts and give the samecmd/bucket-lifecycle.go. On the combined tree,go vet ./cmd, this test and #240'sTestReplicateILMExpiryDeleteMarkerandTestReplicateILMExpiryNoncurrentPurgepass. The closure from #240 readsoptswhenDeleteObjectruns, afterVersionSuspendedis set, andcheckReplicateDeleteonly looks atVersioned, so replication decisions are the same as with #240 alone. If a later revision of #240 does conflict here, the resolution is to keep itsSetEvalMetadataFnblock before the comment and this block after it.Run locally on Go 1.27.3 (darwin/arm64), branch based on main
b91e1c6f2:make verifierssteps: golangci-lint v2.15.0 with.golangci.yml(0 issues),make check-gen rebrand-guard;go vet ./cmd;make buildgo test ./cmd -count=1 -timeout 40m: okgo test ./cmd -count=1 -run 'Lifecycle|Expir|Transition|Tier': okCompatibility impact
No API, configuration, storage metadata, metric, header, route or dependency changes.
DELETEwithoutversionIdalready does. Buckets that rely on Expiration alone, without a noncurrent rule, keep those versions and their tier data, as they already keep noncurrent versions that are not transitioned.nulldelete marker, as it does for versions that are not transitioned. Anullcurrent version is replaced by the marker instead of being removed without one. A current version with a UUID gets the marker on top and stays as a noncurrent version.Types of changes
Checklist:
git commit -s) per the DCOcommit-idorPR #here) (no: the unconditional remote delete predates the fork)make verifierspassesmake buildpasspgsty/silo.pgsty.com, if needed (not needed)