Siyao Meng created HDDS-17240:
---------------------------------

             Summary: Quota repair rolls back unflushed volume changes and 
writes converted volumes under the bare volume name
                 Key: HDDS-17240
                 URL: https://issues.apache.org/jira/browse/HDDS-17240
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: MC-3-quota-repair-keeps-unflushed-volume-changes.patch, 
TestBugMC3QuotaRepairStaleVolumeRow.java, 
TestBugMC4QuotaRepairTenantRefCount.java, TestBugMC5QuotaRepairVolumeKey.java

h3. Mechanism
{{OMQuotaRepairRequest.updateOldVolumeQuotaSupport}} finds volumes with the old 
quota value ({{OLD_QUOTA_DEFAULT}}, -2) through 
{{getVolumeTable().iterator()}}, which reads RocksDB only. For each one it 
builds the new value from that RocksDB row and puts it into the volume table 
cache. The volume table is fully cached on the OM, so that value is what every 
later request reads and what any later write of the volume persists.

{{OzoneManagerStateMachine}} does not wait for the double buffer before 
applying the repair, so a volume change applied just before it in the log may 
not be in RocksDB yet: a volume create or delete, the {{usedNamespace}} change 
of a bucket create or delete, the {{refCount}} change of a tenant create or 
delete, or a quota change. The repair replaces it with the older row, or skips 
a volume whose create is not flushed. Whether an OM sees the older row depends 
on how far its own double buffer has flushed, so OMs that apply the same log 
can end in different states.

{{OMQuotaRepairResponse.addToDBBatch}} then writes each converted volume with 
{{putWithBatch(batchOp, volArgs.getVolume(), volArgs)}}, so under {{legacyvol}} 
instead of the volume table key {{/legacyvol}} ({{getVolumeKey}}) that the 
cache entry and every other volume writer use. The real row keeps -2 and a 
second row is added under the bare name; both are loaded into the cache on the 
next restart.

The bucket half of the same request reads each bucket through 
{{getBucketTable().get()}} and writes it under {{getBucketKey}}; the volume 
half does neither.

h3. Trigger
* A volume with namespace quota -2. Ozone 1.0.0 and earlier had no namespace 
quota field, so every volume created there reads as -2. Ozone 1.4.x converted 
them at its first start ({{ON_FIRST_UPGRADE_START}}, written straight to 
RocksDB), so a cluster that ever ran 1.4.x has none left from that time. They 
remain on clusters upgraded from 1.3.x or earlier directly to 2.x, and a pre 
1.1 client creates new ones on a current cluster, because the OM does not 
normalize the field.
* A full quota repair: {{ozone repair om quota start}} without {{--buckets}}, 
or the upgrade action when the {{QUOTA}} layout feature is finalized. Both are 
admin only.
* For the rollback, a change to that volume applied but not yet flushed when 
the repair is applied. For the lost conversion, an OM restart.

h3. Impact
Reproduced at request level on unmodified source with real requests in log 
order; the tests only decide when the double buffer flushes and when the OM 
restarts.
* *Deleted volume comes back.* After an unflushed DeleteVolume and the repair, 
the volume is visible again, CreateBucket in it returns OK and writes the 
volume back to RocksDB. On an OM that flushed the delete first, the same 
CreateBucket returns {{VOLUME_NOT_FOUND}}. 
[^TestBugMC3QuotaRepairStaleVolumeRow.java]
* *Bucket count lost.* After an unflushed CreateBucket and the repair, the 
volume has {{usedNamespace}} 0 with one bucket. The repair also clears the 
namespace quota, so nothing is enforced at first, but a namespace quota set 
later is checked against a count that is one too low, and deleting that bucket 
takes {{usedNamespace}} to -1. [^TestBugMC3QuotaRepairStaleVolumeRow.java]
* *Tenant volume reference lost.* After an unflushed {{tenant create --force}} 
on such a volume and the repair, the volume has {{refCount}} 0. DeleteVolume 
then deletes the tenant's volume if it is empty, the tenant row stays and 
TenantDelete returns {{VOLUME_NOT_FOUND}}. A TenantDelete on the same state 
throws {{IllegalStateException: refCount should not be negative}} out of 
{{validateAndUpdateCache}}; {{OzoneManagerStateMachine.runCommand}} terminates 
the OM on such an exception (read from the code, the test stops at the 
exception). On an OM that flushed the tenant create first, DeleteVolume returns 
{{VOLUME_IS_REFERENCED}}. [^TestBugMC4QuotaRepairTenantRefCount.java]
* By the same code, not run: an unflushed TenantDelete is rolled back to 
{{refCount}} 1 and the volume can no longer be deleted, and a volume created 
with -2 but not yet flushed is converted on one OM and left at -2 on another.
* *Conversion lost on restart, volume listed twice.* After the repair, a flush 
and a restart, the volume reads -2 again, unless another request wrote the 
volume in between (a bucket create in it does), and {{ozone sh volume 
setquota}} keeps printing the "created before version 1.1.0" warning. The bare 
name row is loaded as a second volume with the same name: a listing of all 
volumes ({{listVolumes}} without a user) returns it twice, 
{{VolumeUtilizationMetrics}} reports it twice, and Recon, which reads the OM 
DB, lists and counts it. By the same code, the bare name row stays listed after 
the volume is deleted (not run). [^TestBugMC5QuotaRepairVolumeKey.java]

