{"record":{"id":"9b31ea0c9bc9e80c","repo":"alibaba/nacos","slug":"recursive-unlock-detected-for-key","errorCode":null,"errorMessage":"Recursive unlock() detected for key={}","messagePattern":"Recursive unlock\\(\\) detected for key=(.+?)","errorType":"exception","errorClass":"IllegalMonitorStateException","httpStatus":null,"severity":"error","filePath":"client/src/main/java/com/alibaba/nacos/client/lock/NacosLock.java","lineNumber":290,"sourceCode":"                firstAttempt = false;\n                grpcClient.waitForNotification(key, currentOwner(), remaining);\n            } catch (InterruptedException e) {\n                grpcClient.cancelWait(key, lockType, currentOwner());\n                localReentrantCount.remove();\n                throw e;\n            } catch (NacosException e) {\n                grpcClient.cancelWait(key, lockType, currentOwner());\n                localReentrantCount.remove();\n                LOGGER.error(\"Failed to try lock with timeout, key={}\", key, e);\n                return false;\n            }\n        }\n    }\n    \n    @Override\n    public void unlock() {\n        if (inUnlock.get()) {\n            throw new IllegalMonitorStateException(\"Recursive unlock() detected for key=\" + key);\n        }\n        inUnlock.set(Boolean.TRUE);\n        boolean removed = false;\n        try {\n            int count = localReentrantCount.get();\n            if (count <= 0) {\n                throw new IllegalMonitorStateException(\"Current thread does not hold the lock\");\n            }\n            try {\n                LockInstance instance = buildInstance(0);\n                LockResult result = grpcClient.unLockWithResult(instance);\n                if (result.isSuccess()) {\n                    localReentrantCount.set(count - 1);\n                    if (result.getReentrantCount() == 0) {\n                        watchdog.unregister(key);\n                        localReentrantCount.remove();\n                        removed = true;\n                    }","sourceCodeStart":272,"sourceCodeEnd":308,"githubUrl":"https://github.com/alibaba/nacos/blob/9b989acdf181d00898f2e8839257bb2b2a3cefe3/client/src/main/java/com/alibaba/nacos/client/lock/NacosLock.java#L272-L308","documentation":"Thrown by NacosLock.unlock() when the inUnlock ThreadLocal is already TRUE, meaning unlock() is being called recursively from within the same unlock() invocation — e.g. via a callback, signal handler, or shutdown hook triggered during the unlock flow. This guard prevents read-modify-write corruption of localReentrantCount. The exception type is IllegalMonitorStateException.","triggerScenarios":"During unlock(), the gRPC unLockWithResult call or watchdog.unregister triggers a callback, listener, or shutdown hook that synchronously calls unlock() again on the same NacosLock instance from the same thread before the first unlock() has completed.","commonSituations":"A lock-state-change listener is registered that calls unlock() on notification; a shutdown hook fires during unlock() and attempts cleanup that includes unlock(); an AOP aspect or interceptor wraps unlock() and triggers a recursive call; a custom Lock wrapper delegates to unlock() and is itself called from within the unlock path.","solutions":["Remove or defer any callback/listener that synchronously calls unlock() from within the unlock path — use a separate thread or queue.","Wrap unlock() in your own guard so it is only called once per acquire.","Audit AOP aspects and interceptors that wrap unlock() to ensure they do not re-enter.","Ensure shutdown hooks do not call unlock() on a lock that is mid-unlock — track lifecycle state externally."],"exampleFix":"// before\n@Override\npublic void onLockReleased(String key) {\n    lock.unlock(); // called from within unlock() → recursion detected\n}\n\n// after — defer the callback to avoid re-entrancy\n@Override\npublic void onLockReleased(String key) {\n    cleanupExecutor.submit(() -> {\n        // handle release cleanup without re-entering unlock()\n    });\n}","handlingStrategy":"validation","validationCode":"// Guard against recursive unlock at the application level\nprivate final AtomicBoolean unlockInProgress = new AtomicBoolean(false);\n\nvoid safeUnlock(NacosLock lock) {\n    if (!unlockInProgress.compareAndSet(false, true)) {\n        return; // already unlocking — skip recursive call\n    }\n    try {\n        lock.unlock();\n    } finally {\n        unlockInProgress.set(false);\n    }\n}","typeGuard":"// N/A — recursion is a control-flow issue, not a type-level concern.","tryCatchPattern":"try {\n    lock.unlock();\n} catch (IllegalMonitorStateException e) {\n    if (e.getMessage().contains(\"Recursive unlock\")) {\n        logger.error(\"Recursive unlock detected for key={}\", lock.getKey());\n        // investigate the callback/listener that triggered re-entry\n    } else {\n        throw e;\n    }\n}","preventionTips":["Never call unlock() from within a callback or listener triggered by the unlock path.","Defer cleanup work to a separate executor rather than re-entering unlock() synchronously.","Wrap unlock() in an application-level guard flag to prevent double-calls.","Audit AOP aspects and shutdown hooks for re-entrant unlock() calls."],"tags":["lock","distributed","reentrancy","unlock","concurrency"],"backgroundTag":null,"analyzedSha":"9b989acdf181d00898f2e8839257bb2b2a3cefe3","analyzedAt":"2026-08-14T07:17:31.569Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}