iflytek/astron-agent · warning
ExecuteShutdown unlock not released (maybe expired or token…
Error message
ExecuteShutdown unlock not released (maybe expired or token mismatch). key={}, tokenTail=***{} What it means
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.
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.
Example fix
// before
released = redisUtil.unlock(LOCK_KEY, token);
// after
// ensure TTL > max expected shutdown work, e.g. acquire with 5 min lease
released = redisUtil.unlock(LOCK_KEY, token);
if (!released && redisUtil.hasKey(LOCK_KEY)) {
log.warn("lock held by another holder; will expire via TTL");
} Defensive patterns
Strategy: retry
Validate before calling
long ttl = redisUtil.getExpire(LOCK_KEY);
if (ttl <= 0) {
log.info("lock already expired before unlock; token mismatch expected");
} Try / catch
try {
if (!redisUtil.unlock(LOCK_KEY, token)) {
// check who holds it now / whether it expired
if (redisUtil.hasKey(LOCK_KEY)) log.warn("lock re-acquired by another holder");
else log.info("lock expired via TTL — benign");
}
} catch (Exception e) {
log.warn("unlock error, relying on TTL", e);
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- ExecuteShutdown unlock threw exception. key=
- RELEASE_FAILED
- REDIS_CONNECTION_ERROR
- Timed out acquiring distributed lock, please try again later
- Distributed lock acquisition timeout, please try again later
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/c866fc03b464eccc.
Report an issue: GitHub.
Appendix: source
Thrown at console/backend/toolkit/src/main/java/com/iflytek/astron/console/toolkit/config/spring/ExecuteShutdown.java:67
return;
}
try {
// Actual shutdown action: Clear canvas hold count
workflowService.removeAllCanvasHold();
log.info("ExecuteShutdown done: removeAllCanvasHold finished.");
} catch (Exception e) {
// Do not block shutdown, but need complete logging
log.error("ExecuteShutdown failed while removing canvas hold.", e);
} finally {
boolean released = false;
try {
released = redisUtil.unlock(LOCK_KEY, token);
} catch (Exception e) {
log.warn("ExecuteShutdown unlock threw exception. key={}, tokenTail=***{}", LOCK_KEY, tail4(token), e);
}
if (!released) {
log.warn("ExecuteShutdown unlock not released (maybe expired or token mismatch). key={}, tokenTail=***{}",
LOCK_KEY, tail4(token));
} else {
log.debug("ExecuteShutdown lock released. key={}", LOCK_KEY);
}
}
}
private boolean shouldSkipByProfile() {
final String[] actives = environment.getActiveProfiles();
if (actives == null || actives.length == 0) {
return false;
}
for (String p : actives) {
for (String skip : SKIP_PROFILES) {
if (skip.equalsIgnoreCase(p)) {
return true;
}
}View on GitHub (pinned to 5e758547a8)