Repository: apache/iceberg
Pull request #12810: Flink: Support zookeeper lock in TableMaintenance
Author: login: Guosmilesmile
Labels: name: flink
name: INFRA
Created: 2025-04-16T08:22:13Z | Merged: 2025-05-15T08:20:05Z

In TableMaintenance, we have already implemented JdbcLock. I believe we will have a need for ZookeeperLock. This PR aims to support ZkLockFactory to implement ZkLock.


Diff:
diff --git a/.baseline/checkstyle/checkstyle-suppressions.xml b/.baseline/checkstyle/checkstyle-suppressions.xml
index 9fe7c38974e6..d88fa01410a3 100644
--- a/.baseline/checkstyle/checkstyle-suppressions.xml
+++ b/.baseline/checkstyle/checkstyle-suppressions.xml
@@ -49,6 +49,9 @@
     <!-- Referencing guava classes should be allowed in classes within bundled-guava module -->
     <suppress files="org.apache.iceberg.GuavaClasses" id="BanUnrelocatedGuavaClasses"/>
 
+    <!-- Allow using Flink's shaded Curator dependency -->
+    <suppress files="org.apache.iceberg.flink.maintenance.api.ZkLockFactory" id="BanShadedClasses"/>
+
     <!-- Suppress checks for CometColumnReader -->
     <suppress files="org.apache.iceberg.spark.data.vectorized.CometColumnReader" checks="IllegalImport"/>
 </suppressions>
