Skip to content

bug(lifespan): shutdown handler joins its own thread, and its except swallows every later cleanup error #13585

Closed

Description

@mrveiss

Observed in the 2026-08-04 log sweep: initialization.lifespan - ERROR - Error during shutdown: cannot join current thread (x4). Runs on every restart.

Cause

autobot-backend/initialization/lifespan.py:2406

    except Exception as e:
        logger.error("Error during shutdown: %s", e)

Two problems:

  1. The join itself — something in the shutdown path calls .join() on the thread it is currently running in, which always raises RuntimeError: cannot join current thread.
  2. The blast radius — that single except wraps the whole cleanup body. The first failure aborts every subsequent step, and the only trace is one ERROR line with no traceback. Today the first failure is the cosmetic self-join, which means any real cleanup below it has been silently skipped on every restart for as long as this has been firing.

The second point is why this is not cosmetic: the log line understates what was lost, and there is no way to tell from the log which cleanup steps ran.

Acceptance criteria

  • Identify and fix the self-join (do not join the current thread)
  • Cleanup steps fail independently — one failing step does not skip the rest
  • The handler logs exc_info=True so the failing step is identifiable from logs alone
  • Test asserts a raising cleanup step does not prevent later steps from running

Found while sweeping logs during the #12907/#12959 session.

Activity

  1. mrveiss commented on Aug 23, 2026

    @mrveiss
    OwnerAuthor

    Fixed in PR #14898, which takes this together with the four other items of the same defect class under umbrella #13852: a failed or impossible operation reporting success or health. Every fix is mutation-checked (the fixed line broken in a scratch copy, the test confirmed red, then discarded); the mutation table is in the PR body.

  2. mrveiss commented on Aug 23, 2026

    @mrveiss
    OwnerAuthor

    Delivered and merged as 42ac95b53 (PR #14898).

    • CI: all green, deduped to the latest run per check name on head a1721728c.
    • Review: independent code-reviewer pass. Verdict: ready to merge. It traced every one of the five fixes for a new silent failure one layer up — a health aggregator that cannot be reached, a counter whose backend is down, a monitor whose probe binary is absent — and found none.

    Verified in base after merge: zero || echo fabricating sentinels remain in access_control_monitor.sh, and _KNOWN_FABRICATING is genuinely emptied rather than the guard being weakened to accommodate the change.

    How each failure is now observable:

    Issue Before Now
    #14851 /status always healthy unreachable health source returns success=False and UNKNOWN naming the exception type; an empty probe registry counts as failure
    #12969 load_catalog returned True either way returns False on fallback; ERROR names every path searched in order; probe reports degraded while built-ins are in use
    #13585 one except swallowed every later cleanup error each step guarded independently; closing line reports "teardown was INCOMPLETE" with the failed step names
    #14843 file-backed denial audit failed silently increments autobot_audit_write_failures_total{action,error_type}, the same counter the RBAC middleware uses
    #14880 getters fabricated UNKNOWN/{}/0|0|0 print nothing, return non-zero; consumers print explicit CHECK FAILED; script exits 2

    #12969 was deliberately not closed. Its acceptance is host evidence (Loaded error catalog ... from <path>, no fallback warning) that a checkout cannot produce, and the second candidate path fixes a layout inferred from a log rather than observed. It stays open.

    A severity claim in this PR was overstated and has been corrected — see the correction on #14880. The backticks executing as shell commands produced two spurious stderr lines per run, not corrupted output; the reviewer reproduced the exact structure and measured the captured value as correct. The genuinely serious bug found alongside it stands: the monitor's pre-flight imported backend.services.feature_flags, a module that has never existed here, so main() exited 1 before the dashboard ran on every invocation.

    Reviewer notes carried forward, neither blocking: cleanup_services(app)'s new list[str] return is not consumed by its only caller (the failure signal is fully visible in the log either way), and /api/system/health itself still reports ok on zero probes — clamped at the consumer instead, a disclosed scope boundary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions