Skip to content

Commit ac9090d

Browse files
committed
gh-81207: Handle socketserver timeouts at the deadline
Treat an empty selector result at the deadline as a timeout instead of polling again with zero remaining time. Cover exact and elapsed deadlines, early wakeups, and ready requests with deterministic clock and selector tests.
1 parent e5ef47e commit ac9090d

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

‎Lib/socketserver.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def handle_request(self):
296296
else:
297297
if timeout is not None:
298298
timeout = deadline - time()
299-
if timeout < 0:
299+
if timeout <= 0:
300300
return self.handle_timeout()
301301

302302
def _handle_request_noblock(self):

‎Lib/test/test_socketserver.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import threading
1212
import unittest
1313
import socketserver
14+
from unittest import mock
1415

1516
import test.support
1617
from test.support import reap_children, verbose
@@ -467,6 +468,59 @@ def run_client():
467468

468469
class MiscTestCase(unittest.TestCase):
469470

471+
def test_handle_request_timeout_at_deadline(self):
472+
# Let a second poll advance past the deadline so a regression fails
473+
# instead of looping forever on a frozen clock.
474+
cases = (
475+
(0, [10, 10, 11], [0]),
476+
(1, [10, 11, 12], [1]),
477+
(1, [10, 10.25, 11, 12], [1, 0.75]),
478+
(1, [10, 11.25], [1]),
479+
)
480+
for timeout, clock, expected_timeouts in cases:
481+
with self.subTest(timeout=timeout, clock=clock):
482+
with socketserver.TCPServer(
483+
(HOST, 0), socketserver.BaseRequestHandler
484+
) as server:
485+
server.timeout = timeout
486+
with (
487+
mock.patch('socketserver.time', side_effect=clock),
488+
mock.patch('socketserver._ServerSelector') as selector_cls,
489+
mock.patch.object(
490+
server, 'handle_timeout',
491+
return_value=mock.sentinel.timeout,
492+
) as handle_timeout,
493+
):
494+
selector = selector_cls.return_value.__enter__.return_value
495+
selector.select.return_value = []
496+
result = server.handle_request()
497+
self.assertIs(result, mock.sentinel.timeout)
498+
self.assertEqual(selector.select.call_args_list,
499+
[mock.call(t) for t in expected_timeouts])
500+
handle_timeout.assert_called_once_with()
501+
502+
def test_handle_request_ready_with_zero_timeout(self):
503+
with socketserver.TCPServer(
504+
(HOST, 0), socketserver.BaseRequestHandler
505+
) as server:
506+
server.timeout = 0
507+
with (
508+
mock.patch('socketserver.time', return_value=10),
509+
mock.patch('socketserver._ServerSelector') as selector_cls,
510+
mock.patch.object(
511+
server, '_handle_request_noblock',
512+
return_value=mock.sentinel.request,
513+
) as handle_request,
514+
mock.patch.object(server, 'handle_timeout') as handle_timeout,
515+
):
516+
selector = selector_cls.return_value.__enter__.return_value
517+
selector.select.return_value = [mock.sentinel.ready]
518+
result = server.handle_request()
519+
self.assertIs(result, mock.sentinel.request)
520+
selector.select.assert_called_once_with(0)
521+
handle_request.assert_called_once_with()
522+
handle_timeout.assert_not_called()
523+
470524
def test_all(self):
471525
# objects defined in the module should be in __all__
472526
expected = []
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a potential busy loop in :meth:`socketserver.BaseServer.handle_request`
2+
when the timeout expires exactly at the deadline.

0 commit comments

Comments
 (0)