Siyao Meng created HDDS-17404:
---------------------------------

             Summary: LeaseManager monitor can expire a lease that was 
re-acquired or renewed after it found the old deadline passed
                 Key: HDDS-17404
                 URL: https://issues.apache.org/jira/browse/HDDS-17404
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-1-expire-only-the-timed-out-lease-atomically-with-renew.patch, 
TestBugMC1ExpiryRemovesReacquiredLease.java, TestBugMC2RenewLostToExpiry.java

h3. Mechanism
{{LeaseManager.LeaseMonitor.run}} decides expiry without the manager lock: it 
calls {{get(resource)}}, {{Lease.getRemainingTime}} and {{Lease.getCallback}}, 
and only then takes the lock in {{release(resource)}}. {{release}} removes 
whatever lease is stored under that resource at that moment and invalidates it. 
It never checks that this is the lease the monitor judged, or that its deadline 
is still in the past.

{{Lease.renew}} takes no lock at all. It checks the non volatile {{expired}} 
flag and adds to {{leaseTimeout}}. The monitor does not read the timeout again 
before {{release}}.

So two things can change between the monitor's decision and its commit:
* the lease is released and the same resource is acquired again, so {{release}} 
removes the new lease;
* the lease is renewed, so {{release}} removes a lease whose new deadline is in 
the future.

h3. Trigger
Any caller of {{LeaseManager}} that either releases and re-acquires the same 
resource, or renews a lease, close to the moment the monitor finds the old 
deadline passed. The window runs from the monitor's unlocked read until it gets 
the manager lock, so other callers doing {{acquire}} or {{release}} on the same 
manager make it wider.

In tree, no production code does either at this commit. 
{{CloseContainerEventHandler}} only acquires; its leases are released only by 
the monitor and by {{shutdown}}. {{EventWatcher}} releases and re-acquires by 
event ID, but its only main subclass, {{RetriableDatanodeEventWatcher}}, is not 
instantiated in production code at this commit. Nothing outside tests calls 
{{Lease.renew}}. The defect is in the public API of {{hdds-server-framework}} 
and would affect any future caller of this kind.

h3. Impact
Reproduced on unmodified source through the public API:
* *Re-acquired lease removed early.* A lease that is released and acquired 
again while the monitor is expiring the old one is removed and invalidated a 
few milliseconds after it was acquired, instead of at its 1000 ms deadline. 
{{get}} throws {{LeaseNotFoundException}}, {{renew}} throws 
{{LeaseExpiredException}}, the callback of the lease that was explicitly 
released runs once, and the new lease's own callback never runs. 
[^TestBugMC1ExpiryRemovesReacquiredLease.java]
* *Successful renew dropped.* {{renew(60000)}} returns normally, the lifetime 
reads 60100 ms, and the monitor then invalidates the lease and runs its expiry 
callback; a second {{acquire}} of the same resource succeeds. 
[^TestBugMC2RenewLostToExpiry.java]

A lost renew always lands after the monitor found the lease timed out. It 
returns normally, although the lease is then expired anyway.

h3. Reproduction
The two attached classes are self contained JUnit tests for 
{{hadoop-hdds/framework}} (target path and Maven command are in each class 
comment). They pass on unmodified source when the bug is present. Each holds 
the manager's own monitor, the lock that {{acquire}} and {{release}} already 
take, while the monitor thread is blocked on it to release the timed out lease, 
and does the release and re-acquire, or the renew, meanwhile. This only fixes 
an order that can also happen without help, so the tests show that the window 
exists, not how often it is hit. No state is injected and no code under test is 
changed.

h3. Patch
[^MC-1-expire-only-the-timed-out-lease-atomically-with-renew.patch], against 
f804bf26867246067017fdd9e5319c2cbdbd5db8. It also applies to master at 
a7da4f70195.

The monitor now commits through a new private {{LeaseManager.expire(resource, 
lease)}}, which holds the manager lock and the lease's lock. It releases the 
lease only if it is still the active lease for the resource, compared by 
identity since {{Lease.equals}} compares only the resource, and its remaining 
time is still not positive. Otherwise the monitor runs no callback and scans 
again at once, so a renewed lease still expires at its new deadline. 
{{Lease.renew}} is {{synchronized}}, so it either completes before the 
monitor's check or fails after the invalidation. A renew that runs after the 
deadline but before the monitor removes the lease still extends it, as it 
already does today when it runs before the monitor's check; making it throw 
would break renews that work today. {{Lease.expired}} is {{volatile}}, so 
{{hasExpired}} and {{renew}} see an invalidation done by another thread. 
{{acquire}}, {{release}} and {{shutdown}} are unchanged, and callbacks still 
run on the executor, outside any lock.

Covered by two new cases in {{TestLeaseManager}}: 
{{testExpiryKeepsLeaseReacquiredMeanwhile}} and 
{{testRenewDuringExpiryKeepsLease}}. The first also checks that the released 
lease's callback has not run once a later lease on the same resource has 
expired; the second checks that the renewed lease still expires at its new 
deadline with exactly one callback. Without the change both fail ("lease 
acquired after the release" and "lease renewed before it was expired": expected 
false but was true). With it {{TestLeaseManager}}, {{TestEventWatcher}}, 
{{TestCloseContainerEventHandler}} and {{TestBlockManager}} pass (32 tests), 
checkstyle is clean, and both attached reproductions no longer show the bug.

Found by TLA+ model checking and code review of the HDDS LeaseManager expiry 
and shutdown paths under HDDS-15926, on commit 
f804bf26867246067017fdd9e5319c2cbdbd5db8. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. Related but not 
duplicates: HDDS-8418 (removed an acquire variant that attached the callback 
after acquire, a different race) and HDDS-10899 (callback refactoring). The 
attached patch is a proposal for review. Generated with Specula (Claude Opus 
5.5).



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to