Skip to content

Commit b14f0ab

Browse files
authored
gh-110038: KqueueSelector must count all read/write events (#110039)
1 parent 7e0fbf5 commit b14f0ab

File tree

3 files changed

+38
-1
lines changed

3 files changed

+38
-1
lines changed

Lib/selectors.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ class KqueueSelector(_BaseSelectorImpl):
491491
def __init__(self):
492492
super().__init__()
493493
self._selector = select.kqueue()
494+
self._max_events = 0
494495

495496
def fileno(self):
496497
return self._selector.fileno()
@@ -502,10 +503,12 @@ def register(self, fileobj, events, data=None):
502503
kev = select.kevent(key.fd, select.KQ_FILTER_READ,
503504
select.KQ_EV_ADD)
504505
self._selector.control([kev], 0, 0)
506+
self._max_events += 1
505507
if events & EVENT_WRITE:
506508
kev = select.kevent(key.fd, select.KQ_FILTER_WRITE,
507509
select.KQ_EV_ADD)
508510
self._selector.control([kev], 0, 0)
511+
self._max_events += 1
509512
except:
510513
super().unregister(fileobj)
511514
raise
@@ -516,6 +519,7 @@ def unregister(self, fileobj):
516519
if key.events & EVENT_READ:
517520
kev = select.kevent(key.fd, select.KQ_FILTER_READ,
518521
select.KQ_EV_DELETE)
522+
self._max_events -= 1
519523
try:
520524
self._selector.control([kev], 0, 0)
521525
except OSError:
@@ -525,6 +529,7 @@ def unregister(self, fileobj):
525529
if key.events & EVENT_WRITE:
526530
kev = select.kevent(key.fd, select.KQ_FILTER_WRITE,
527531
select.KQ_EV_DELETE)
532+
self._max_events -= 1
528533
try:
529534
self._selector.control([kev], 0, 0)
530535
except OSError:
@@ -537,7 +542,7 @@ def select(self, timeout=None):
537542
# If max_ev is 0, kqueue will ignore the timeout. For consistent
538543
# behavior with the other selector classes, we prevent that here
539544
# (using max). See https://bugs.python.org/issue29255
540-
max_ev = len(self._fd_to_key) or 1
545+
max_ev = self._max_events or 1
541546
ready = []
542547
try:
543548
kev_list = self._selector.control(None, max_ev, timeout)

Lib/test/test_selectors.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,35 @@ def test_select(self):
285285

286286
self.assertEqual([(wr_key, selectors.EVENT_WRITE)], result)
287287

288+
def test_select_read_write(self):
289+
# gh-110038: when a file descriptor is registered for both read and
290+
# write, the two events must be seen on a single call to select().
291+
s = self.SELECTOR()
292+
self.addCleanup(s.close)
293+
294+
sock1, sock2 = self.make_socketpair()
295+
sock2.send(b"foo")
296+
my_key = s.register(sock1, selectors.EVENT_READ | selectors.EVENT_WRITE)
297+
298+
seen_read, seen_write = False, False
299+
result = s.select()
300+
# We get the read and write either in the same result entry or in two
301+
# distinct entries with the same key.
302+
self.assertLessEqual(len(result), 2)
303+
for key, events in result:
304+
self.assertTrue(isinstance(key, selectors.SelectorKey))
305+
self.assertEqual(key, my_key)
306+
self.assertFalse(events & ~(selectors.EVENT_READ |
307+
selectors.EVENT_WRITE))
308+
if events & selectors.EVENT_READ:
309+
self.assertFalse(seen_read)
310+
seen_read = True
311+
if events & selectors.EVENT_WRITE:
312+
self.assertFalse(seen_write)
313+
seen_write = True
314+
self.assertTrue(seen_read)
315+
self.assertTrue(seen_write)
316+
288317
def test_context_manager(self):
289318
s = self.SELECTOR()
290319
self.addCleanup(s.close)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed an issue that caused :meth:`KqueueSelector.select` to not return all
2+
the ready events in some cases when a file descriptor is registered for both
3+
read and write.

0 commit comments

Comments
 (0)