Siyao Meng created HDDS-16125:
---------------------------------

             Summary: Quota repair aliases the live cached OmBucketInfo and 
double-counts bucket usedBytes after crash and Ratis replay
                 Key: HDDS-16125
                 URL: https://issues.apache.org/jira/browse/HDDS-16125
             Project: Apache Ozone
          Issue Type: Bug
          Components: OM
            Reporter: Siyao Meng


h3. Problem

On an OM bucket, online quota repair can leave the durable {{usedBytes}} 
double-counted after a crash and Ratis replay: the bucket durably records usage 
2 for a single 1-byte key.

h3. Root cause

{{OMQuotaRepairRequest.updateBucketInfo}} reads the bucket via 
{{getBucketTable().get()}} (a copy), then stores that same object into both the 
cache ({{addCacheEntry}}) and the client response map ({{bucketMap.put}}) with 
no {{copyObject()}} (OMQuotaRepairRequest.java:120,144,146). Every other 
mutating request copies its response bucket for replay safety, for example 
{{omBucketInfo.copyObject()}} at OMKeyCommitRequest.java:410. A later key 
commit fetches the same live cached object via {{OMKeyRequest.getBucketInfo}} 
({{getCacheValue}}, no copy; OMKeyRequest.java:903-910) and mutates it in 
place. If the double buffer serializes the repair batch after that in-place 
mutation but the key-commit batch is not yet durable, a crash strands a durable 
prefix that already contains the key commit's increment under the repair 
transaction index; on restart Ratis replays the committed key commit and the 
count is applied twice.

h3. Trigger

# Bucket {{usedBytes=1}} (drifted; true usage 0). Repair computes delta -1, 
live becomes 0; the response and cache entry alias one object.
# The double buffer begins flushing the repair batch (index1) and pauses before 
serialization.
# A 1-byte key commit (index2) reads the aliased live object (0) and increments 
it to 1.
# The repair batch serializes the mutated aliased object, so durable index1 
records usedBytes=1 (repair's own result is 0).
# A crash before index2 flushes loses the unflushed index2 and the in-memory 
cache.
# Restart reloads caches from the durable DB (bucket=1) and Ratis replays 
committed index2, driving live and durable usedBytes to 2 while true usage is 1.

h3. Impact

{{OmBucketInfo.getUsedBytes()}} is read by 
{{OMKeyRequest.checkBucketQuotaInBytes}} on every key write, so an inflated 
count rejects legitimate writes early when a space quota is set, and {{ozone 
admin}} bucket usage reporting shows the wrong value. The wrong count is 
durable, and re-running repair is subject to the same race.

h3. Suggested fix

Store {{bucketInfo.copyObject()}} into the response map (and/or add a distinct 
copy to the cache) at OMQuotaRepairRequest.java:146, mirroring 
{{OMKeyCommitRequest}}, so the repair response owns an immutable snapshot and a 
later key commit's mutation cannot bleed into the repair's durable write.

h3. Notes

Discovered via TLA+ formal verification and model checking under HDDS-16123. 
All code claims were verified against the source and reproduced 
deterministically on a single-module ozone-manager JUnit test that drives the 
real OM state machine, double buffer, and request handlers (observed 
liveCount=2, durableCount=2, trueUsage=1). Pinned source commit 
9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb. Analysis assisted by AI tooling 
(Claude Code, Opus 4.8) via the Specula pipeline.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to