Siyao Meng created HDDS-16790:
---------------------------------
Summary: KeyDeletingService can purge a deleted key in one
PurgeKeys request and carry its size in another, leaving the size in the bucket
usage for good
Key: HDDS-16790
URL: https://issues.apache.org/jira/browse/HDDS-16790
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-3-keep-deleted-key-and-its-size-in-one-purge-request.patch,
TestBugMC3SplitPurgeLeaksBucketUsage.java
h3. Mechanism
{{KeyDeletingService.submitPurgeKeysRequest}} builds PurgeKeys requests from
block groups, one per version of a {{deletedTable}} entry. For every block
group it adds the name of the entry to the request, but it adds the size to
{{bucketPurgeKeysSize}} only for a committed version. After every block group
it compares the request size with 90% of
{{ozone.om.ratis.log.appender.queue.byte-limit}} and, when the limit is
reached, submits the request and starts a new one. An entry that holds a
committed version and a group of uncommitted blocks can therefore be named
without its size in one request and with its size in the next.
{{OMKeyPurgeRequest}} and {{OMKeyPurgeResponse}} delete the entry by name and
subtract from {{snapshotUsedBytes}} and {{snapshotUsedNamespace}} of the bucket
only what the same request carries. If the first of the two requests is applied
and the second is not, the entry is gone and nothing subtracts its size any
more.
The size based split (HDDS-13517) and the per bucket size in the request
(HDDS-13764) are both first contained in release 2.1.0 (by history, not run on
older code).
h3. Trigger
# A key is overwritten by a commit that leaves an allocated block uncommitted,
so its {{deletedTable}} entry has the old version and a group of uncommitted
blocks.
# One run of the service collects more than fits into one request. At default
limits (32MB, {{ozone.key.deleting.limit.per.task}} 50000) that needs
{{deletedTable}} keys of about 600 bytes on average, which means key names or
paths of about that length (the average is over every entry of the run,
whatever the bucket layout). It needs less when the per task limit is raised or
the Ratis limit is lowered.
# The cut falls between the two block groups of that entry, with the
uncommitted blocks first. The two groups are close to each other in iteration
order, so only the entries right at a cut are exposed. Which of the two comes
first is about even.
# The next request of the run is not applied.
For step 4 I ran this case: a snapshot of any bucket is created between the two
submits. {{OMKeyPurgeRequest}} then rejects every later request of that run
with {{INVALID_REQUEST}}, because the expected previous snapshot has changed.
No fault is involved. Not run by me: an OM stop or a leader change between the
two submits (argued, the state after the first request is the same). Also
argued, not run: the request that carries the size fails with a
{{ServiceException}}, which the service catches before it submits the next
request, and the request that only names the entry is applied.
h3. Impact
* The bucket keeps the size of the purged entry in {{snapshotUsedBytes}} and
one key in {{snapshotUsedNamespace}}. In the reproduction the bucket is empty,
its deleted table is empty, and both OM and the client still report 805306368
bytes and 1 key in use.
* The leaked amount counts against the quota. With a namespace quota of 1 the
first key written to the empty bucket is rejected with {{QUOTA_EXCEEDED}}.
* Nothing recomputes the counters on its own. A quota repair started by an
admin does (by code reading, not run).
* No data is lost and the blocks are deleted as intended. One incident leaks
the entries that sit at a cut, typically one.
h3. Reproduction
PASS on unmodified source. [^TestBugMC3SplitPurgeLeaksBucketUsage.java]
({{hadoop-ozone/ozone-manager}}) drives one real OM through
{{OzoneManagerProtocol}}, without datanodes. It overwrites 20 keys so that each
leaves one uncommitted block, runs {{KeyDeletingService}} once, and creates a
snapshot of an unrelated bucket through the client API right after the first
applied request that names an entry without its size. It then shows that the
later request for that entry is rejected with {{INVALID_REQUEST}}, that the
deleted table is empty, and the leaked usage and the {{QUOTA_EXCEEDED}} error
described above. To keep the test small the Ratis limit is 64KB and the key
names are 59000 characters long, so every block group fills its own request. A
split at default limits (about 30MB of names in one run) was not run. A passing
test means the defect is present.
h3. Patch
[^MC-3-keep-deleted-key-and-its-size-in-one-purge-request.patch], against
a6332fb59879b80615b9233e953d6813d03ae7a8. It also applies to master at
3a22acfaa21.
{{KeyDeletingService.submitPurgeKeysRequest}} groups the block groups by
deleted key name and adds one {{deletedTable}} entry per step, its name once
and the sizes of all its committed versions together. The size check stays
where it is, so a request can only be cut between entries. An entry that alone
exceeds the limit is sent in a request of its own, as before. The request
format and the way it is applied do not change. One visible side effect:
{{NumKeysPurged}} is counted from the names in the request, so an entry with
two reclaimable versions now counts once instead of twice.
{{NumKeysSentForPurge}} and the deleted key count of the service still count
versions, so the two no longer agree for such entries.
Covered by {{testPurgeKeysRequestKeepsKeyVersionsTogether}} in the existing
{{TestKeyDeletingService}}, which also pins that an entry that is rewritten
instead of purged carries its size in the request that updates it. Without the
change it fails because the overwritten entry is spread over two requests (four
requests instead of three). With it {{TestKeyDeletingService}},
{{TestOMKeyPurgeRequestAndResponse}} and {{TestKeyManagerImpl}} pass (59 tests)
and checkstyle is clean. {{TestKeyPurging}} and
{{TestSnapshotDeletingServiceIntegrationTest}} on a mini cluster (9 tests) pass
in a run that had this patch and a second, unrelated change to
{{OMKeyPurgeRequest}} applied together. With the patch the reproduction fails
at its first assertion, because no request names an entry without its size any
more. Buckets that already leaked are not repaired by the patch.
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. Related but not
duplicates: HDDS-13517 (introduced the size based split of PurgeKeys requests),
HDDS-13764 (introduced the per bucket size in the request), HDDS-16184 (quota
repair of the same counters). 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]