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

  1. 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.
  2. 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.
  3. 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

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


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)