Skip to content

fix: back off when the watchdog rebuild is triggered by degradation - #399

Merged
Jeomon merged 1 commit into
mainfrom
fix/watchdog-backoff
Aug 29, 2026
Merged

fix: back off when the watchdog rebuild is triggered by degradation#399
Jeomon merged 1 commit into
mainfrom
fix/watchdog-backoff

Conversation

@Jeomon

@Jeomon Jeomon commented Aug 29, 2026

Copy link
Copy Markdown
Member

Refs #332. This is a bug in the recovery loop added by #334, not the terminal crash — see the caveat at the bottom.

The bug

_run reset backoff = 1.0 on any clean return from _event_loop, with the comment "clean exit (stopped or rebuild requested)". But a rebuild requested by FAIL_THRESHOLD consecutive COMErrors returns exactly that way — _event_loop exits normally with _needs_rebuild set. Treating that as clean means the exponential backoff only ever applied to the exception path.

The consequence in the environment #332 describes, where the focus pipeline is persistently degraded:

  1. five focus events fail, each logging at DEBUG
  2. request_rebuild() fires
  3. _event_loop returns, logging WatchDog event pipeline degraded, rebuilding UIA client at WARNING
  4. backoff resets to 1.0, waits one second
  5. a fresh IUIAutomation client is built and immediately degrades again

Repeat forever, once a second. That is the "thousands of identical lines" in the original report, plus a COM client constructed and torn down every second — churn that plausibly contributes to the state collapse rather than damping it.

The fix

Reset the backoff only after a run that survived HEALTHY_RUN_SECONDS (60s). An intermittent glitch after hours of healthy operation still retries in one second, as intended; an environment that degrades the moment it rebuilds escalates 1, 2, 4, 8 … up to the existing 30s cap.

Verification

Six tests in tests/test_watchdog_backoff.py drive _run directly with every COM touchpoint stubbed, capturing what it would have slept.

Checked against the pre-fix code, which is how the diagnosis was confirmed rather than assumed:

before:  waits == [1.0, 1.0, 1.0, 1.0]        # never escalates
after:   waits == [1.0, 2.0, 4.0, 8.0]        # and caps at 30.0

Twelve consecutive immediate degradations produced twelve 1.0s waits before, and reach the 30s cap now. The exception path, the "long healthy run resets backoff" path, the stop path, and #334's contract that each cycle builds a fresh client with clean failure counters are all covered too.

Full suite: 587 passed.

What this does not fix

The terminal -2147418113 / E_UNEXPECTED crash from the original report is untouched. That is a native access violation during PumpEvents, which no Python except can catch, and isolating the server from it needs the watchdog moved out of process. This change reduces how hard and how often the watchdog hammers a collapsing UIA stack on the way there, and makes the degradation legible in the log instead of drowning it — but #332 should stay open for the out-of-process work.

The rebuild loop reset backoff to 1s on any clean return from _event_loop,
and a rebuild requested by FAIL_THRESHOLD consecutive COMErrors returns
exactly that way. So a UIA stack that degrades immediately on every rebuild
was retried once a second forever: a fresh COM client built and torn down
per second, and one "WatchDog event pipeline degraded" warning per second.
The exponential backoff only ever applied to the exception path.

Reset backoff only after a run that survived HEALTHY_RUN_SECONDS, so an
intermittent glitch after hours of healthy operation still retries promptly,
while a persistently broken environment escalates to the 30s cap. Verified
against the old code: twelve consecutive immediate degradations produced
twelve 1.0s waits, where they now escalate 1, 2, 4, 8 ... 30.

This is the log flood in #332 and the client churn behind it. It does not
address the terminal E_UNEXPECTED crash, which is a native access violation
no Python except can catch and needs the watchdog out of process.

Refs #332
monkeypatch.setattr(watchdog_service.comtypes, "CoUninitialize", lambda: None)

watchdog = WatchDog()
monkeypatch.setattr(watchdog, "_create_uia", lambda: object())
@Jeomon
Jeomon merged commit aea715f into main Aug 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant