Skip to content

fix(pd): make the TTL cleanup delete the expired key under its marker - #3382

Open
bitflicker64 wants to merge 2 commits into
apache:masterfrom
hugegraph:task/pd-ttl-cleanup
Open

bitflicker64 wants to merge 2 commits into
apache:masterfrom
hugegraph:task/pd-ttl-cleanup

Conversation

@bitflicker64

@bitflicker64 bitflicker64 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Purpose of the PR

KvService.clearTTLData runs every second on the PD leader and should delete each key whose TTL
marker K@T@<expireAt>@<key> has expired. It took the key from the marker at the delimiter after
T, so it deleted <expireAt>@<key>, a name that never exists, and then removed the marker. A
read hides an expired value and removes it when it finds one, but a value that nobody reads again
stayed in the store for good, with no marker left that would ever delete it. #3378 has the
measurement on master.

The parse fix alone would make the cleanup delete a key that a writer is renewing at that moment,
which the review of #3376 asked to prevent: the cleanup's delete should be bound to the expired
marker. This PR carries the parse fix and that guard without the txn RPC of #3376, so the guard
is a check before the delete, not one atomic raft entry. Once that RPC lands, the check and the
delete can become one guarded txn; its two compares are the two checks below.

This PR is limited to the TTL cleanup's parse and its guard; a bug that already exists on master in
code this PR did not write is tracked as an issue, not fixed here.

Main Changes

Two commits, two files, 59 production lines (56 added, 3 removed) and 131 test lines. The second
commit answers the review: a marker that is not due yet is left alone.

