Skip to content

Ignore stale kqueue events for an already-deregistered key - #3502

Open
afonsojanu wants to merge 1 commit into
python-trio:mainfrom
afonsojanu:fix/kqueue-stale-event-keyerror
Open

Ignore stale kqueue events for an already-deregistered key#3502
afonsojanu wants to merge 1 commit into
python-trio:mainfrom
afonsojanu:fix/kqueue-stale-event-keyerror

Conversation

@afonsojanu

Copy link
Copy Markdown

Closes #3500.

With a PySide6 app running trio in guest mode, process_events() in the kqueue backend can occasionally blow up with KeyError: (46, -2), which guest mode then wraps into a fatal TrioInternalError.

The root cause is a timing gap between get_events() and process_events(). In guest mode, GuestState.guest_tick fetches a batch of ready kqueue events with a non-blocking get_events(0) right after resuming the scheduler for the current tick, then hands that batch off to be delivered on the next tick, once the host loop gets around to scheduling it. notify_closing() is a plain synchronous function, not tied to a checkpoint, so it can run in that gap and remove the fd/filter key from _registered before the already-fetched event for that same key gets delivered. process_events() looked the key up with a bare self._registered[key], so a stale event for a since-deregistered key crashed the whole run.

The epoll backend doesn't have this problem because its _registered is a defaultdict, so a lookup for a missing key just returns an empty waiter set instead of raising. This PR gives kqueue the same tolerance: a missing key means there's nothing left to wake up, so the event is skipped.

Added a regression test that constructs a KqueueIOManager directly, registers then deregisters a key (mimicking what notify_closing does), and feeds a matching event straight into process_events(). It reproduces the exact KeyError on the old code and passes with the fix. Ran the full _core test suite locally on macOS (arm64), 277 passed.

In guest mode, get_events() can grab a batch of ready kqueue events
ahead of when they actually reach process_events() on a later tick.
notify_closing() can run in that window and remove the same key from
_registered, since it's a plain synchronous call rather than something
gated behind a checkpoint. When the stale event then shows up,
process_events() looked it up with a bare dict subscript and blew up
with a KeyError, which guest mode wraps into a fatal TrioInternalError.

This matches issue python-trio#3500, where a PySide6 app running trio in guest
mode occasionally crashed with KeyError: (46, -2) from exactly this
path. Treat a missing key the same way the epoll backend already does:
there's nothing left to wake up, so just move on to the next event.
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00000%. Comparing base (d5eb534) to head (0125a8b).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@               Coverage Diff               @@
##                 main        #3502   +/-   ##
===============================================
  Coverage   100.00000%   100.00000%           
===============================================
  Files             128          128           
  Lines           19452        19467   +15     
  Branches         1321         1322    +1     
===============================================
+ Hits            19452        19467   +15     
Files with missing lines Coverage Δ
src/trio/_core/_io_kqueue.py 100.00000% <100.00000%> (ø)
src/trio/_core/_tests/test_io.py 100.00000% <100.00000%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@A5rocks

A5rocks commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Please first write a reproducer that doesn't look into the attributes for the kqueue manager. You claim notify_closing is enough; then just use that!

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.

KeyError in KqueueIOManager.process_events (TrioInternalError)

2 participants