nsivabalan opened a new pull request, #20028:
URL: https://github.com/apache/hudi/pull/20028

   ### Describe the issue this Pull Request addresses
   
   Closes #20027.
   
   `StorageBasedLockProvider` leaves a dangling lock file when the storage 
backend returns an HTTP 5xx on the lock-expire write. Observed in production: a 
writer acquired the lock, completed everything inside the lock, then hit `503 
SERVICE_UNAVAILABLE` on release. The lock file was left un-expired, and ~30s 
later a second writer detected the dangling lock and alerted.
   
   This is the same class of bug as #18438 / #18439, which fixed it for HTTP 
429 only. 5xx was never covered.
   
   **Root cause.** All three storage lock clients classify HTTP 5xx as 
`UNKNOWN_ERROR` — the 5xx branch logs a warning and falls through to the 
`UNKNOWN_ERROR` return. `tryExpireCurrentLock` maps that to 
`ExpireLockResult.FAILED`, and the retry loop added in #18439 only engages 
while the result is `THROTTLED`:
   
   ```java
   for (int attempt = 1; attempt <= THROTTLE_MAX_RETRIES && expireResult == 
ExpireLockResult.THROTTLED; attempt++) {
   ```
   
   So a 503 fails the release on the **first** attempt, with no retry. The 
cloud SDKs cannot cover for this either: both clients are deliberately built 
with retries disabled (`S3` `retryStrategy(r -> r.maxAttempts(1))`, `GCS` 
`RetrySettings.setMaxAttempts(1)`), so a single transient blip dangles the lock 
every time.
   
   **Why retrying is safe.** The expire write is conditional and pinned to the 
*same* precondition token on every attempt (`getLock()` is only cleared on 
SUCCESS). If the write never landed, the precondition still matches and the 
retry succeeds. If it silently did land, the token no longer matches, yielding 
`412` -> `ACQUIRED_BY_OTHERS`, which is already handled as released. Either way 
the retry converges — the same argument that made the #18439 throttle retry 
safe.
   
   ### Summary and Changelog
   
   Users running `StorageBasedLockProvider` on S3, GCS or Azure will no longer 
get a dangling lock from a single transient 5xx during lock release.
   
   - Added `LockUpsertResult.TRANSIENT_ERROR`, returned for HTTP 5xx by 
`S3StorageLockClient`, `GCSStorageLockClient` and `AzureStorageLockClient`. 
`409`, client-side timeouts and other 4xx keep their existing `UNKNOWN_ERROR` 
behavior.
   - `unlock()`: generalized the existing retry loop from THROTTLED-only to any 
retriable outcome via `ExpireLockResult.isRetriable()`, keeping the 3-retry 
1s/2s/4s budget and the lock-identity guard against a concurrent `tryLock()`.
   - `tryLock()`: split into a `synchronized tryLockInternal()` plus a wrapper 
that retries once, with the backoff sleep **outside** the provider monitor so a 
waiting acquirer never blocks `unlock()` or the heartbeat. `UNKNOWN_ERROR` is 
deliberately **not** retried here — the write may have landed, and an identical 
retry could act on a lock we cannot reason about.
   - `renewLock()`: same split into `renewLockOnce()` plus a bounded in-cycle 
retry, sized to stay well inside one heartbeat interval.
   - Added a dedicated `lock.transient.error` metric so an unavailable backend 
is distinguishable from a rate-limited one and from a genuinely unknown lock 
state, plus a `TRANSIENT_RETRIES_EXHAUSTED` cause on the `FAILED_TO_RELEASE` 
exception.
   
   Retry budgets differ by path on purpose: a failed `tryLock()` is safe 
(callers retry higher up, and another writer can take the lock), whereas a 
failed release dangles the lock until its lease elapses.
   
   No code was copied from elsewhere.
   
   ### Impact
   
   Reduces dangling locks for `StorageBasedLockProvider` on S3, GCS and Azure 
when storage returns a transient 5xx. No behavior change for 429 (already 
handled), 409, other 4xx, or client-side timeouts. No config change and no 
public API change; `LockUpsertResult` gains an enum value.
   
   `tryLock()` and `renewLock()` are no longer declared `synchronized` — all 
state mutation still happens inside the synchronized single-attempt bodies, and 
only the backoff sleep sits outside the monitor. Reentrancy is unchanged and 
still covered by existing tests.
   
   ### Risk Level
   
   Medium.
   
   Verification:
   - `TestStorageBasedLockProvider` **56/56**: 503-on-release retry, retry 
exhaustion asserting the 1s/2s/4s schedule and the exhausted-transient cause, 
the lock-replaced-during-retry-sleep concurrency guard for the transient path, 
acquire-path retry, plus negative tests proving contention and `UNKNOWN_ERROR` 
are **not** retried.
   - `TestHoodieLockMetrics` **14/14**.
   - `TestS3StorageLockClient` **20/20**, `TestGCSStorageLockClient` **20/20**, 
`TestAzureStorageLockClient` **31/31** — each with a parameterized 
500/502/503/504 case so the whole 5xx range is covered, not just the 503 seen 
in production.
   - Checkstyle clean on all four touched modules. Sleeps are stubbed, so the 
suites run in seconds.
   
   ### Documentation Update
   
   None required — no new configs and no user-facing config change. One new 
metric (`lock.transient.error`) is emitted alongside the existing lock metrics.
   
   ### Contributor's checklist
   
   - [x] Read through [contributor's 
guide](https://hudi.apache.org/contribute/how-to-contribute)
   - [x] Enough context is provided in the sections above
   - [x] Adequate tests were added if applicable
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to