apache/dolphinscheduler · error · IllegalMonitorStateException

Jdbc lock count has gone negative for lock:

Error message

Jdbc lock count has gone negative for lock: 

What it means

JdbcRegistryLockManager.releaseJdbcRegistryLock decrements the reentrant lockCount for the lock key; if the count goes below zero, more releases than acquires occurred, indicating unbalanced lock bookkeeping. It throws IllegalMonitorStateException, mirroring java.util.concurrent lock semantics where unlocking an unheld lock is illegal.

Source

Thrown at dolphinscheduler-registry/dolphinscheduler-registry-plugins/dolphinscheduler-registry-jdbc/src/main/java/org/apache/dolphinscheduler/plugin/registry/jdbc/server/JdbcRegistryLockManager.java:149

    }

    @Override
    public void releaseJdbcRegistryLock(Long clientId, String lockKey) {
        String lockOwner = LockUtils.getLockOwner();
        LockEntry lockEntry = jdbcRegistryLockHolderMap.get(lockKey);
        if (lockEntry == null || !lockOwner.equals(lockEntry.getLockOwner())) {
            return;
        }
        if (!clientId.equals(lockEntry.getJdbcRegistryLock().getClientId())) {
            throw new UnsupportedOperationException(
                    "The client " + clientId + " is not the lock owner of the lock: " + lockKey);
        }
        int newLockCount = lockEntry.lockCount.decrementAndGet();
        if (newLockCount > 0) {
            return;
        }
        if (newLockCount < 0) {
            throw new IllegalMonitorStateException("Jdbc lock count has gone negative for lock: " + lockKey);
        }
        jdbcRegistryLockRepository.deleteById(lockEntry.getJdbcRegistryLock().getId());
        jdbcRegistryLockHolderMap.remove(lockKey);
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class LockEntry {

        private String lockKey;
        private String lockOwner;
        final AtomicInteger lockCount = new AtomicInteger(1);
        private JdbcRegistryLockDTO jdbcRegistryLock;
    }
}

View on GitHub (pinned to 02eac45a1b)

Solutions

  1. Only release in a finally block when acquireJdbcRegistryLock returned/entered successfully
  2. Track acquired count locally and release exactly that many times
  3. Use the timeout-based acquire which returns false on failure, and skip release when it returns false

Example fix

// before
try {
    // may fail to acquire
} finally {
    registryClient.releaseJdbcRegistryLock(clientId, lockKey); // releases even on failed acquire
}
// after
if (registryClient.acquireJdbcRegistryLock(clientId, lockKey, timeout)) {
    try {
        // work
    } finally {
        registryClient.releaseJdbcRegistryLock(clientId, lockKey);
    }
}
Defensive patterns

Strategy: validation

Validate before calling

private final AtomicInteger held = new AtomicInteger(); // increment only after successful acquire; release only if held.get() > 0

Try / catch

try { registryClient.releaseJdbcRegistryLock(clientId, lockKey); } catch (IllegalMonitorStateException e) { log.error("Unbalanced release for {}: {}", lockKey, e.getMessage()); }

Prevention

When it happens

Trigger: Calling releaseJdbcRegistryLock more times than acquireJdbcRegistryLock for the same lockKey/clientId; concurrent release from multiple threads racing the decrement; release after the entry was already removed by a prior final release but a stale reference re-decrements.

Common situations: finally blocks that release unconditionally even when acquire failed, double-release due to retry logic, non-reentrant usage patterns where code assumes one release per process rather than per acquire.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of apache/dolphinscheduler@02eac45a1b (2026-09-06). Data as JSON: /api/errors/381f83ebf2b1487c. Report an issue: GitHub.