Skip to content

Drop the stale-client close message in sync runs - #97

Merged
kelkalot merged 1 commit into
devfrom
fix/sync-run-event-loop-noise
Oct 2, 2026
Merged

kelkalot merged 1 commit into
devfrom
fix/sync-run-event-loop-noise

Conversation

@kelkalot

@kelkalot kelkalot commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #96. This commit was pushed after #96 merged.

After several run() calls in one process, asyncio printed "Task exception was never retrieved … RuntimeError: Event loop is closed". This showed up in the live run that #96 came from. Results were not affected.

Cause. Each sync run() calls asyncio.run(), so each run has its own event loop. OpenAI SDK clients sit in reference cycles and are freed whenever the cyclic GC runs. If that happens while a later run's loop is running, the client's __del__ schedules aclose() on that loop for connections opened on the earlier, closed one, and the task fails. Reusing one auditor across runs is fine: three consecutive run() calls on one auditor worked live.

Change. ModelAuditor.run, AuditExperiment.run, CrossJudgeExperiment.run and reframing's _run_sync (rejudge and friends) now go through simpleaudit._event_loop.run_sync. It works like asyncio.run, but sets an exception handler on the loop that drops exactly one case: an AsyncClient.aclose() task failing with "Event loop is closed". Everything else still goes to asyncio's default handler.

The real fix is to close the clients before their loop ends, but any-llm has no public close method. Asked upstream: mozilla-ai/any-llm#1439.

Not covered: run_async() called from your own event loop, for example in a notebook, uses that loop's handler, so the message can still appear there.

Testing

  • pytest on Python 3.11 (-n auto) and 3.13: 1366 passed, 1 skipped.
  • tests/test_event_loop.py, 6 tests:
    • The filter drops only the targeted case and passes everything else to the default handler.
    • A regression test reproduces the message without a network, using a real OpenAI SDK client against a local HTTP server, used in one run and collected during the next. The message appears under asyncio.run and not under run_sync.
    • With the filter removed, the two tests that depend on it fail.

Each sync run() gets its own event loop. The OpenAI SDK's async client
sits in a reference cycle, so one from an earlier run can be garbage-
collected while a later run's loop is running. Its __del__ schedules
aclose() on that loop for connections opened on the earlier one, and
asyncio prints "Task exception was never retrieved ... Event loop is
closed". Results are not affected.

any-llm has no public way to close a provider's client, so the client
cannot be closed before its loop ends. The four sync wrappers
(ModelAuditor.run, AuditExperiment.run, CrossJudgeExperiment.run and
reframing's _run_sync) now go through run_sync, which installs an
exception handler that drops exactly that case: an AsyncClient.aclose()
task failing with "Event loop is closed". Anything else still reaches
asyncio's default handler.

The regression test reproduces it without a network: a real OpenAI
client against a local HTTP server, used in one run and collected
during the next.
@kelkalot
kelkalot requested a review from SushantGautam October 2, 2026 15:10
@kelkalot
kelkalot merged commit e5ec692 into dev Oct 2, 2026
3 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