clearTTLData parses the key after the marker's time, reads the key, and hands both to the new
KvService.removeExpired(key, marker, stored), which deletes the key and the marker only while the
key still holds the value the marker names:

  • The marker's time has not passed: the value and the marker are left alone, since a read still
    returns the value. The scan compares bytes, so a time with more digits than now can sort below
    it; today a TTL of 10^13 ms is selected right after the put.
  • The value is absent (a read removed it and left the marker behind, point 4 of [Bug] PD TTL cleanup deletes the marker-shaped name instead of the expired key, so expired values stay in the store until read #3378): only the
    marker goes.
  • The stored value does not carry the marker's expiry: it was written after the marker, with
    another TTL (putTTL, keepAlive) or none (a plain put), so it is kept and the stale marker
    goes.
  • Right before the delete, the key no longer holds the bytes the cleanup read: a renewal landed
    since, so the value is kept and the stale marker goes. The bytes include the expiry, so this
    catches a renewal of the identical string.
  • Otherwise the key and the marker are deleted.

The check and the delete are two store operations, so a renewal that lands in between, after the
cleanup compared the bytes and before its two removes, is still deleted. That is the window the
txn of #3376 closes.

Verifying these changes

  • Trivial rework / code cleanup without any test coverage. (No Need)
  • Already covered by existing tests, such as (please modify tests here).
  • Need tests and can be verified as follows:
    • KvServiceTest (hg-pd-test, pd-core-test suite), seven new tests:
      testTtlCleanupDeletesTheExpiredKey is the issue's measurement, it fails on master's parse
      and passes here; testTtlCleanupKeepsAValueRenewedBeforeItsRead,
      testTtlCleanupKeepsAValueRenewedAfterItsRead,
      testTtlCleanupKeepsALeaseAKeepAliveRenewedAfterItsRead and
      testTtlCleanupKeepsAPlainPutOverATtlValue are the interleavings;
      testTtlCleanupDropsTheMarkerOfARemovedValue is the absent value;
      testTtlCleanupKeepsAValueThatHasNotExpired is a 10^13 ms TTL whose marker the scan picks.
    • Negative controls, the class run once with one guard removed (local, JDK 17, 2026-10-21):
      master's parse fails testTtlCleanupDeletesTheExpiredKey; without the expiry check,
      testTtlCleanupKeepsAPlainPutOverATtlValue and testTtlCleanupKeepsAValueRenewedBeforeItsRead
      fail; without the byte compare, testTtlCleanupKeepsAValueRenewedAfterItsRead and
      testTtlCleanupKeepsALeaseAKeepAliveRenewedAfterItsRead fail; without the due check,
      testTtlCleanupKeepsAValueThatHasNotExpired fails. All nine pass with every guard.
    • Suites on Pune101 (JDK 17, 2026-10-21, under the shared lock) at 37ca255: package and
      editorconfig:check clean, pd-common 83/0/0/0, pd-core 197/0/0/2 (190 on master, the 7
      new tests), pd-client 83/0/0/0 against a packaged PD (tests, failures, errors, skipped).

Does this PR potentially affect the following parts?

  • Dependencies (add/update license info & regenerate_known_dependencies.sh)
  • Modify configurations
  • The public API
  • Other affects (typed here): keys written with a TTL (putTTL, PD locks, every
    KvService.put(key, value, ttl) caller) now leave the store within a second of their expiry,
    as they were meant to. Before, an expired value stayed until a read found it; no read served it
    as live either way.
  • Nope

Documentation Status

Select one option and provide the documentation location when applicable.

  • Doc - TODO: required documentation is pending; complete it before merging.
  • Doc - Done: documentation is included here or linked below.
  • Doc - No Need: no user-visible documentation is affected.

Documentation files in this PR or paired hugegraph-doc PR:

clearTTLData took the key from the marker K@T@<time>@<key> at the
delimiter after T, so it deleted "<time>@<key>", a name that never
exists, and then removed the marker: an expired value that nobody read
again stayed in the store for good.

The cleanup now parses the key after the time, and deletes it only
while it still holds the value the marker names: the stored value must
carry the marker's expiry, and right before the delete the key must
still hold the bytes the cleanup read. A renewal with another TTL, a
plain put without one, or a keepAlive keeps its value; only the stale
marker goes. The check and the delete are two store operations; one
atomic delete needs the txn RPC of apache#3376.

Fixes apache#3378
@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.94%. Comparing base (c2ec570) to head (75468db).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
...c/main/java/org/apache/hugegraph/pd/KvService.java 84.61% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3382      +/-   ##
============================================
+ Coverage     55.93%   55.94%   +0.01%     
- Complexity     2277     2318      +41     
============================================
  Files           774      776       +2     
  Lines         68534    68888     +354     
  Branches       9333     9417      +84     
============================================
+ Hits          38334    38542     +208     
- Misses        26634    26760     +126     
- Partials       3566     3586      +20     
Flag Coverage Δ *Carryforward flag
commons 96.37% <ø> (ø) Carriedforward from c2ec570
hstore 36.10% <ø> (+0.29%) ⬆️
pd 49.63% <84.61%> (+0.45%) ⬆️
server_hbase 41.84% <ø> (-0.06%) ⬇️ Carriedforward from c2ec570
server_memory 54.29% <ø> (-0.04%) ⬇️ Carriedforward from c2ec570
server_rocksdb 42.29% <ø> (-0.06%) ⬇️ Carriedforward from c2ec570
store 31.93% <ø> (ø)

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: yes. Summary: The parse fix and the expiry guard are correct for every TTL a marker can sort under today, but the cleanup now deletes a live value whenever its marker falls inside the scan range without having expired, which the byte-wise range lets happen for large TTLs. Evidence: read the full diff at 75468db and KvService, MetadataRocksDBStore, HgKVStoreImpl.scanRange (RocksDB iterator bounds, default byte-wise comparator), TaskScheduleService and the putTTL/keepAlive RPCs; computed marker membership in [K@T@0@, K@T@@) for several TTLs; all non-skipped checks pass at this head.

The cleanup selects markers with a byte-wise range up to K@T@<now>@, so
a time with more digits than now can sort below it: today a TTL of
10^13 ms is selected right after the put. With the parse fixed, the
cleanup then deleted a value that a read still returns.

removeExpired now leaves a marker whose time has not passed alone, with
its value, using the expiry test of getValue. The interleaving tests
pass a marker that is due, as the scan does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] PD TTL cleanup deletes the marker-shaped name instead of the expired key, so expired values stay in the store until read

1 participant