Siyao Meng created HDDS-16795:
---------------------------------

             Summary: KeyDeletingService can purge keys twice after a leader 
change to an OM still applying a backlog, or after an OM restart
                 Key: HDDS-16795
                 URL: https://issues.apache.org/jira/browse/HDDS-16795
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-1-wait-for-flush-of-earlier-terms-before-aos-purge.patch, 
TestBugMC1PurgeRepeatedAfterRestart.java

h3. Mechanism
Before a run on the active object store, {{KeyDeletingService}} and 
{{DirectoryDeletingService}} call 
{{AbstractKeyDeletingService.isPreviousPurgeTransactionFlushed}} (HDDS-13760). 
It compares the last purge transaction, kept in memory in 
{{DeletingServiceMetrics}}, with the transaction info flushed to the DB. The 
in-memory value is set when this OM process applies a purge. After a restart it 
is empty, and on an OM that has just become leader it does not include a purge 
that is committed in the Ratis log but not applied yet.

The OM reports leader ready as soon as the Ratis state machine updater has 
passed the first entry of the new term. The updater hands entries to the single 
OM executor and does not wait for them, so earlier entries, including a 
{{PurgeKeys}}, can still be queued at that time. A run that starts then passes 
{{shouldRun}} and the flush check, finds the same keys in {{deletedTable}} and 
submits a second {{PurgeKeys}}. {{OMKeyPurgeRequest}} subtracts the sizes in 
{{bucketPurgeKeysSize}} from {{snapshotUsedBytes}} and 
{{snapshotUsedNamespace}} for both requests. Deleting the already deleted rows 
is a no-op and nothing rejects the second request.

h3. Trigger
# A key is deleted and the delete is flushed.
# A KeyDeletingService run submits {{PurgeKeys}} for it. The request is 
committed in the Ratis log.
# Before the purge is applied on another OM that OM becomes leader, or before 
the purge is flushed the OM restarts.
# The (new) leader reports leader ready while the purge is still waiting behind 
earlier entries in its executor.
# A KeyDeletingService run starts in that window and purges the key again.

The window is short, from leader ready until the executor has caught up, which 
is bounded by {{ozone.om.unflushed.transaction.max.count}} entries. With the 
default service interval of 60s a run rarely starts inside it. I expect the 
leader change onto an OM that is still applying a backlog (for example during a 
rolling restart under load) to be the realistic form, and the restart of a 
single OM to be practically unreachable at defaults, because its first run 
comes before leader ready and the next one 60s later (both argued, not run). 
The reproduction uses a restart and keeps the replay waiting.

By history the defect is present since 2.1.0: {{ozone-2.1.0}} is the first 
release tag that has both the flush check (HDDS-13760) and the usage accounting 
in the purge (HDDS-13764). Not run on older code.

h3. Impact
* The pending deletion bytes and namespace of the bucket become too low, 
negative for a bucket with nothing else pending. 
{{OmBucketInfo.getTotalBucketSpace}} and {{getTotalBucketNamespace}} include 
them without a lower bound, so clients see wrong (in the reproduction negative) 
used bytes and namespace, and the quota checks in {{OMKeyRequest}} admit more 
than the quota. In the reproduction a bucket with a namespace quota of 1 
accepted two keys. The space quota check reads the same total (code reading, 
not run).
* The second request repeats the whole batch of the run, not a single key.
* The wrong values are flushed to the bucket table and replicated, and nothing 
corrects them automatically. The quota repair recomputes these counters when an 
operator starts it (code reading, not run).
* {{DirectoryDeletingService}} uses the same check and 
{{OMDirectoriesPurgeRequestWithFSO}} also reduces the bucket usage without a 
presence check, so it has the same exposure (code reading, not run).