The bare name write hides part of the rollback on restart, but fixing only the 
key makes it worse: the repair's own write then makes the rolled back value 
durable, and in the tenant case the replayed TenantDelete terminates the OM 
again after every restart. Both changes are in one patch for that reason.

Not run on a three OM cluster; the difference between OMs is shown by applying 
the same log with and without the flush.

h3. Reproduction
The three attached classes go into {{hadoop-ozone/ozone-manager}} (target 
package and Maven command in the class comment) and pass on unmodified source 
while the bug is present. They apply real CreateVolume, DeleteVolume, 
CreateBucket, CreateTenant, DeleteTenant and QuotaRepair requests through 
{{validateAndUpdateCache}} and flush the responses as the double buffer does; 
the first two have a control that flushes before the repair, the third reopens 
the OM metadata manager on the same RocksDB directory, twice. The legacy volume 
is created by a CreateVolume request without the namespace quota field, or 
written as Ozone 1.0.0 left it. The multi tenant manager and S3 secret manager 
are mocks.

h3. Patch
[^MC-3-quota-repair-keeps-unflushed-volume-changes.patch], against 
20d95ed37fe35013b5320713665db03c06ee36fa. It also applies to master at 
41d95dd3286.

The repair now finds candidates through {{getVolumeTable().cacheIterator()}}, 
the view every request reads and the one {{listAllVolumes}} already uses, so 
deleted volumes are skipped and unflushed creates are included. Inside the 
existing volume lock it rereads the volume and resets the quota on that current 
value. The response writes the volume under {{getVolumeKey}}. Nothing changes 
when no volume write is pending, apart from the key.

{{testOldVolumeQuotaRepairKeepsUnflushedVolumeChanges}} in the existing 
{{TestOMQuotaRepairRequest}} covers an unflushed delete, bucket count and 
refCount change, quota change and create; it fails without the change 
("expected: null"). 
{{TestQuotaRepairTask.testQuotaRepairForOldVersionVolumeBucket}} now also reads 
the row from RocksDB and asserts that no bare name row exists; it fails without 
the change ("expected: -1L"). With the patch both pass, the quota repair, 
volume, tenant and bucket request suites (86 tests) pass, as does 
{{TestOzoneRepairShell}} on a mini cluster, and checkstyle is clean. With the 
patch the five bug tests of the three reproductions fail and the two controls 
pass.

Volumes already damaged are not repaired, and bare name rows written by earlier 
repairs (2.0.0 to 2.2.1) stay. Those rows are never read as a volume, because 
every lookup uses the {{/}} prefixed key; they show up only where the whole 
table is iterated, as listed above. No Ozone tool removes a single volume table 
row ({{ozone debug ldb}} is read only, and {{ozone sh volume delete}} addresses 
the {{/}} prefixed key), so removing them needs a follow-up.

Found by TLA+ model checking and code review of the OM volume, bucket and 
snapshot lifecycle paths under HDDS-15926, on commit 
20d95ed37fe35013b5320713665db03c06ee36fa. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. The attached patch is 
a proposal for review. Generated with Specula (Claude Opus 5.5).



--
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