Skip to content

Commit 4042a85

Browse files
authored
fix(core): use selectors to poll connections instead of raw select in threading,gevent,eventlet (#656)
Co-authored-by: lawrentwang <[email protected]> Solve the select limitation on a maximum file handler value and dynamic choose the best poller in the system.
1 parent f585d60 commit 4042a85

10 files changed

+245
-98
lines changed

kazoo/handlers/eventlet.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
import logging
66

77
import eventlet
8-
from eventlet.green import select as green_select
98
from eventlet.green import socket as green_socket
109
from eventlet.green import time as green_time
1110
from eventlet.green import threading as green_threading
11+
from eventlet.green import selectors as green_selectors
1212
from eventlet import queue as green_queue
1313

1414
from kazoo.handlers import utils
1515
import kazoo.python2atexit as python2atexit
16-
16+
from kazoo.handlers.utils import selector_select
1717

1818
LOG = logging.getLogger(__name__)
1919

@@ -41,6 +41,7 @@ class TimeoutError(Exception):
4141

4242
class AsyncResult(utils.AsyncResult):
4343
"""A one-time event that stores a value or an exception"""
44+
4445
def __init__(self, handler):
4546
super(AsyncResult, self).__init__(handler,
4647
green_threading.Condition,
@@ -164,7 +165,8 @@ def create_connection(self, *args, **kwargs):
164165

165166
def select(self, *args, **kwargs):
166167
with _yield_before_after():
167-
return green_select.select(*args, **kwargs)
168+
return selector_select(*args, selectors_module=green_selectors,
169+
**kwargs)
168170

169171
def async_result(self):
170172
return AsyncResult(self)

kazoo/handlers/gevent.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import gevent.queue
1010
import gevent.select
1111
import gevent.thread
12+
import gevent.selectors
13+
14+
from kazoo.handlers.utils import selector_select
15+
1216
try:
1317
from gevent.lock import Semaphore, RLock
1418
except ImportError:
@@ -17,7 +21,6 @@
1721
from kazoo.handlers import utils
1822
from kazoo import python2atexit
1923

20-
2124
_using_libevent = gevent.__version__.startswith('0.')
2225

2326
log = logging.getLogger(__name__)
@@ -84,6 +87,7 @@ def greenlet_worker():
8487
del func # release before possible idle
8588
except self.queue_empty:
8689
continue
90+
8791
return gevent.spawn(greenlet_worker)
8892

8993
def start(self):
@@ -122,7 +126,8 @@ def stop(self):
122126
python2atexit.unregister(self.stop)
123127

124128
def select(self, *args, **kwargs):
125-
return gevent.select.select(*args, **kwargs)
129+
return selector_select(*args, selectors_module=gevent.selectors,
130+
**kwargs)
126131

127132
def socket(self, *args, **kwargs):
128133
return utils.create_tcp_socket(socket)

kazoo/handlers/threading.py

Lines changed: 4 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@
1212
"""
1313
from __future__ import absolute_import
1414

15-
from collections import defaultdict
16-
import errno
17-
from itertools import chain
1815
import logging
19-
import select
2016
import socket
2117
import threading
2218
import time
@@ -25,20 +21,18 @@
2521

2622
import kazoo.python2atexit as python2atexit
2723
from kazoo.handlers import utils
24+
from kazoo.handlers.utils import selector_select
2825

2926
try:
3027
import Queue
3128
except ImportError: # pragma: nocover
3229
import queue as Queue
3330

34-
3531
# sentinel objects
3632
_STOP = object()
3733

3834
log = logging.getLogger(__name__)
3935

40-
_HAS_EPOLL = hasattr(select, "epoll")
41-
4236

4337
def _to_fileno(obj):
4438
if isinstance(obj, six.integer_types):
@@ -65,6 +59,7 @@ class KazooTimeoutError(Exception):
6559

6660
class AsyncResult(utils.AsyncResult):
6761
"""A one-time event that stores a value or an exception"""
62+
6863
def __init__(self, handler):
6964
super(AsyncResult, self).__init__(handler,
7065
threading.Condition,
@@ -133,6 +128,7 @@ def _thread_worker(): # pragma: nocover
133128
del func # release before possible idle
134129
except self.queue_empty:
135130
continue
131+
136132
t = self.spawn(_thread_worker)
137133
return t
138134

@@ -173,82 +169,7 @@ def stop(self):
173169
python2atexit.unregister(self.stop)
174170

175171
def select(self, *args, **kwargs):
176-
# if we have epoll, and select is not expected to work
177-
# use an epoll-based "select". Otherwise don't touch
178-
# anything to minimize changes
179-
if _HAS_EPOLL:
180-
# if the highest fd we've seen is > 1023
181-
if max(map(_to_fileno, chain.from_iterable(args[:3]))) > 1023:
182-
return self._epoll_select(*args, **kwargs)
183-
return self._select(*args, **kwargs)
184-
185-
def _select(self, *args, **kwargs):
186-
timeout = kwargs.pop('timeout', None)
187-
# either the time to give up, or None
188-
end = (time.time() + timeout) if timeout else None
189-
while end is None or time.time() < end:
190-
if end is not None:
191-
# make a list, since tuples aren't mutable
192-
args = list(args)
193-
194-
# set the timeout to the remaining time
195-
args[3] = end - time.time()
196-
try:
197-
return select.select(*args, **kwargs)
198-
except select.error as ex:
199-
# if the system call was interrupted, we'll retry until timeout
200-
# in Python 3, system call interruptions are a native exception
201-
# in Python 2, they are not
202-
errnum = ex.errno if isinstance(ex, OSError) else ex[0]
203-
if errnum == errno.EINTR:
204-
continue
205-
raise
206-
# if we hit our timeout, lets return as a timeout
207-
return ([], [], [])
208-
209-
def _epoll_select(self, rlist, wlist, xlist, timeout=None):
210-
"""epoll-based drop-in replacement for select to overcome select
211-
limitation on a maximum filehandle value
212-
"""
213-
if timeout is None:
214-
timeout = -1
215-
eventmasks = defaultdict(int)
216-
rfd2obj = defaultdict(list)
217-
wfd2obj = defaultdict(list)
218-
xfd2obj = defaultdict(list)
219-
read_evmask = select.EPOLLIN | select.EPOLLPRI # Just in case
220-
221-
def store_evmasks(obj_list, evmask, fd2obj):
222-
for obj in obj_list:
223-
fileno = _to_fileno(obj)
224-
eventmasks[fileno] |= evmask
225-
fd2obj[fileno].append(obj)
226-
227-
store_evmasks(rlist, read_evmask, rfd2obj)
228-
store_evmasks(wlist, select.EPOLLOUT, wfd2obj)
229-
store_evmasks(xlist, select.EPOLLERR, xfd2obj)
230-
231-
poller = select.epoll()
232-
233-
for fileno in eventmasks:
234-
poller.register(fileno, eventmasks[fileno])
235-
236-
try:
237-
events = poller.poll(timeout)
238-
revents = []
239-
wevents = []
240-
xevents = []
241-
for fileno, event in events:
242-
if event & read_evmask:
243-
revents += rfd2obj.get(fileno, [])
244-
if event & select.EPOLLOUT:
245-
wevents += wfd2obj.get(fileno, [])
246-
if event & select.EPOLLERR:
247-
xevents += xfd2obj.get(fileno, [])
248-
finally:
249-
poller.close()
250-
251-
return revents, wevents, xevents
172+
return selector_select(*args, **kwargs)
252173

253174
def socket(self):
254175
return utils.create_tcp_socket(socket)

kazoo/handlers/utils.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22

33
import errno
44
import functools
5+
import os
56
import select
67
import ssl
78
import socket
89
import time
910

11+
from collections import defaultdict
12+
13+
import six
14+
15+
if six.PY34:
16+
import selectors
17+
else:
18+
import selectors2 as selectors
19+
1020
HAS_FNCTL = True
1121
try:
1222
import fcntl
@@ -19,6 +29,7 @@
1929

2030
class AsyncResult(object):
2131
"""A one-time event that stores a value or an exception"""
32+
2233
def __init__(self, handler, condition_factory, timeout_factory):
2334
self._handler = handler
2435
self._exception = _NONE
@@ -126,6 +137,7 @@ def _do_callbacks(self):
126137
else:
127138
functools.partial(callback, self)()
128139

140+
129141
def _set_fd_cloexec(fd):
130142
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
131143
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
@@ -272,14 +284,17 @@ def capture_exceptions(async_result):
272284
:param async_result: An async result implementing :class:`IAsyncResult`
273285
274286
"""
287+
275288
def capture(function):
276289
@functools.wraps(function)
277290
def captured_function(*args, **kwargs):
278291
try:
279292
return function(*args, **kwargs)
280293
except Exception as exc:
281294
async_result.set_exception(exc)
295+
282296
return captured_function
297+
283298
return capture
284299

285300

@@ -291,12 +306,86 @@ def wrap(async_result):
291306
:param async_result: An async result implementing :class:`IAsyncResult`
292307
293308
"""
309+
294310
def capture(function):
295311
@capture_exceptions(async_result)
296312
def captured_function(*args, **kwargs):
297313
value = function(*args, **kwargs)
298314
if value is not None:
299315
async_result.set(value)
300316
return value
317+
301318
return captured_function
319+
302320
return capture
321+
322+
323+
def fileobj_to_fd(fileobj):
324+
"""Return a file descriptor from a file object.
325+
326+
Parameters:
327+
fileobj -- file object or file descriptor
328+
329+
Returns:
330+
corresponding file descriptor
331+
332+
Raises:
333+
TypeError if the object is invalid
334+
"""
335+
if isinstance(fileobj, int):
336+
fd = fileobj
337+
else:
338+
try:
339+
fd = int(fileobj.fileno())
340+
except (AttributeError, TypeError, ValueError):
341+
raise TypeError("Invalid file object: "
342+
"{!r}".format(fileobj))
343+
if fd < 0:
344+
raise TypeError("Invalid file descriptor: {}".format(fd))
345+
os.fstat(fd)
346+
return fd
347+
348+
349+
def selector_select(rlist, wlist, xlist, timeout=None,
350+
selectors_module=selectors):
351+
"""Selector-based drop-in replacement for select to overcome select
352+
limitation on a maximum filehandle value.
353+
354+
Need backport selectors2 package in python 2.
355+
"""
356+
if timeout is not None:
357+
if not (isinstance(timeout, six.integer_types) or isinstance(
358+
timeout, float)):
359+
raise TypeError('timeout must be a number')
360+
if timeout < 0:
361+
raise ValueError('timeout must be non-negative')
362+
363+
events_mapping = {selectors_module.EVENT_READ: rlist,
364+
selectors_module.EVENT_WRITE: wlist}
365+
fd_events = defaultdict(int)
366+
fd_fileobjs = defaultdict(list)
367+
368+
for event, fileobjs in events_mapping.items():
369+
for fileobj in fileobjs:
370+
fd = fileobj_to_fd(fileobj)
371+
fd_events[fd] |= event
372+
fd_fileobjs[fd].append(fileobj)
373+
374+
selector = selectors_module.DefaultSelector()
375+
for fd, events in fd_events.items():
376+
selector.register(fd, events)
377+
378+
revents, wevents, xevents = [], [], []
379+
try:
380+
ready = selector.select(timeout)
381+
finally:
382+
selector.close()
383+
384+
for info in ready:
385+
k, events = info
386+
if events & selectors.EVENT_READ:
387+
revents.extend(fd_fileobjs[k.fd])
388+
elif events & selectors.EVENT_WRITE:
389+
wevents.extend(fd_fileobjs[k.fd])
390+
391+
return revents, wevents, xevents

kazoo/tests/test_eventlet_handler.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,28 @@ def broken():
130130
with pytest.raises(IOError):
131131
r.get()
132132

133+
def test_huge_file_descriptor(self):
134+
import resource
135+
from eventlet.green import socket
136+
from kazoo.handlers.utils import create_tcp_socket
137+
138+
try:
139+
resource.setrlimit(resource.RLIMIT_NOFILE, (4096, 4096))
140+
except (ValueError, resource.error):
141+
self.skipTest('couldnt raise fd limit high enough')
142+
fd = 0
143+
socks = []
144+
while fd < 4000:
145+
sock = create_tcp_socket(socket)
146+
fd = sock.fileno()
147+
socks.append(sock)
148+
with start_stop_one() as h:
149+
h.start()
150+
h.select(socks, [], [], 0)
151+
h.stop()
152+
for sock in socks:
153+
sock.close()
154+
133155

134156
class TestEventletClient(test_client.TestClient):
135157
def setUp(self):

0 commit comments

Comments
 (0)