Siyao Meng created HDDS-16794:
---------------------------------
Summary: KeyDeletingService run that outlives a lost and regained
OM leadership purges keys again and reduces bucket usage twice
Key: HDDS-16794
URL: https://issues.apache.org/jira/browse/HDDS-16794
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-2-do-not-submit-key-purge-after-term-change-during-run.patch,
TestBugMC2StaleRunDoublePurge.java
h3. Mechanism
{{KeyDeletingService.KeyDeletingTask.call}} checks {{shouldRun}} (leader ready)
and the flush of the previous purge once, at the start of a run. It then scans
{{deletedTable}}, calls SCM {{deleteKeyBlocks}} and submits {{PurgeKeys}}.
Nothing ties the request to the leadership under which the scan was made, and
Ratis accepts it whenever this OM is the leader at the time of the submit.
{{OMKeyPurgeRequest.validateAndUpdateCache}} subtracts the sizes in
{{bucketPurgeKeysSize}} from {{snapshotUsedBytes}} and
{{snapshotUsedNamespace}} whether or not the keys still exist, and deleting the
absent {{deletedTable}} rows is a no-op.
If the OM loses the leadership and gets it back while a run is between its
checks and its submit, and the leader in between has purged the same keys, the
usage is subtracted a second time on every OM.
HDDS-13760 added the flush check "so that duplicate purge requests are not
submitted". It covers a second scan on the same OM before the flush, not a run
that has already passed the check.
h3. Trigger
# OM1 is leader and a key is deleted. A KeyDeletingService run on OM1 passes
its checks and scans the key.
# The run stalls before it submits the purge, for example in the SCM call. OM1
loses the leadership, OM2 becomes leader and its own run purges the key.
# OM1 becomes leader again.
# The stalled run continues and submits its purge of the key, which is applied
in the new term.
The stall has to cover both leadership changes and the run of the other leader,
which comes on that leader's own schedule, so with the default service interval
of 60s it has to last up to about a minute. I expect this to be rare (argued,
not measured).
By history the defect is present since 2.1.0: {{ozone-2.1.0}} is the first
release tag in which the purge reduces the bucket usage (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 stale request repeats the whole batch of the stalled run, not a single
key.
* The wrong values are written to the bucket table on all OMs and nothing
corrects them automatically. The quota repair recomputes these counters when an
operator starts it (code reading, not run).
h3. Reproduction
PASS, unmodified source. [^TestBugMC2StaleRunDoublePurge.java]
({{hadoop-ozone/integration-test}}) starts a mini cluster with three OMs and
one datanode. The deleting services get a long interval and are run by hand
with {{runPeriodicalTaskNow}}, and the leadership is moved with
{{OzoneManager.transferLeadership}}. The run on the first leader is made by a
second {{KeyDeletingService}} on that OM whose SCM client delays the reply of
{{deleteKeyBlocks}}, so the run is held in the SCM call. After the sequence
above the client reads {{usedBytes = -1024}} and {{usedNamespace = -1}} for the
empty bucket, the bucket row holds -1024 pending deletion bytes in RocksDB on
all three OMs, 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.
h3. Patch
[^MC-2-do-not-submit-key-purge-after-term-change-during-run.patch], against
a6332fb59879b80615b9233e953d6813d03ae7a8. It also applies to master at
3a22acfaa21.
A run of {{KeyDeletingService}} records the Raft term right after the leader
check, before the flush check and the scan, and compares it with the current
term immediately before each submit of the run ({{PurgeKeys}} and the snapshot
property updates, for the active object store and for the snapshot tasks). If
the term is different the request is not submitted and the run ends like a run
whose purge failed. The term is read from the same Ratis division info that
{{shouldRun}} uses for leader ready. There is no proto change and nothing
changes at apply time. The keys stay in {{deletedTable}} and the next run,
which starts in the new term, purges them once (asserted in the new test).
This narrows the window, it does not close it. The comparison is not atomic
with the append to the Ratis log, so a run that stalls between the comparison
and the append for as long as described under Trigger still purges twice. An
earlier form of the reproduction, which holds the run inside the submit call
after the comparison, still passes with the patch. The stricter alternative is
a new optional field in {{PurgeKeysRequest}} with the term of the run, and a
check in {{OMKeyPurgeRequest}} that rejects the request when the term of its
log entry is different, which is exact and decided identically on every OM. It
is not proposed here because it adds a field without a layout feature gate to
an existing request, and in a ring where OMs of two versions apply the same log
an OM without the check applies a stale request that an OM with the check
rejects, so the OMs disagree.
Subtracting only for keys that are still in {{deletedTable}} was also
considered and not taken. It cannot be decided consistently at apply time,
because {{deletedTable}} has no table cache entries and the DB lags the apply
by the double buffer, so OMs could decide differently.
Covered by {{testNoPurgeAfterTermChangeDuringRun}} in the existing
{{TestKeyDeletingService}}: the term changes while the run is in the SCM call,
the run does not purge, and the next run purges the key once. It fails when the
comparison in {{KeyDeletingService.submitRequest}} is removed, because the
first run then purges the key. With the patch 173 unit tests of
{{hadoop-ozone/ozone-manager}} (deleting service, purge request, open key
cleanup, key manager, snapshot manager and quota repair suites) and 22 mini
cluster tests (key purging, OM HA with stopped nodes and the snapshot deleting
service suite) pass, checkstyle is clean. With the patch the reproduction fails
at its first assertion of the buggy outcome, because the stale run is not
submitted and the usage stays 0.
Not covered by the patch: {{DirectoryDeletingService}} submits
{{PurgeDirectories}} with the same single check at the start of a run, and
{{OMDirectoriesPurgeRequestWithFSO}} also reduces the bucket usage without a
presence check (code reading, not run). It submits from its worker threads, so
the term would have to be handed to them, and it is left out here. A term
change between two requests of one run now stops the later requests, which is
one more way into HDDS-16790 (a key purged by one request with its size in the
next) until that is fixed.
The flush check that misses a purge which is not applied yet on a new leader is
a different defect and is filed separately. Neither fix covers the other: that
one covers a run that starts after a leader change, this 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]