{"record":{"id":"09c58fb57bef86f4","repo":"apache/iceberg","slug":"failed-to-release-lock-for-path","errorCode":null,"errorMessage":"Failed to release lock for path: {}","messagePattern":"Failed to release lock for path: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java","lineNumber":242,"sourceCode":"    public boolean isHeld() {\n      return isHeld(sharedCount.getVersionedValue());\n    }\n\n    private static boolean isHeld(VersionedValue<Integer> versionedValue) {\n      try {\n        return versionedValue.getValue() == LOCKED;\n      } catch (Exception e) {\n        throw new RuntimeException(\"Failed to check Zookeeper lock status\", e);\n      }\n    }\n\n    @Override\n    public void unlock() {\n      try {\n        sharedCount.setCount(UNLOCKED);\n        LOG.debug(\"Released lock for path: {}\", lockPath);\n      } catch (Exception e) {\n        LOG.warn(\"Failed to release lock for path: {}\", lockPath, e);\n        throw new RuntimeException(\"Failed to release lock\", e);\n      }\n    }\n  }\n\n  @VisibleForTesting\n  RetryPolicy createRetryPolicy() {\n    ZKRetryPolicies effectivePolicy =\n        (retryPolicy == null) ? ZKRetryPolicies.EXPONENTIAL_BACKOFF : retryPolicy;\n\n    switch (effectivePolicy) {\n      case ONE_TIME:\n        return new RetryOneTime(baseSleepTimeMs);\n\n      case N_TIME:\n        return new RetryNTimes(maxRetries, baseSleepTimeMs);\n\n      case BOUNDED_EXPONENTIAL_BACKOFF:","sourceCodeStart":224,"sourceCodeEnd":260,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java#L224-L260","documentation":"ZkLock.unlock() sets the SharedCount back to UNLOCKED (0); if that ZK write throws, the warning \"Failed to release lock for path: {}\" is logged and a RuntimeException(\"Failed to release lock\", e) is thrown to the caller. Unlike tryLock (which swallows), release failures are propagated because a stuck lock would block all future maintenance triggers for that table.","triggerScenarios":"Calling unlock() while the Curator session is expired/disconnected so setCount(0) cannot reach ZooKeeper; KeeperException (CONNECTIONLOSS, SESSIONEXPIRED, NOAUTH) during the ZK write; the shared count znode was deleted externally; interrupt during the ZK operation.","commonSituations":"ZooKeeper restart or failover while a maintenance task is releasing the lock; network blip between Flink and ZK during unlock; ZK ACL change removing write access to the lock path; task cancellation racing with session expiry.","solutions":["Restore ZooKeeper connectivity/session, then release the lock (unlock retries the setCount(UNLOCKED) write)","Check the exception cause for KeeperException.Code (SESSIONEXPIRED vs CONNECTIONLOSS vs NOAUTH) to pick the right remedy","Fix ZK ACLs if NOAUTH — the Flink user must have write access to /iceberg/flink/maintenance/locks/<lockId>","If the lock remains stuck at LOCKED after recovery, manually reset the znode count to 0 (or delete it) and make lockIds unique per job","Increase retry-policy retries so transient connection loss is retried inside setCount"],"exampleFix":"// before: unlock failure kills the maintenance task without cleanup\ntry {\n  lock.unlock();\n} catch (RuntimeException e) {\n  throw e;\n}\n\n// after: retry release on transient connection loss before giving up\ntry {\n  lock.unlock();\n} catch (RuntimeException e) {\n  if (isRetryableKeeperException(e.getCause())) { // e.g. CONNECTIONLOSS\n    Thread.sleep(retryBackoffMs);\n    lock.unlock();\n  } else {\n    throw e;\n  }\n}","handlingStrategy":"try-catch","validationCode":"// check lock state and ZK session before attempting release\nif (lock.isHeld()) {\n  client.getZookeeperClient().blockUntilConnectedOrTimedOut();\n  lock.unlock();\n}","typeGuard":null,"tryCatchPattern":"try {\n  lock.unlock();\n} catch (RuntimeException e) {\n  Throwable cause = e.getCause();\n  if (cause instanceof org.apache.zookeeper.KeeperException\n      && isRetryable((org.apache.zookeeper.KeeperException) cause)) { // CONNECTIONLOSS\n    retryUnlockWithBackoff(lock);\n  } else {\n    throw e;\n  }\n}","preventionTips":["Keep ZK sessions healthy: size sessionTimeoutMs above worst-case GC pauses","Ensure the Flink user retains write ACLs on /iceberg/flink/maintenance/locks/<lockId>","Run a recovery job that detects locks stuck at LOCKED and resets them after verifying no job is active","Minimize the window between tryLock and unlock so the session cannot expire mid-flight"],"tags":["zookeeper","distributed-lock","flink","session-expired"],"backgroundTag":"connection-refused","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}