[ 
https://issues.apache.org/jira/browse/HDDS-17404?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-17404:
------------------------------
    Attachment: TestBugMC2RenewLostToExpiry.java

> 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
>    Affects Versions: 0.2.1, 0.3.0, 0.4.0, 0.4.1, 0.5.0, 1.0.0, 1.1.0, 1.2.0, 
> 1.3.0, 1.2.1, 1.4.0, 2.0.0, 1.4.1, 2.1.0, 2.2.0, 2.1.1, 2.1.2, 2.2.1
>            Reporter: Siyao Meng
>            Priority: Minor
>         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