apache/iceberg · warning · IllegalStateException

Lock for currently held by , expiration

Error message

Lock for %s currently held by %s, expiration: %s

What it means

InMemoryLockManager.acquireOnce checks whether the entity's lock is currently held by another owner and unexpired; if so, it throws IllegalStateException with the entity ID, current owner, and expiration timestamp. This is the in-memory lock contention signal for single-process commit coordination.

Solutions

  1. Retry acquire() after the reported expiration time passes (acquire already blocks/retries on heartbeat timeouts)
  2. Investigate the owner operation holding the lock and whether it is stuck or excessively long
  3. Ensure the heartbeat interval/timeout properties fit your operation duration; use a persistent LockManager across JVMs

Example fix

// before
lockManager.acquire(entityId, ownerId); // may throw if held
// after
try {
  lockManager.acquire(entityId, ownerId);
} catch (IllegalStateException e) {
  Thread.sleep(1000);
  lockManager.acquire(entityId, ownerId);
}
Defensive patterns

Strategy: retry

Try / catch

try { lockManager.acquire(entityId, ownerId); } catch (IllegalStateException e) { retryAfter(expirationFrom(e)); }

Prevention

When it happens

Trigger: Two threads in the same JVM acquire the same entity lock concurrently and the first heartbeat has not yet expired when the second attempts acquisition.

Common situations: Long-running commits holding in-memory locks while another operation (e.g., expireSnapshots vs. commit) touches the same table; leaked locks from a crashed thread within the heartbeat timeout window.

Related errors


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

Appendix: source

Thrown at core/src/main/java/org/apache/iceberg/util/LockManagers.java:192

   * implementation should only be used for testing, or if the caller only needs locking within the
   * same JVM during table commits.
   */
  static class InMemoryLockManager extends BaseLockManager {

    private static final Logger LOG = LoggerFactory.getLogger(InMemoryLockManager.class);

    private static final Map<String, InMemoryLockContent> LOCKS = Maps.newConcurrentMap();
    private static final Map<String, ScheduledFuture<?>> HEARTBEATS = Maps.newHashMap();

    InMemoryLockManager(Map<String, String> properties) {
      initialize(properties);
    }

    @VisibleForTesting
    void acquireOnce(String entityId, String ownerId) {
      InMemoryLockContent content = LOCKS.get(entityId);
      if (content != null && content.expireMs() > System.currentTimeMillis()) {
        throw new IllegalStateException(
            String.format(
                "Lock for %s currently held by %s, expiration: %s",
                entityId, content.ownerId(), content.expireMs()));
      }

      long expiration = System.currentTimeMillis() + heartbeatTimeoutMs();
      boolean succeed;
      if (content == null) {
        InMemoryLockContent previous =
            LOCKS.putIfAbsent(entityId, new InMemoryLockContent(ownerId, expiration));
        succeed = previous == null;
      } else {
        succeed = LOCKS.replace(entityId, content, new InMemoryLockContent(ownerId, expiration));
      }

      if (succeed) {
        // cleanup old heartbeat
        if (HEARTBEATS.containsKey(entityId)) {

View on GitHub (pinned to 86d9c8fc54)