linliu-code commented on code in PR #20028:
URL: https://github.com/apache/hudi/pull/20028#discussion_r4128891554


##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java:
##########
@@ -618,20 +740,77 @@ synchronized ExpireLockResult 
tryExpireCurrentLock(boolean fromShutdownHook) {
   }
 
   /**
-   * Renews (heartbeats) the current lock if we are the holder, it forcefully 
set
-   * the expiration flag
-   * to false and the lock expiration time to a later time in the future.
+   * Whether storage now holds the expired version of the lock we are 
releasing, i.e. whether an
+   * earlier expire attempt that was answered with an error actually landed.
+   */
+  private boolean earlierExpireWriteLanded() {
+    Pair<LockGetResult, Option<StorageLockFile>> current = 
storageLockClient.readCurrentLockFile();

Review Comment:
   Addressed in the PR changes: the reconcile read catches runtime storage 
errors and returns false, so the existing acquired-by-others path clears the 
local lock and reports release failure. Added 
testUnlockClearsLocalLockWhenReconcileReadThrows.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java:
##########
@@ -593,6 +680,12 @@ synchronized ExpireLockResult tryExpireCurrentLock(boolean 
fromShutdownHook) {
         logWarnLockState(FAILED_TO_RELEASE, "Lock expiration write was 
throttled.");
         
hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockThrottledMetric);
         return ExpireLockResult.THROTTLED;
+      case TRANSIENT_ERROR:
+        // The storage backend rejected the write server-side (HTTP 5xx), so 
the lock file is
+        // unchanged and still ours. Retrying the same conditional write is 
safe.

Review Comment:
   Addressed in follow-up commit 3bdc6d3. After a retriable release write, a 
retry precondition failure now triggers a reread; it is treated as released 
only when the stored lock is expired and still matches this owner and lease. 
The existing failure path remains for another owner, and the reconcile-read 
exception path now clears local state and reports release failure. Added tests 
cover both landed-write reconciliation and a throwing reconcile read.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java:
##########
@@ -593,12 +698,29 @@ synchronized ExpireLockResult 
tryExpireCurrentLock(boolean fromShutdownHook) {
         logWarnLockState(FAILED_TO_RELEASE, "Lock expiration write was 
throttled.");
         
hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockThrottledMetric);
         return ExpireLockResult.THROTTLED;
+      case TRANSIENT_ERROR:
+        // HTTP 5xx. Retrying the same conditional write is safe; if this 
attempt did land despite
+        // the error, the retry's precondition failure is reconciled in the 
ACQUIRED_BY_OTHERS case.
+        logWarnLockState(FAILED_TO_RELEASE, "Lock expiration write hit a 
retriable server-side error.");
+        
hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockTransientErrorMetric);
+        return ExpireLockResult.TRANSIENT_ERROR;
       case SUCCESS:
         logInfoLockState(RELEASED);
         recordAuditOperation(AuditOperationState.END, lockExpirationTimeMs);
         setLock(null);
         return ExpireLockResult.SUCCESS;
       case ACQUIRED_BY_OTHERS:
+        if (afterRetriableAttempt && earlierExpireWriteLanded()) {

Review Comment:
   I am preserving failure behavior when another owner holds the lock, per the 
requested correctness constraint. A foreign owner is not sufficient proof of 
our earlier expire write; reconciliation therefore accepts only our own expired 
lock and lets the existing failure path handle a different owner.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java:
##########
@@ -618,20 +711,79 @@ synchronized ExpireLockResult 
tryExpireCurrentLock(boolean fromShutdownHook) {
   }
 
   /**
-   * Renews (heartbeats) the current lock if we are the holder, it forcefully 
set
-   * the expiration flag
-   * to false and the lock expiration time to a later time in the future.
+   * Renews (heartbeats) the current lock if we are the holder.
+   *
+   * <p>On a retriable storage rejection (throttling or a 5xx) this makes one 
bounded extra
+   * attempt within the same heartbeat cycle before giving up and leaving the 
rest to the next
+   * cycle. The backoff sleep happens outside the provider monitor so the 
heartbeat thread does
+   * not hold it while waiting, which would stall {@link #unlock()} and {@link 
#tryLock()}.
    *
-   * @return True if we successfully renewed the lock, false if not.
+   * @return True if the lock lease is still believed to be ours, false if 
renewal should stop.
+   */
+  @VisibleForTesting
+  protected boolean renewLock() {
+    for (int attempt = 0; attempt < RENEW_MAX_RETRIES; attempt++) {
+      RenewAttemptOutcome outcome = renewLockOnce();
+      if (outcome.result != RenewAttemptOutcome.Result.RETRIABLE_FAILURE) {
+        return outcome.renewed;
+      }
+      long delaySeconds = THROTTLE_INITIAL_RETRY_DELAY_SECONDS;
+      logger.warn("Owner {}: Lock renewal hit a retriable storage error (retry 
{}/{}), "
+          + "backing off for {} seconds.", ownerId, attempt + 1, 
RENEW_MAX_RETRIES, delaySeconds);
+      try {
+        sleepForThrottleRetry(delaySeconds);
+      } catch (InterruptedException ie) {
+        // The heartbeat thread was asked to stop. Keep the lease (we still 
believe it is ours)
+        // and let the shutdown path expire the lock.
+        Thread.currentThread().interrupt();
+        logger.warn("Owner {}: Interrupted while backing off before retrying 
lock renewal.", ownerId, ie);
+        
hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockInterruptedMetric);
+        return true;
+      }
+    }
+    // Budget exhausted within this cycle; the lease is untouched, so let the 
next heartbeat try.
+    return renewLockOnce().renewed;
+  }
+
+  /**
+   * Outcome of a single {@link #renewLockOnce()} pass: whether the caller 
should keep the

Review Comment:
   I kept the acquire and renew outcome types separate. Acquire distinguishes 
acquired, ordinary non-acquisition, and retriable failure; renewal 
distinguishes keep-heartbeat, stop-heartbeat, and retry. A shared type would 
make the call sites less clear, and the later review agreed with keeping them 
separate.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java:
##########
@@ -79,7 +79,9 @@ public class StorageBasedLockProvider implements 
LockProvider<StorageLockFile> {
   // However, since our lock leases are pretty long, we can use a high buffer.
   private static final long CLOCK_DRIFT_BUFFER_MS = 500;
 
-  // Max number of retry attempts on the lock-expire write after a THROTTLED 
response.
+  // Max number of retry attempts on the lock-expire write after a retriable 
response
+  // (THROTTLED or TRANSIENT_ERROR). Kept under the historical name because 
the release path's
+  // budget has not changed.

Review Comment:
   Addressed in the PR changes: retry budgets and backoff helpers now use 
neutral names because they cover all retriable outcomes.



-- 
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