diff --git a/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
new file mode 100644
index 000000000000..0d31f7fa7e57
--- /dev/null
+++ b/flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.shaded.curator5.org.apache.curator.framework.CuratorFramework;
+import org.apache.flink.shaded.curator5.org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.flink.shaded.curator5.org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.flink.shaded.curator5.org.apache.curator.framework.recipes.shared.VersionedValue;
+import org.apache.flink.shaded.curator5.org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+  private static final int LOCKED = 1;
+  private static final int UNLOCKED = 0;
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      VersionedValue<Integer> versionedValue = sharedCount.getVersionedValue();
+      if (isHeld(versionedValue)) {
+        LOG.debug("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(versionedValue, LOCKED);
+      } catch (Exception e) {
+        LOG.debug("Failed to acquire Zookeeper lock ", e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      return isHeld(sharedCount.getVersionedValue());
+    }
+
+    private static boolean isHeld(VersionedValue<Integer> versionedValue) {
+      try {
+        return versionedValue.getValue() == LOCKED;
+      } catch (Exception e) {
+        throw new RuntimeException("Failed to check Zookeeper lock status", e);
+      }
+    }
+
+    @Override
+    public void unlock() {
+      try {
+        sharedCount.setCount(UNLOCKED);
+      } catch (Exception e) {
+        throw new RuntimeException("Failed to release lock", e);
+      }
+    }
+  }
+}
diff --git a/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java
new file mode 100644
index 000000000000..5f16c8966dd1
--- /dev/null
+++ b/flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import org.apache.curator.test.TestingServer;
+import org.junit.After;
+import org.junit.jupiter.api.BeforeEach;
+
+public class TestZkLockFactory extends TestLockFactoryBase {
+  private final String testLockId = "tableName";
+  private TestingServer zkTestServer;
+
+  @BeforeEach
+  @Override
+  void before() {
+    try {
+      zkTestServer = new TestingServer();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    super.before();
+  }
+
+  @Override
+  TriggerLockFactory lockFactory() {
+    return new ZkLockFactory(zkTestServer.getConnectString(), testLockId, 5000, 3000, 1000, 3);
+  }
+
+  @After
+  public void after() throws IOException {
+    super.after();
+
+    if (zkTestServer != null) {
+      zkTestServer.close();
+    }
+  }
+}


Reviews:
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T08:26:29Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:45:12Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:46:31Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:47:02Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:49:34Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:49:59Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:54:37Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:57:51Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:58:01Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:58:18Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:59:27Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T11:28:02Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T11:29:45Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T11:34:57Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:17:12Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:20:28Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:21:27Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:23:07Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:42:35Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:43:06Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:43:42Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:43:47Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:51:38Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:01:58Z
user: login: stevenzwu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:41:57Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T01:43:47Z
user: login: gyfora
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:08:39Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:35:16Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:35:24Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:36:26Z
user: login: gyfora
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:08:18Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:40:16Z
user: login: mxm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:51:32Z
user: login: mxm
body: This looks great. Thank you @Guosmilesmile! I've left some comments inline. Please see https://github.com/apache/iceberg/pull/12810#discussion_r2084863331.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:52:53Z
user: login: gyfora
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:32:21Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T05:53:10Z
user: login: pvary
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T20:30:19Z
user: login: pvary
body: Small nit
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T20:32:12Z
user: login: Guosmilesmile
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T01:20:17Z
user: login: gyfora
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T05:50:55Z

Review comments:
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/recovery/" + lockId);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (client != null) {
+      client.close();
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final String lockPath;
+    private final CuratorFramework client;
+
+    private ZkLock(CuratorFramework client, String lockPath) {
+      this.client = client;
+      this.lockPath = lockPath;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      String newInstanceId = UUID.randomUUID().toString();
+      try {
+        client
+            .create()
+            .creatingParentsIfNeeded()
+            .forPath(lockPath, newInstanceId.getBytes(Charset.defaultCharset()));
+        return true;
+      } catch (KeeperException.NodeExistsException e) {
+        // Check if the lock creation was successful behind the scenes.
+        if (newInstanceId.equals(instanceId())) {
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: I found that there are two dependencies: org.apache.flink.shaded.zookeeper3.org.apache.zookeeper.KeeperException and org.apache.zookeeper.KeeperException. Currently, only the latter is being checked. In this scenario with multiple dependencies, what would be the appropriate way to handle it?

Also, if there is no scenario where the client successfully creates a node but returns an error, then the catch for NodeExistsException can be remove.
created_at: 2025-04-16T08:26:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/recovery/" + lockId);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (client != null) {
+      client.close();
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final String lockPath;
+    private final CuratorFramework client;
+
+    private ZkLock(CuratorFramework client, String lockPath) {
+      this.client = client;
+      this.lockPath = lockPath;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      String newInstanceId = UUID.randomUUID().toString();
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: Do we need this `newInstanceId` at all?
With the `JDBCLockFactory` we needed it because we had to handle retries.
ZK retries are handled by the curator framework, so I think we can just rely on that, and create/delete nodes.
Do you have different experience with ZK?
created_at: 2025-04-16T09:45:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: nit: `LOCK_BASE_PATH + lockId + "/task"`
created_at: 2025-04-16T09:46:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/recovery/" + lockId);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: nit: `LOCK_BASE_PATH + lockId + "/recovery"`

This way the nodes are grouped by lockId which seems more natural to me
created_at: 2025-04-16T09:47:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: this is for distinguish different tables. So no 2 jobs could run table maintenance on the same table
created_at: 2025-04-16T09:49:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: This is not documented in the javadoc
created_at: 2025-04-16T09:49:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/recovery/" + lockId);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (client != null) {
+      client.close();
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final String lockPath;
+    private final CuratorFramework client;
+
+    private ZkLock(CuratorFramework client, String lockPath) {
+      this.client = client;
+      this.lockPath = lockPath;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      String newInstanceId = UUID.randomUUID().toString();
+      try {
+        client
+            .create()
+            .creatingParentsIfNeeded()
+            .forPath(lockPath, newInstanceId.getBytes(Charset.defaultCharset()));
+        return true;
+      } catch (KeeperException.NodeExistsException e) {
+        // Check if the lock creation was successful behind the scenes.
+        if (newInstanceId.equals(instanceId())) {
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: >I found that there are two dependencies:
> org.apache.flink.shaded.zookeeper3.org.apache.zookeeper.KeeperException and org.apache.zookeeper.KeeperException. Currently, only the latter is being checked. In this scenario with multiple dependencies, what would be the appropriate way to handle it?

I would expect that the exception depends on the framework we use. Since we use the non-shaded framework, is should throw `org.apache.zookeeper.KeeperException`.

> Also, if there is no scenario where the client successfully creates a node but returns an error, then the catch for NodeExistsException can be remove.

Since the curator framework is there for retries and such, I would delegate the whole retry logic to the framework. OTOH, it could happen that concurrent node creation happens between the `isHeld` and the `create`, so we still have to check for the exceptions
created_at: 2025-04-16T09:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/recovery/" + lockId);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (client != null) {
+      client.close();
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final String lockPath;
+    private final CuratorFramework client;
+
+    private ZkLock(CuratorFramework client, String lockPath) {
+      this.client = client;
+      this.lockPath = lockPath;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      String newInstanceId = UUID.randomUUID().toString();
+      try {
+        client
+            .create()
+            .creatingParentsIfNeeded()
+            .forPath(lockPath, newInstanceId.getBytes(StandardCharsets.UTF_8));
+        return true;
+      } catch (KeeperException.NodeExistsException e) {
+        // Check if the lock creation was successful behind the scenes.
+        if (newInstanceId.equals(instanceId())) {
+          return true;
+        } else {
+          throw new UncheckedSQLException(e, "Failed to create %s lock", this);
+        }
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock: {}", lockPath, e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return client.checkExists().forPath(lockPath) != null;
+      } catch (Exception e) {
+        LOG.info("Failed to check Zookeeper lock status: {}", lockPath, e);
+        return false;
+      }
+    }
+
+    @Override
+    public void unlock() {
+      // If the path is not exists, delete will fail.
+      if (!isHeld()) {
+        return;
+      }
+      try {
+        client.delete().forPath(lockPath);
+      } catch (Exception e) {
+        LOG.info("Failed to release Zookeeper lock: {}", lockPath, e);
+        throw new RuntimeException("Failed to release lock", e);
+      }
+    }
+
+    public String instanceId() {
+      try {
+        byte[] data = client.getData().forPath(lockPath);
+        return new String(data, StandardCharsets.UTF_8);
+      } catch (Exception e) {
+        LOG.info("Failed to get lock value: {}", lockPath, e);
+        return null;
+      }
+    }
+  }
+
+  // Since ExponentialBackoffRetry contains non-serializable fields, it needs to be wrapped and
+  // lazily initialized.
+  public static class ExponentialBackoffRetryWrapper implements Serializable {
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: Could we just expose `int baseSleepTimeMs, int maxRetries` in the `ZkLockFactory` constructor instead of exposing a whole new object?

Also in this chase the `ExponentialBackoffRetryWrapper` could be private class.

This would help to narrow down the public API
created_at: 2025-04-16T09:57:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/recovery/" + lockId);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (client != null) {
+      client.close();
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final String lockPath;
+    private final CuratorFramework client;
+
+    private ZkLock(CuratorFramework client, String lockPath) {
+      this.client = client;
+      this.lockPath = lockPath;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      String newInstanceId = UUID.randomUUID().toString();
+      try {
+        client
+            .create()
+            .creatingParentsIfNeeded()
+            .forPath(lockPath, newInstanceId.getBytes(StandardCharsets.UTF_8));
+        return true;
+      } catch (KeeperException.NodeExistsException e) {
+        // Check if the lock creation was successful behind the scenes.
+        if (newInstanceId.equals(instanceId())) {
+          return true;
+        } else {
+          throw new UncheckedSQLException(e, "Failed to create %s lock", this);
+        }
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock: {}", lockPath, e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return client.checkExists().forPath(lockPath) != null;
+      } catch (Exception e) {
+        LOG.info("Failed to check Zookeeper lock status: {}", lockPath, e);
+        return false;
+      }
+    }
+
+    @Override
+    public void unlock() {
+      // If the path is not exists, delete will fail.
+      if (!isHeld()) {
+        return;
+      }
+      try {
+        client.delete().forPath(lockPath);
+      } catch (Exception e) {
+        LOG.info("Failed to release Zookeeper lock: {}", lockPath, e);
+        throw new RuntimeException("Failed to release lock", e);
+      }
+    }
+
+    public String instanceId() {
+      try {
+        byte[] data = client.getData().forPath(lockPath);
+        return new String(data, StandardCharsets.UTF_8);
+      } catch (Exception e) {
+        LOG.info("Failed to get lock value: {}", lockPath, e);
+        return null;
+      }
+    }
+  }
+
+  // Since ExponentialBackoffRetry contains non-serializable fields, it needs to be wrapped and
+  // lazily initialized.
+  public static class ExponentialBackoffRetryWrapper implements Serializable {
+    private transient ExponentialBackoffRetry retryPolicy;
+
+    public ExponentialBackoffRetryWrapper(int baseSleepTimeMs, int maxRetries) {
+      this.retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries);
+    }
+
+    public ExponentialBackoffRetry getRetryPolicy() {
+      if (retryPolicy == null) {
+        retryPolicy = new ExponentialBackoffRetry(1000, 3);
+      }
+      return retryPolicy;
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: nit: newline
created_at: 2025-04-16T09:58:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import org.apache.curator.test.TestingServer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestZkLockFactory {
+  private TestingServer zkTestServer;
+  private ZkLockFactory lockFactory;
+  private final String testLockId = "tableName";
+
+  @Before
+  public void before() throws Exception {
+    // Start test Zookeeper server
+    zkTestServer = new TestingServer();
+    // Create lock factory instance
+    lockFactory =
+        new ZkLockFactory(
+            zkTestServer.getConnectString(),
+            testLockId,
+            5000,
+            3000,
+            new ZkLockFactory.ExponentialBackoffRetryWrapper(1000, 3));
+    lockFactory.open();
+  }
+
+  @After
+  public void after() throws IOException {
+    if (lockFactory != null) {
+      lockFactory.close();
+    }
+    if (zkTestServer != null) {
path: flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java
user: login: pvary
body: nit: newline
created_at: 2025-04-16T09:58:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import org.apache.curator.test.TestingServer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestZkLockFactory {
path: flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java
user: login: pvary
body: Why not inherit from `TestLockFactoryBase`?

It's ok to add extra tests here, but please reuse the `TestLockFactoryBase` for the standard tests
created_at: 2025-04-16T09:59:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: I wll fix it 
created_at: 2025-04-16T11:28:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import org.apache.curator.test.TestingServer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestZkLockFactory {
path: flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java
user: login: Guosmilesmile
body: Oh, that was my mistake; I didn't pay attention to this. It should indeed be based on TestLockFactoryBase.
created_at: 2025-04-16T11:29:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final ExponentialBackoffRetryWrapper retryWrapper;
+  private transient CuratorFramework client;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId Lock ID to distinguish different jobs
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      ExponentialBackoffRetryWrapper retryWrapper) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.retryWrapper = retryWrapper;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(retryWrapper.getRetryPolicy())
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/maintenance/" + lockId);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(client, LOCK_BASE_PATH + "/recovery/" + lockId);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (client != null) {
+      client.close();
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final String lockPath;
+    private final CuratorFramework client;
+
+    private ZkLock(CuratorFramework client, String lockPath) {
+      this.client = client;
+      this.lockPath = lockPath;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      String newInstanceId = UUID.randomUUID().toString();
+      try {
+        client
+            .create()
+            .creatingParentsIfNeeded()
+            .forPath(lockPath, newInstanceId.getBytes(StandardCharsets.UTF_8));
+        return true;
+      } catch (KeeperException.NodeExistsException e) {
+        // Check if the lock creation was successful behind the scenes.
+        if (newInstanceId.equals(instanceId())) {
+          return true;
+        } else {
+          throw new UncheckedSQLException(e, "Failed to create %s lock", this);
+        }
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock: {}", lockPath, e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return client.checkExists().forPath(lockPath) != null;
+      } catch (Exception e) {
+        LOG.info("Failed to check Zookeeper lock status: {}", lockPath, e);
+        return false;
+      }
+    }
+
+    @Override
+    public void unlock() {
+      // If the path is not exists, delete will fail.
+      if (!isHeld()) {
+        return;
+      }
+      try {
+        client.delete().forPath(lockPath);
+      } catch (Exception e) {
+        LOG.info("Failed to release Zookeeper lock: {}", lockPath, e);
+        throw new RuntimeException("Failed to release lock", e);
+      }
+    }
+
+    public String instanceId() {
+      try {
+        byte[] data = client.getData().forPath(lockPath);
+        return new String(data, StandardCharsets.UTF_8);
+      } catch (Exception e) {
+        LOG.info("Failed to get lock value: {}", lockPath, e);
+        return null;
+      }
+    }
+  }
+
+  // Since ExponentialBackoffRetry contains non-serializable fields, it needs to be wrapped and
+  // lazily initialized.
+  public static class ExponentialBackoffRetryWrapper implements Serializable {
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: We should simplify the public API,  let users know that ExponentialBackoffRetryWrapper is actually not suitable. I have made changes to the API.
created_at: 2025-04-16T11:34:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: nit: newline. Please check every code block too
created_at: 2025-05-05T12:17:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return sharedCount.getCount() == 1;
+      } catch (Exception e) {
+        LOG.info("Failed to check Zookeeper lock status", e);
+        return false;
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: Do we return false here?
JdbcLockFactory throws an exception when error is occured
created_at: 2025-05-05T12:20:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return sharedCount.getCount() == 1;
+      } catch (Exception e) {
+        LOG.info("Failed to check Zookeeper lock status", e);
+        return false;
+      }
+    }
+
+    @Override
+    public void unlock() {
+      if (!isHeld()) {
+        return;
+      }
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: Do we need this?
This is an extra call to the ZooKeeper if the lock is held (which should be the typical situation)
created_at: 2025-05-05T12:21:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import org.apache.curator.test.TestingServer;
+import org.junit.After;
+
+public class TestZkLockFactory extends TestLockFactoryBase {
+  private final String testLockId = "tableName";
+  private TestingServer zkTestServer;
+
+  @Override
+  TriggerLockFactory lockFactory() {
+
+    try {
+      zkTestServer = new TestingServer();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
path: flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java
user: login: pvary
body: This should be in the `before` method
created_at: 2025-05-05T12:23:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: Fix 
created_at: 2025-05-05T12:42:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return sharedCount.getCount() == 1;
+      } catch (Exception e) {
+        LOG.info("Failed to check Zookeeper lock status", e);
+        return false;
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: Change to throw an exception instead. 
created_at: 2025-05-05T12:43:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return sharedCount.getCount() == 1;
+      } catch (Exception e) {
+        LOG.info("Failed to check Zookeeper lock status", e);
+        return false;
+      }
+    }
+
+    @Override
+    public void unlock() {
+      if (!isHeld()) {
+        return;
+      }
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: Yes, remove it can reduce an extra call to ZooKeeper. Done
created_at: 2025-05-05T12:43:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import org.apache.curator.test.TestingServer;
+import org.junit.After;
+
+public class TestZkLockFactory extends TestLockFactoryBase {
+  private final String testLockId = "tableName";
+  private TestingServer zkTestServer;
+
+  @Override
+  TriggerLockFactory lockFactory() {
+
+    try {
+      zkTestServer = new TestingServer();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
path: flink/v1.20/flink/src/test/java/org/apache/iceberg/flink/maintenance/api/TestZkLockFactory.java
user: login: Guosmilesmile
body: fix 
created_at: 2025-05-05T12:43:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(1);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: Sorry for noticing this so late in the review, but `trySetCount(int)` is deprecated. We should use the new method
created_at: 2025-05-05T13:51:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(1);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: Good catch ! It was my negligence.  Fix.
created_at: 2025-05-05T14:01:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: stevenzwu
body: My main concern would be the dependency and wondering if we need to require zk/curator dep in the flink module.

but looks like the `curator` dep is already pulled in (probably transitively). which lib pulls in `curator` transitively?

created_at: 2025-05-05T16:41:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: I found that the following packages include Curator, but only at the compile stage. When packaging `iceberg-flink-runtime-1.19-1.8.1.jar`, the Curator dependency is not included.

```
org.apache.flink:flink-connector-test-utils:1.20.1
org.apache.hadoop:hadoop-common:3.4.1
org.apache.hadoop:hadoop-minicluster:3.4.1
org.apache.hive:hive-exec:2.3.10
org.apache.hive:hive-metastore:2.3.10
```

So, if we want to use `zkLock`, we need to add the Curator dependency in the Flink lib or business package. 

Currently, if we want to use `JdbcCatalog`, we need to add `mysql-connector-java-8.0.30.jar` to the lib. In my view if we want to use `jdbcLock`, we also need to add the dependency myself

If you have better suggestions, please let me know. Thank you very much!
created_at: 2025-05-06T01:43:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: gyfora
body: I think these logs should be on debug level, not info.
created_at: 2025-05-08T12:07:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(sharedCount.getVersionedValue(), 1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: gyfora
body: I think these logs should be on debug level, not info.
created_at: 2025-05-08T12:07:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(sharedCount.getVersionedValue(), 1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return sharedCount.getCount() == 1;
+      } catch (Exception e) {
+        throw new RuntimeException("Failed to check Zookeeper lock status", e);
+      }
+    }
+
+    @Override
+    public void unlock() {
+      try {
+        sharedCount.setCount(0);
+      } catch (Exception e) {
+        LOG.info("Failed to release Zookeeper lock ", e);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: gyfora
body: log on error level maybe? Depending on how this error is handling logging may be completely unnecessary here
created_at: 2025-05-08T12:08:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: fix 
created_at: 2025-05-08T12:35:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(sharedCount.getVersionedValue(), 1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: fix
created_at: 2025-05-08T12:35:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.info("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(sharedCount.getVersionedValue(), 1);
+      } catch (Exception e) {
+        LOG.info("Failed to acquire Zookeeper lock ", e);
+        return false;
+      }
+    }
+
+    @Override
+    public boolean isHeld() {
+      try {
+        return sharedCount.getCount() == 1;
+      } catch (Exception e) {
+        throw new RuntimeException("Failed to check Zookeeper lock status", e);
+      }
+    }
+
+    @Override
+    public void unlock() {
+      try {
+        sharedCount.setCount(0);
+      } catch (Exception e) {
+        LOG.info("Failed to release Zookeeper lock ", e);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: Yes, since throw exception after, log the message is unnecessary. Remove it . 
created_at: 2025-05-08T12:36:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: gyfora
body: should this be optionally configurable?
created_at: 2025-05-12T14:08:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: If we expose the configuration for this path, it would require adding more validations on user input, such as ensuring it starts with "/", ends with "/", and does not contain consecutive "/" characters. I’m concerned this might lead to more input errors, so I chose not to expose this configuration. 
But this is just my personal opinion. If there are any issues, please feel free to ping me.
Thank you very much.

created_at: 2025-05-12T14:40:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: mxm
body: You don't directly see the Curator dependency because Flink uses a custom shaded version of Curator, see https://github.com/apache/flink-shaded/blob/6c887612faa475384b3aab7778c762ac19a746e8/flink-shaded-zookeeper-parent/flink-shaded-zookeeper-35/pom.xml#L63

We could use Flink's `shaded-zookeeper` dependency which contains Curator. Scope would have to be "provided". Alternatively, we could also use our own version, since it won't conflict with Flink's relocated version.
created_at: 2025-05-12T14:50:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: mxm
body: Alternatively, we could just allow configuring a namespace. Such configs could still be added later.
created_at: 2025-05-12T14:50:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: gyfora
body: adding this later if needed sounds good enough :)
created_at: 2025-05-12T15:32:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.flink.maintenance.api;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.flink.shaded.curator5.org.apache.curator.framework.CuratorFramework;
+import org.apache.flink.shaded.curator5.org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.flink.shaded.curator5.org.apache.curator.framework.recipes.shared.SharedCount;
+import org.apache.flink.shaded.curator5.org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Zookeeper backed implementation of the {@link TriggerLockFactory}. */
+public class ZkLockFactory implements TriggerLockFactory {
+  private static final Logger LOG = LoggerFactory.getLogger(ZkLockFactory.class);
+
+  private static final String LOCK_BASE_PATH = "/iceberg/flink/maintenance/locks/";
+
+  private final String connectString;
+  private final String lockId;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxRetries;
+  private transient CuratorFramework client;
+  private transient SharedCount taskSharedCount;
+  private transient SharedCount recoverySharedCount;
+
+  /**
+   * Create Zookeeper lock factory
+   *
+   * @param connectString Zookeeper connection string
+   * @param lockId which should identify the job and the table
+   * @param sessionTimeoutMs Session timeout in milliseconds
+   * @param connectionTimeoutMs Connection timeout in milliseconds
+   * @param baseSleepTimeMs Base sleep time in milliseconds
+   * @param maxRetries Maximum number of retries
+   */
+  public ZkLockFactory(
+      String connectString,
+      String lockId,
+      int sessionTimeoutMs,
+      int connectionTimeoutMs,
+      int baseSleepTimeMs,
+      int maxRetries) {
+    Preconditions.checkNotNull(connectString, "Zookeeper connection string cannot be null");
+    Preconditions.checkNotNull(lockId, "Lock ID cannot be null");
+    this.connectString = connectString;
+    this.lockId = lockId;
+    this.sessionTimeoutMs = sessionTimeoutMs;
+    this.connectionTimeoutMs = connectionTimeoutMs;
+    this.baseSleepTimeMs = baseSleepTimeMs;
+    this.maxRetries = maxRetries;
+  }
+
+  @Override
+  public void open() {
+    this.client =
+        CuratorFrameworkFactory.builder()
+            .connectString(connectString)
+            .sessionTimeoutMs(sessionTimeoutMs)
+            .connectionTimeoutMs(connectionTimeoutMs)
+            .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
+            .build();
+    client.start();
+
+    try {
+      if (!client.blockUntilConnected(connectionTimeoutMs, TimeUnit.MILLISECONDS)) {
+        throw new IllegalStateException("Connection to Zookeeper timed out");
+      }
+
+      this.taskSharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/task", 0);
+      this.recoverySharedCount = new SharedCount(client, LOCK_BASE_PATH + lockId + "/recovery", 0);
+      taskSharedCount.start();
+      recoverySharedCount.start();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while connecting to Zookeeper", e);
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to initialize SharedCount", e);
+    }
+  }
+
+  @Override
+  public Lock createLock() {
+    return new ZkLock(taskSharedCount);
+  }
+
+  @Override
+  public Lock createRecoveryLock() {
+    return new ZkLock(recoverySharedCount);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      if (taskSharedCount != null) {
+        taskSharedCount.close();
+      }
+
+      if (recoverySharedCount != null) {
+        recoverySharedCount.close();
+      }
+    } finally {
+      if (client != null) {
+        client.close();
+      }
+    }
+  }
+
+  /** Zookeeper lock implementation */
+  private static class ZkLock implements Lock {
+    private final SharedCount sharedCount;
+
+    private ZkLock(SharedCount sharedCount) {
+      this.sharedCount = sharedCount;
+    }
+
+    @Override
+    public boolean tryLock() {
+      if (isHeld()) {
+        LOG.debug("Lock is already held for {}", this);
+        return false;
+      }
+
+      try {
+        return sharedCount.trySetCount(sharedCount.getVersionedValue(), 1);
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: I'm not comfortable with this one.

Shouldn't we get/check/trySet? This way, if there is a concurrent lock between `isHeld` and the `trySet`, then we just ignore it.
created_at: 2025-05-14T05:53:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,8 +155,12 @@ public boolean tryLock() {
 
     @Override
     public boolean isHeld() {
+      return isHeld(sharedCount.getVersionedValue());
+    }
+
+    private boolean isHeld(VersionedValue<Integer> versionedValue) {
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: pvary
body: Could this be a static method?
created_at: 2025-05-14T20:30:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,8 +155,12 @@ public boolean tryLock() {
 
     @Override
     public boolean isHeld() {
+      return isHeld(sharedCount.getVersionedValue());
+    }
+
+    private boolean isHeld(VersionedValue<Integer> versionedValue) {
path: flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/ZkLockFactory.java
user: login: Guosmilesmile
body: Alright , done ! 
created_at: 2025-05-15T01:20:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: Guosmilesmile
created_at: 2025-04-16T09:19:07Z
body: @pvary Hi Peter, if you have time, could you please help with the review? 
Thank you very much!
author_association: CONTRIBUTOR
user: login: Guosmilesmile
created_at: 2025-04-16T11:42:31Z
body: @pvary I have changed the code based on the comments. However, there is still one issue we need to discuss: whether to keep the InstanceId.

The InstanceId is mainly used to solve the problem of whether a retry was successfully done by self. Even though we have given the retries to the curator framework, I am not completely sure if the framework will work as expected in this case, so I kept the InstanceId.

If it is not needed, I think we can remove it.
author_association: CONTRIBUTOR
user: login: pvary
created_at: 2025-04-29T10:01:09Z
body: > @pvary I have changed the code based on the comments. However, there is still one issue we need to discuss: whether to keep the InstanceId.
> 
> The InstanceId is mainly used to solve the problem of whether a retry was successfully done by self. Even though we have given the retries to the curator framework, I am not completely sure if the framework will work as expected in this case, so I kept the InstanceId.
> 
> If it is not needed, I think we can remove it.

This problem appears a lot for the users, so there is some already existing solution for this.
See: https://curator.apache.org/docs/recipes-shared-lock

Also this seems like a typical problem for distributed system. Could we use one of the existing curator recepies?
https://curator.apache.org/docs/recipes
author_association: CONTRIBUTOR
user: login: Guosmilesmile
created_at: 2025-05-02T10:37:49Z
body: Yes,It seems that these existing methods are more suitable than my original ones.I switch SharedCount to implement related functionality.

On lock we set the counter to 1. (trySetCount(1))
On unlock we set the counter to 0 (setCount(0))
On isHeld we check the counter value (getCount()==1).

author_association: CONTRIBUTOR
user: login: Guosmilesmile
created_at: 2025-05-12T13:51:19Z
body: @pvary @stevenzwu @gyfora Thanks for the review! Could you please check if there is anything else I need to do to further advance this PR?

Thanks!
GuoYu
author_association: CONTRIBUTOR
user: login: Guosmilesmile
created_at: 2025-05-13T02:32:46Z
body: @mxm As we discussed offline, Flink always includes the Curator library, so it is fine to bundle Curator for iceberg. But in checkstyleMain, import repackaged classes will make the CI fail.
author_association: CONTRIBUTOR
user: login: mxm
created_at: 2025-05-13T10:46:58Z
body: In this case, we may add a Checkstyle suppression rule to allow using Flink's shaded Curator. I think it's better to transitively depend on Flink's shaded Curator than to bundle Curator once more.
author_association: CONTRIBUTOR
user: login: Guosmilesmile
created_at: 2025-05-13T13:17:10Z
body: @mxm I've completed the modifications for this PR.
Please review it again when you have time.Thank you!
author_association: CONTRIBUTOR
user: login: pvary
created_at: 2025-05-15T08:20:35Z
body: Merged to main.
Thanks for the PR @Guosmilesmile, and @mxm, @gyfora and @stevenzwu for the reviews!
author_association: CONTRIBUTOR