Skip to content

Commit 202398c

Browse files
jhatch28mjs
authored andcommitted
poll() when available to surpass 1024 file descriptor limit with select() (mjs#377)
1 parent d73bd1f commit 202398c

File tree

2 files changed

+126
-7
lines changed

2 files changed

+126
-7
lines changed

imapclient/imapclient.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
from .util import to_bytes, to_unicode, assert_imap_protocol, chunk
2929
xrange = moves.xrange
3030

31+
try:
32+
from select import poll
33+
POLL_SUPPORT = True
34+
except:
35+
# Fallback to select() on systems that don't support poll()
36+
POLL_SUPPORT = False
37+
3138
if PY3:
3239
long = int # long is just int in python3
3340

@@ -760,6 +767,26 @@ def idle(self):
760767
if resp is not None:
761768
raise exceptions.IMAPClientError('Unexpected IDLE response: %s' % resp)
762769

770+
def _poll_socket(self, sock, timeout=None):
771+
"""
772+
Polls the socket for events telling us it's available to read.
773+
This implementation is more scalable because it ALLOWS your process
774+
to have more than 1024 file descriptors.
775+
"""
776+
poller = select.poll()
777+
poller.register(sock.fileno(), select.POLLIN)
778+
timeout = timeout * 1000 if timeout is not None else None
779+
return poller.poll(timeout)
780+
781+
def _select_poll_socket(self, sock, timeout=None):
782+
"""
783+
Polls the socket for events telling us it's available to read.
784+
This implementation is a fallback because it FAILS if your process
785+
has more than 1024 file descriptors.
786+
We still need this for Windows and some other niche systems.
787+
"""
788+
return select.select([sock], [], [], timeout)[0]
789+
763790
@require_capability('IDLE')
764791
def idle_check(self, timeout=None):
765792
"""Check for any IDLE responses sent by the server.
@@ -785,10 +812,16 @@ def idle_check(self, timeout=None):
785812
# implemented for this call
786813
sock.settimeout(None)
787814
sock.setblocking(0)
815+
816+
if POLL_SUPPORT:
817+
poll_func = self._poll_socket
818+
else:
819+
poll_func = self._select_poll_socket
820+
788821
try:
789822
resps = []
790-
rs, _, _ = select.select([sock], [], [], timeout)
791-
if rs:
823+
events = poll_func(sock, timeout)
824+
if events:
792825
while True:
793826
try:
794827
line = self._imap._get_line()

tests/test_imapclient.py

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import logging
1212

1313
import six
14+
from select import POLLIN
1415

1516
from imapclient.exceptions import (
1617
CapabilityError, IMAPClientError, ProtocolError
@@ -379,14 +380,23 @@ def setUp(self):
379380
super(TestIdleAndNoop, self).setUp()
380381
self.client._cached_capabilities = [b'IDLE']
381382

382-
def assert_sock_calls(self, sock):
383+
def assert_sock_select_calls(self, sock):
383384
self.assertListEqual(sock.method_calls, [
384385
('settimeout', (None,), {}),
385386
('setblocking', (0,), {}),
386387
('setblocking', (1,), {}),
387388
('settimeout', (None,), {}),
388389
])
389390

391+
def assert_sock_poll_calls(self, sock):
392+
self.assertListEqual(sock.method_calls, [
393+
('settimeout', (None,), {}),
394+
('setblocking', (0,), {}),
395+
('fileno', (), {}),
396+
('setblocking', (1,), {}),
397+
('settimeout', (None,), {}),
398+
])
399+
390400
def test_idle(self):
391401
self.client._imap._command.return_value = sentinel.tag
392402
self.client._imap._get_response.return_value = None
@@ -396,6 +406,7 @@ def test_idle(self):
396406
self.client._imap._command.assert_called_with('IDLE')
397407
self.assertEqual(self.client._idle_tag, sentinel.tag)
398408

409+
@patch('imapclient.imapclient.POLL_SUPPORT', False)
399410
@patch('imapclient.imapclient.select.select')
400411
def test_idle_check_blocking(self, mock_select):
401412
mock_sock = Mock()
@@ -416,9 +427,10 @@ def fake_get_line():
416427
responses = self.client.idle_check()
417428

418429
mock_select.assert_called_once_with([mock_sock], [], [], None)
419-
self.assert_sock_calls(mock_sock)
430+
self.assert_sock_select_calls(mock_sock)
420431
self.assertListEqual([(1, b'EXISTS'), (0, b'EXPUNGE')], responses)
421432

433+
@patch('imapclient.imapclient.POLL_SUPPORT', False)
422434
@patch('imapclient.imapclient.select.select')
423435
def test_idle_check_timeout(self, mock_select):
424436
mock_sock = Mock()
@@ -428,9 +440,10 @@ def test_idle_check_timeout(self, mock_select):
428440
responses = self.client.idle_check(timeout=0.5)
429441

430442
mock_select.assert_called_once_with([mock_sock], [], [], 0.5)
431-
self.assert_sock_calls(mock_sock)
443+
self.assert_sock_select_calls(mock_sock)
432444
self.assertListEqual([], responses)
433445

446+
@patch('imapclient.imapclient.POLL_SUPPORT', False)
434447
@patch('imapclient.imapclient.select.select')
435448
def test_idle_check_with_data(self, mock_select):
436449
mock_sock = Mock()
@@ -449,7 +462,80 @@ def fake_get_line():
449462
responses = self.client.idle_check()
450463

451464
mock_select.assert_called_once_with([mock_sock], [], [], None)
452-
self.assert_sock_calls(mock_sock)
465+
self.assert_sock_select_calls(mock_sock)
466+
self.assertListEqual([(99, b'EXISTS')], responses)
467+
468+
@patch('imapclient.imapclient.POLL_SUPPORT', True)
469+
@patch('imapclient.imapclient.select.poll')
470+
def test_idle_check_blocking(self, mock_poll_module):
471+
mock_sock = Mock(fileno=Mock(return_value=1))
472+
self.client._imap.sock = self.client._imap.sslobj = mock_sock
473+
474+
mock_poller = Mock(poll=Mock(return_value=[(1, POLLIN)]))
475+
mock_poll_module.return_value = mock_poller
476+
counter = itertools.count()
477+
478+
def fake_get_line():
479+
count = six.next(counter)
480+
if count == 0:
481+
return b'* 1 EXISTS'
482+
elif count == 1:
483+
return b'* 0 EXPUNGE'
484+
else:
485+
raise socket.timeout
486+
487+
self.client._imap._get_line = fake_get_line
488+
489+
responses = self.client.idle_check()
490+
491+
assert mock_poll_module.call_count == 1
492+
mock_poller.register.assert_called_once_with(1, POLLIN)
493+
mock_poller.poll.assert_called_once_with(None)
494+
self.assert_sock_poll_calls(mock_sock)
495+
self.assertListEqual([(1, b'EXISTS'), (0, b'EXPUNGE')], responses)
496+
497+
@patch('imapclient.imapclient.POLL_SUPPORT', True)
498+
@patch('imapclient.imapclient.select.poll')
499+
def test_idle_check_timeout(self, mock_poll_module):
500+
mock_sock = Mock(fileno=Mock(return_value=1))
501+
self.client._imap.sock = self.client._imap.sslobj = mock_sock
502+
503+
mock_poller = Mock(poll=Mock(return_value=[]))
504+
mock_poll_module.return_value = mock_poller
505+
506+
responses = self.client.idle_check(timeout=0.5)
507+
508+
assert mock_poll_module.call_count == 1
509+
mock_poller.register.assert_called_once_with(1, POLLIN)
510+
mock_poller.poll.assert_called_once_with(500)
511+
self.assert_sock_poll_calls(mock_sock)
512+
self.assertListEqual([], responses)
513+
514+
@patch('imapclient.imapclient.POLL_SUPPORT', True)
515+
@patch('imapclient.imapclient.select.poll')
516+
def test_idle_check_with_data(self, mock_poll_module):
517+
mock_sock = Mock(fileno=Mock(return_value=1))
518+
self.client._imap.sock = self.client._imap.sslobj = mock_sock
519+
520+
mock_poller = Mock(poll=Mock(return_value=[(1, POLLIN)]))
521+
mock_poll_module.return_value = mock_poller
522+
counter = itertools.count()
523+
524+
def fake_get_line():
525+
count = six.next(counter)
526+
if count == 0:
527+
return b'* 99 EXISTS'
528+
else:
529+
raise socket.timeout
530+
531+
self.client._imap._get_line = fake_get_line
532+
533+
responses = self.client.idle_check()
534+
535+
assert mock_poll_module.call_count == 1
536+
mock_poller.register.assert_called_once_with(1, POLLIN)
537+
mock_poller.poll.assert_called_once_with(None)
538+
self.assert_sock_poll_calls(mock_sock)
453539
self.assertListEqual([(99, b'EXISTS')], responses)
454540

455541
def test_idle_done(self):
@@ -835,4 +921,4 @@ def test_tagged_response_with_parse_error(self):
835921
client._imap._get_response = lambda: b'NOT-A-STAR 99 EXISTS'
836922

837923
with self.assertRaises(ProtocolError):
838-
client._consume_until_tagged_response(sentinel.tag, b'IDLE')
924+
client._consume_until_tagged_response(sentinel.tag, b'IDLE')

0 commit comments

Comments
 (0)