iflytek/astron-agent · warning

ExecuteShutdown unlock threw exception. key=

Error message

ExecuteShutdown unlock threw exception. key={}, tokenTail=***{}

What it means

In ExecuteShutdown.onShutdown, the distributed Redis lock held by the shutdown hook is released via redisUtil.unlock(LOCK_KEY, token). If the unlock call itself throws (Redis connection down at JVM shutdown, serialization issue, etc.), this warning is logged with the key and the last-4 chars of the token, and the lock is left to expire via its TTL rather than being released. The shutdown still completes.

Solutions

  1. Rely on the lock's TTL: since unlock failed the lock will expire on its own; verify the LOCK_KEY TTL is short enough that stale holds don't block the next ExecuteShutdown run.
  2. Check why Redis was unavailable at shutdown — connection pool shutdown ordering in Spring context (add a DependsOn / close the Redis client after this hook), or network policy.
  3. Make unlock retry once with a short backoff inside the hook, or use Redisson's lock.unlock() (idempotent, with built-in Lua token check) instead of a hand-rolled unlock.
Defensive patterns

Strategy: retry

Validate before calling

if (!redisUtil.hasKey(LOCK_KEY)) {
    log.info("lock already gone; skip unlock");
}

Try / catch

try {
    if (!redisUtil.unlock(LOCK_KEY, token)) {
        log.warn("unlock rejected; lock will expire via TTL, key={}", LOCK_KEY);
    }
} catch (Exception e) {
    log.warn("unlock failed at shutdown; relying on TTL, key={}", LOCK_KEY, e);
}

Prevention

When it happens

Trigger: Redis is unreachable or times out when the shutdown hook runs; redisUtil.unlock throws (JedisConnectionException, pool exhaustion) inside the finally block of onShutdown.

Common situations: Kubernetes pod termination during a Redis failover; Redis connection pool already closed by earlier shutdown-hook ordering; network partition between app and Redis at SIGTERM.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12). Data as JSON: /api/errors/f6e05e438749add4. Report an issue: GitHub.

Appendix: source

Thrown at console/backend/toolkit/src/main/java/com/iflytek/astron/console/toolkit/config/spring/ExecuteShutdown.java:64

        if (!redisUtil.tryLock(LOCK_KEY, LOCK_TTL, token)) {
            log.info("ExecuteShutdown skipped: lock already held by another instance. key={}", LOCK_KEY);
            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)) {

View on GitHub (pinned to 5e758547a8)