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
- Retry acquire() after the reported expiration time passes (acquire already blocks/retries on heartbeat timeouts)
- Investigate the owner operation holding the lock and whether it is stuck or excessively long
- 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
- Keep commit operations shorter than the heartbeat timeout
- Increase heartbeat interval/timeout for long-running operations
- Log and monitor lock owners to identify stuck operations
- Use a persistent LockManager for multi-JVM deployments
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
- Cannot heartbeat to a deleted lock
- Fail to acquire lock
- Failed to create lock
- Interrupted during tryLock
- Unable to acquire lock
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)