apache/iceberg · error · UncheckedSQLException

Failed to remove lock %s

Error message

Failed to remove lock %s

What it means

JdbcLock.unlock() failed to delete/update the lock row in the JDBC lock table; the SQLException is wrapped into UncheckedSQLException with this message. The lock may still be held in the database even though unlock() threw.

Source

Thrown at flink/v1.20/flink/src/main/java/org/apache/iceberg/flink/maintenance/api/JdbcLockFactory.java:273

                  sql.setString(2, lockId);
                  sql.setString(3, instanceId);
                  long count = sql.executeUpdate();
                  LOG.info(
                      "Deleted {} lock with instanceId {} with row count {}",
                      this,
                      instanceId,
                      count);
                }

                return null;
              });
        }
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new UncheckedInterruptedException(e, "Interrupted during unlock");
      } catch (SQLException e) {
        // SQL exception happened when getting/updating lock information
        throw new UncheckedSQLException(e, "Failed to remove lock %s", this);
      }
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this).add("type", type).add("lockId", lockId).toString();
    }

    @SuppressWarnings("checkstyle:NestedTryDepth")
    private String instanceId() {
      try {
        return pool.run(
            conn -> {
              try (PreparedStatement sql = conn.prepareStatement(GET_LOCK_SQL)) {
                sql.setString(1, type.key);
                sql.setString(2, lockId);
                try (ResultSet rs = sql.executeQuery()) {
                  if (rs.next()) {

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Grant the JDBC user DELETE (and SELECT) privileges on the lock table.
  2. Verify the lock table exists with the expected schema (create it via the JdbcCatalog init SQL).
  3. Check the wrapped cause for SQLState to distinguish connectivity vs permission vs deadlock.
  4. Manually remove the stale lock row from the lock table if the lock is blocking new maintenance.
  5. Add DB-side retry/connection pool settings (e.g. socketTimeout) to survive transient failovers.

Example fix

// before
CREATE USER 'flink'@'%' IDENTIFIED BY '...'; -- no grants: unlock fails
GRANT SELECT ON iceberg.* TO 'flink'@'%';
// after
GRANT SELECT, DELETE, INSERT ON iceberg.* TO 'flink'@'%';
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-grant check
try (ResultSet rs = c.createStatement().executeQuery(
    "SELECT DELETE_PRIV FROM mysql.db WHERE User='flink'")) { ... }

Try / catch

try {
  lock.unlock();
} catch (UncheckedSQLException e) {
  LOG.error("Failed to remove lock {}", lockId, e.getCause());
  // schedule manual cleanup / rely on lock expiry
}

Prevention

When it happens

Trigger: SQLException while executing the DELETE/UPDATE on the lock table inside unlock(): connection lost, table missing, insufficient DELETE privilege, or DB deadlock/timeout.

Common situations: DB user lacks DELETE privilege on the lock table; database failover drops the connection mid-unlock; lock table schema was altered; lock row already removed by another instance and the statement errors.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/239ef84130a39142. Report an issue: GitHub.