Problem
Bucket metadata is persisted as one buckets/<bucket>/.metadata.bin record, but most writers perform an unlocked load-modify-save. Different configuration types can therefore read the same old record and overwrite each other's fields.
Confirmed writer paths include:
BucketMetadataSys.Update/Delete through updateAndParse;
- local and peer CORS transitions, currently protected only by
cors-config.lock;
- legacy/bulk
PeerBucketMetadataUpdateHandler;
PeerBucketMakeWithVersioningHandler;
ImportBucketMetadataHandler;
erasureServerPools.MakeBucket;
convertLegacyConfigs and migrateTargetConfig, which save from metadata load paths.
A related confirmed clobber exists in MakeBucket{ForceCreate:true}: storage-level errVolumeExists is ignored and a fresh .metadata.bin is written for the existing bucket, dropping policy, CORS, tagging, encryption, and other fields. Import can reach this path after a metadata read failure.
This is separate from #77. Issue #77 tracks site-replication source timestamps, tombstones, heal selection, diagnostics, and counters; it does not own cross-type whole-record serialization.
Required design
Introduce one distributed namespace lock:
.minio.sys / buckets/<bucket>/metadata.lock
Lock order must be:
<bucket>.lck -> metadata.lock -> .metadata.bin
Requirements:
- hold
metadata.lock from the authoritative disk read through mutation and .metadata.bin save;
- never acquire it inside
BucketMetadataSys.save or BucketMetadata.Save because object saves take the .metadata.bin namespace lock and locks are not re-entrant;
- do not call
MakeBucket or DeleteBucket while already holding metadata.lock;
- update local cache while the committed record is known, then release
metadata.lock before peer LoadBucketMetadata fan-out or BucketMetaHook;
- replace the CORS-specific lock with the shared lock;
- make
ForceCreate read-or-fresh and preserve an existing record;
- parse imports off-lock, then apply per-bucket field patches to a fresh under-lock record; a non-
errConfigNotFound read failure must be reported, not converted into ForceCreate;
- serialize migration saves or route unlocked load paths through a locked migration helper.
Test-first reproduction
Add a controllable ObjectLayer wrapper around .metadata.bin writes:
- writer A reads the old record and pauses at
PutObject entry;
- writer B starts;
- on the unfixed tree B reaches a second stale read and completes, then A overwrites B;
- with
metadata.lock, B attempts the lock but cannot read until A is released;
- final persisted and resident metadata must contain both fields.
Required pairs, on both ObjectLayer backends:
- policy + CORS;
- tagging + SSE;
- local Update + peer/bulk direct-save variants.
Behavioral coverage:
- ForceCreate preserves policy/CORS and
Created;
- peer bucket adoption preserves existing metadata;
- Import overwrites only supplied fields and preserves unrelated concurrent fields;
- migration cannot overwrite a concurrent update;
- lock acquisition is context bounded and no re-entrant deadlock occurs.
Documentation
Update docs/site-replication/CORS-LWW-DESIGN.md:
Deliberate follow-ups
These are adjacent but may be split if needed to keep the first PR reviewable:
- higher-level lifecycle XML merge atomicity outside
BucketMetadataSys.Delete;
DeleteBucket versus an in-flight metadata update (ghost-record resurrection);
- overlapping peer reloads leaving a local cache one save behind until refresh.
The first PR must not claim these follow-ups are fixed unless their reproductions and tests are included.
Problem
Bucket metadata is persisted as one
buckets/<bucket>/.metadata.binrecord, but most writers perform an unlocked load-modify-save. Different configuration types can therefore read the same old record and overwrite each other's fields.Confirmed writer paths include:
BucketMetadataSys.Update/DeletethroughupdateAndParse;cors-config.lock;PeerBucketMetadataUpdateHandler;PeerBucketMakeWithVersioningHandler;ImportBucketMetadataHandler;erasureServerPools.MakeBucket;convertLegacyConfigsandmigrateTargetConfig, which save from metadata load paths.A related confirmed clobber exists in
MakeBucket{ForceCreate:true}: storage-levelerrVolumeExistsis ignored and a fresh.metadata.binis written for the existing bucket, dropping policy, CORS, tagging, encryption, and other fields. Import can reach this path after a metadata read failure.This is separate from #77. Issue #77 tracks site-replication source timestamps, tombstones, heal selection, diagnostics, and counters; it does not own cross-type whole-record serialization.
Required design
Introduce one distributed namespace lock:
Lock order must be:
Requirements:
metadata.lockfrom the authoritative disk read through mutation and.metadata.binsave;BucketMetadataSys.saveorBucketMetadata.Savebecause object saves take the.metadata.binnamespace lock and locks are not re-entrant;MakeBucketorDeleteBucketwhile already holdingmetadata.lock;metadata.lockbefore peerLoadBucketMetadatafan-out orBucketMetaHook;ForceCreateread-or-fresh and preserve an existing record;errConfigNotFoundread failure must be reported, not converted into ForceCreate;Test-first reproduction
Add a controllable ObjectLayer wrapper around
.metadata.binwrites:PutObjectentry;metadata.lock, B attempts the lock but cannot read until A is released;Required pairs, on both ObjectLayer backends:
Behavioral coverage:
Created;Documentation
Update
docs/site-replication/CORS-LWW-DESIGN.md:cors-config.lockwithmetadata.lock;Deliberate follow-ups
These are adjacent but may be split if needed to keep the first PR reviewable:
BucketMetadataSys.Delete;DeleteBucketversus an in-flight metadata update (ghost-record resurrection);The first PR must not claim these follow-ups are fixed unless their reproductions and tests are included.