{"record":{"id":"c866fc03b464eccc","repo":"iflytek/astron-agent","slug":"executeshutdown-unlock-not-released-maybe-expired-or-token","errorCode":null,"errorMessage":"ExecuteShutdown unlock not released (maybe expired or token mismatch). key={}, tokenTail=***{}","messagePattern":"ExecuteShutdown unlock not released \\(maybe expired or token mismatch\\)\\. key=(.+?), tokenTail=\\*\\*\\*(.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"console/backend/toolkit/src/main/java/com/iflytek/astron/console/toolkit/config/spring/ExecuteShutdown.java","lineNumber":67,"sourceCode":"            return;\n        }\n\n        try {\n            // Actual shutdown action: Clear canvas hold count\n            workflowService.removeAllCanvasHold();\n            log.info(\"ExecuteShutdown done: removeAllCanvasHold finished.\");\n        } catch (Exception e) {\n            // Do not block shutdown, but need complete logging\n            log.error(\"ExecuteShutdown failed while removing canvas hold.\", e);\n        } finally {\n            boolean released = false;\n            try {\n                released = redisUtil.unlock(LOCK_KEY, token);\n            } catch (Exception e) {\n                log.warn(\"ExecuteShutdown unlock threw exception. key={}, tokenTail=***{}\", LOCK_KEY, tail4(token), e);\n            }\n            if (!released) {\n                log.warn(\"ExecuteShutdown unlock not released (maybe expired or token mismatch). key={}, tokenTail=***{}\",\n                        LOCK_KEY, tail4(token));\n            } else {\n                log.debug(\"ExecuteShutdown lock released. key={}\", LOCK_KEY);\n            }\n        }\n    }\n\n    private boolean shouldSkipByProfile() {\n        final String[] actives = environment.getActiveProfiles();\n        if (actives == null || actives.length == 0) {\n            return false;\n        }\n        for (String p : actives) {\n            for (String skip : SKIP_PROFILES) {\n                if (skip.equalsIgnoreCase(p)) {\n                    return true;\n                }\n            }","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/console/backend/toolkit/src/main/java/com/iflytek/astron/console/toolkit/config/spring/ExecuteShutdown.java#L49-L85","documentation":"After redisUtil.unlock returns false (not throwing), ExecuteShutdown logs this warning: the lock was NOT released because its value no longer matches the holder's token (expired and re-acquired by someone else) or the key already expired. This is the non-exception path of the same finally block; the lock's TTL determines when it disappears.","triggerScenarios":"The shutdown hook takes longer than the lock TTL (lock expired before unlock); another process acquired the lock after expiry so the compare-and-delete token check fails; or a previous crashed run already consumed the key.","commonSituations":"Long-running canvas removal in the shutdown hook exceeding a short lock TTL; clock/latency issues under load; lock TTL tuned too small for the shutdown work.","solutions":["Increase the lock TTL so it comfortably covers the maximum duration of the onShutdown work, or implement a watchdog/renewal (Redisson lockHeartbeat) that extends the lease while the hook runs.","Confirm the token passed to unlock is exactly the value set at acquisition (tail4 in the log shows only the last 4 chars for diagnosis); token mismatch usually means a stale/secondary holder.","Treat this warning as benign if the work is complete — the lock will expire via TTL; but if it recurs every shutdown, instrument elapsed time of the removal step and right-size the TTL."],"exampleFix":"// before\nreleased = redisUtil.unlock(LOCK_KEY, token);\n// after\n// ensure TTL > max expected shutdown work, e.g. acquire with 5 min lease\nreleased = redisUtil.unlock(LOCK_KEY, token);\nif (!released && redisUtil.hasKey(LOCK_KEY)) {\n    log.warn(\"lock held by another holder; will expire via TTL\");\n}","handlingStrategy":"retry","validationCode":"long ttl = redisUtil.getExpire(LOCK_KEY);\nif (ttl <= 0) {\n    log.info(\"lock already expired before unlock; token mismatch expected\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    if (!redisUtil.unlock(LOCK_KEY, token)) {\n        // check who holds it now / whether it expired\n        if (redisUtil.hasKey(LOCK_KEY)) log.warn(\"lock re-acquired by another holder\");\n        else log.info(\"lock expired via TTL — benign\");\n    }\n} catch (Exception e) {\n    log.warn(\"unlock error, relying on TTL\", e);\n}","preventionTips":["Size the lock TTL to exceed the worst-case duration of the shutdown work (measure elapsed time and add margin).","Use a renewal/watchdog thread that extends the lease while long work runs.","Alert on recurring 'unlock not released' warnings — they indicate TTL too short or concurrent holders."],"tags":["redis","distributed-lock","lock-expired","shutdown"],"backgroundTag":"invalid-state-transition","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}