Repository navigation
fix(pools): do not let data movement overwrite a newer copy - #248
Closed
botdeniska wants to merge 1 commit into
Closed
botdeniska wants to merge 1 commit into
botdeniska wants to merge 1 commit into
Conversation
Signed-off-by: Denis Babchuk <babchukdenis@icloud.com>
Member
|
Thank you, @botdeniska. Your fix was incorporated in 60c7179dd, preserving your authorship and DCO, and shipped in 20261018000000. 1748f62c7 adds the related delete-marker and source-cleanup safeguards. Final-artifact rebalance/decommission checks passed with 2 and 3 pools, concurrent writes/deletes, and interrupted-operation recovery. Closing this PR as incorporated through those commits. GitHub will show it as closed rather than merged; the contribution is included and credited in the release. Pool-affinity work remains separate in #245. |
7 of 12 tasks
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
Problem
Decommission and rebalance move each version out of the draining pool through
z.PutObject, a multipart upload,z.DeleteObject(delete markers) orz.DecomTieredObject, all withDataMovementset. The pools layer refuses only the source pool as the destination (idx == opts.SrcPoolIdx). Nothing compares the moved version with a copy of the same version ID that another pool already holds. The set layer keeps the moved ModTime and replaces the existing version.A key can have copies in several pools. An overwrite that lands in another pool leaves the previous null version behind; this became the common case with upstream minio#21115 (
83b2ad418b), and #245 stops creating new ones. The full-pool fallback, an interrupted decommission or rebalance, and replication can also leave copies. Reads stay correct because the newest copy wins. A move does not:kin an unversioned bucket is written to pool 0 at 10:00 and overwritten at 11:00 into pool 1. Pool 0 keeps the 10:00 null version, and reads return the 11:00 one.MTime: 10:00. Pool selection skips pool 0, findskin pool 1 and writes the old version there, replacing the 11:00 null version.On two pools this happens to every such key. With three or more pools, main picks the destination at random; with #245 it is always the pool that holds the current copy. The same window exists without any old duplicate: a client overwrite that commits while a move is copying the previous version is replaced by the moved copy. Unversioned and suspended buckets (null versions) are affected. Versions in a versioned bucket have distinct IDs and are not.
Change
checkDataMovementCopies(cmd/erasure-server-pool-consistency.go) reads the moved version in every pool withobjectPoolInfos. It returnsDataMovementOverwriteErrwith the newerrDataMovementSupersededwhen a pool other than the source holds a strictly newer copy.PutObject,CompleteMultipartUpload, theDataMovementbranch ofDeleteObjectandDecomTieredObjectcall it under the pools-layer object lock they already hold.PutObjectand completion, the same reads are handed to the set layer's lock and tag reconciliation asreplicaObjectInfo, so they replace that read instead of adding one.DataMovementwrite withoutMTimeis now rejected. Otherwise the set layer would stamp the moved copy with the current time, and it would win over every other copy. All movers already pass it.checkAfterDecomdoes not fail on it. Every otherDataMovementOverwriteErris still ignored without counting, as before.failure = err != nilread a shadowederrthat was always nil. A failedDecomTieredObjecttherefore counted as moved, and the source was removed. A failure now counts as a failure and keeps the source, as for other versions. This matters more now, because the check can fail when a pool is unreadable.SrcPoolIdx; it was 0 for every source pool. Both movers now passVersionIDto completion, so the check addresses the moved version. The set layer's completion reads neither field.Equal ModTime is moved as before. The same version ID with the same ModTime is the same write: moves and replication preserve both, and an interrupted move is the usual way to get such a copy. Moving it again also reconciles the copies' Object Lock and tag state (
ReplicaLockReconcile). Skipping it would delete the source copy, and with it lock state that deletes currently honour across copies.Disk reads. Single-part moves make no extra reads. A multipart move makes one extra metadata read per pool, before copying parts. Delete-marker and tiered moves make one per pool; these paths read no other pool before. Ordinary writes are unchanged.
Motivation and Context
The stand is one server with two pools,
/data/p1/d{1...4} /data/p2/d{1...4}, on linux/arm64.mc admin decommission start local/ /data/p1/d{1...4}) or rebalances it. For rebalance the pools sit on tmpfs of different sizes, so that rebalance drains pool 1.mc catdoes not return its last write.pgsty/silo:RELEASE.2026-09-16T00-00-00Z86fb2b83On main, every lost key returns an older write, never a 404. In every run, the versioned bucket loses nothing and lists 6 versions per key. With the change:
Decommission of pool /data/p1/d{1...4} is completewith 0 failed items, and rebalance reportsCompleted;xl.metaafterwards, so the stale duplicates are gone as well.Relation to other changes. #245 removes the main source of new duplicates. This change makes decommission and rebalance safe for the duplicates already on disk, and for writes that race a move.
Refs #207, #245, minio#21115.
How to test this PR?
New tests in
cmd/erasure-server-pool-migration_test.gouse the real multi-pool fixtures and both movers:TestPoolsMigrationKeepsCurrentCopycovers decommission and rebalance, single-part and multipart, null and versioned duplicates, source pool 0 and 1, and a source copy that is older, equal or newer. A move never changes what reads return, and an older copy is skipped before any multipart part is copied.TestPoolsMigrationYieldsToNewerWritecommits a client overwrite, or a replica of the same version, while the move copies. For multipart, a gated reader makes the check at completion run.TestPoolsMigrationThreePoolschecks that a superseded copy neither replaces the newer copy nor appears in the third pool.TestPoolsMigrationRetiresSupersededCopydrivesdecommissionPoolandrebalanceBucketover stale and newer keys: single-part, multipart, versioned, delete marker (suspended bucket) and tiered (decommission only). Reads are unchanged, the source pool is empty, and no version failed.TestPoolsMigrationTieredFailureKeepsSource: a tiered move that fails because another pool is unreadable is counted as failed and keeps its source.Without the change, 36 subtests fail on main and on main + #245, for example
stale: reads "old" after the move, want "new". For this check the production code was reverted, andisDataMovementSupersededwas stubbed withisDataMovementOverWriteErrso that the tests build. I also removed each part of the change on its own: the completion check, the early multipart check,SrcPoolIdxorVersionIDon completion, the walker accounting for data, marker and tiered versions, and the tiered error handling. Each removal makes at least one test fail.TestPoolsConditionalPutVersionSelectionnow passesMTimein its data-movement case, as the movers do.Run locally on Go 1.27.2 (darwin/arm64):
make verifiers(golangci-lint v2.15.0: 0 issues, check-gen, rebrand-guard),go vet ./cmd,make buildCGO_ENABLED=0 MINIO_API_REQUESTS_MAX=10000 go test ./cmd/ -count=1 -timeout 30mgo test -race -count=3 ./cmd -run 'TestPoolsMigration|TestPoolsReplica|TestPoolsConditionalPut|TestPoolsDeleteVersion|Decom|Rebalance'go test ./cmd -run 'Pool|Decom|Rebalance|Consistency|Multipart|Placement|Tag'on main + fix(pools): keep overwrites in the pool that holds the object #245Compatibility impact
No API, configuration, storage format, metric, header, route or dependency changes.
data movement skipped, another pool holds a newer copy of …. It is not counted as a failure.DataMovementwrites. Delete-marker and tiered moves now fail the same way.Known issues, not changed here. The first two concern a newer delete marker on the draining pool with a stale copy elsewhere. Both are confirmed with unit tests, on main and with this change.
DeleteObjecttreats it as a versioned delete when the version exists. An older version then becomes visible.Happy to open separate issues or a follow-up PR for them.
Types of changes
Checklist:
git commit -s) per the DCOcommit-idorPR #here): the missing check is inherited. Upstream83b2ad418b(fix: restrict SinglePool by the minimum free drive threshold minio/minio#21115) made the duplicates it trips on common.make verifierspassesmake buildpasspgsty/silo.pgsty.com, if needed (not needed)