h3. Reproduction
PASS, unmodified source. [^TestBugMC1PurgeRepeatedAfterRestart.java] 
({{hadoop-ozone/ozone-manager}}) runs one real OM with a recording SCM block 
client and no datanodes. The KeyDeletingService gets a long interval and is run 
by hand. A key is deleted and flushed, then the double buffer is paused, a key 
is created in a second bucket and the service purges the deleted key, and the 
OM is stopped. A new {{OzoneManager}} is created on the same storage while the 
test holds the lock of the second bucket, so the replayed key create and the 
replayed purge behind it wait while the OM is already ready leader. The service 
run in that state sends the blocks to SCM again and submits a second purge. 
Afterwards the bucket row has {{snapshotUsedBytes}} of minus three times the 
key size and {{snapshotUsedNamespace = -1}} in RocksDB, the client reads the 
same negative usage, and with a namespace quota of 1 the bucket accepts two 
keys and rejects the third with {{QUOTA_EXCEEDED}}. A passing test means the 
defect is present.

Note that {{OzoneManager.restart}} on the same object keeps the in-memory value 
and therefore does not show the problem.

h3. Patch
[^MC-1-wait-for-flush-of-earlier-terms-before-aos-purge.patch], against 
a6332fb59879b80615b9233e953d6813d03ae7a8. It also applies to master at 
3a22acfaa21.

{{isPreviousPurgeTransactionFlushed}} now also returns false while the last 
applied term index of the OM state machine is from an earlier term than the 
current term of the Ratis server. In OM that index is advanced by the double 
buffer after a flush, so once it is in the current term everything committed 
before this OM became leader is applied and flushed, and the scan sees it. 
After that the existing in-memory value covers the purges of the current term. 
A new leader with nothing to do is not held back: the first entry of every term 
is a Ratis configuration entry, and the state machine moves the last applied 
index onto it as soon as everything before it is flushed, so the check opens 
without any OM transaction in the new term. No new state is added, and both 
services are covered for the active object store. A run that starts in the 
window is skipped and logged at INFO.

Subtracting only for keys that are still in {{deletedTable}}, or flooring the 
counters at 0, was considered and not taken: {{deletedTable}} has no table 
cache entries and the DB lags the apply by the double buffer, so an earlier 
purge that is not flushed would still look present, and OMs could decide 
differently.

Covered by {{testNoPurgeUntilReplayedPurgeIsFlushed}} in a new nested class of 
the existing {{TestKeyDeletingService}}. It asserts that the check is closed 
for both services while the replayed purge is not applied, and that the key is 
then purged once. Without the change to {{AbstractKeyDeletingService}} it fails 
at the first of these assertions, because the check is open. {{OmTestManagers}} 
gets a small static helper so that a test can create a second OM on the same 
storage. With the patch 131 unit tests of {{hadoop-ozone/ozone-manager}} 
(deleting service, purge request, key manager, snapshot manager and quota 
repair suites) and 31 mini cluster tests (key purging, OM HA with stopped 
nodes, snapshot and directory deleting service suites) pass, checkstyle is 
clean. With the patch the reproduction fails at its first assertion of the 
buggy outcome (SCM gets the blocks of the key once, not twice), because the run 
after the restart is skipped.

Not covered by the patch, on purpose: the snapshot tasks of the two services 
use a different check, {{OmSnapshotManager.areSnapshotChangesFlushedToDB}}, 
which reads the last transaction recorded in the snapshot info when a purge is 
applied. It has the same weakness, because on a new leader a snapshot purge 
that is committed but not applied is not recorded yet (code reading, not run). 
That check needs its own change and test and is left for a follow up.

A run that has already passed the check when the leadership moves away and 
comes back is a different defect, filed separately as HDDS-16794. Neither fix 
covers the other: this patch covers a run that starts after the leader change, 
the other one a run that started before it.

Found by TLA+ model checking and code review of the OM key deletion service and 
key purge paths under HDDS-15926, on commit 
a6332fb59879b80615b9233e953d6813d03ae7a8. 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