Skip to content

Commit acac4db

Browse files
committed
Use nonblocking sockets instead of selectors for healthy connections
This replaces the work in 3.2.0 to use nonblocking sockets instead of selectors. Selectors proved to be problematic for some environments including eventlet and gevent. Nonblocking sockets should be available in all environments.
1 parent 9ed2132 commit acac4db

File tree

8 files changed

+128
-373
lines changed

8 files changed

+128
-373
lines changed

CHANGES

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
* 3.2.2 (in development)
1+
* 3.3.0 (in development)
22
* Resolve a race condition with the PubSubWorkerThread. #1150
33
* Cleanup socket read error messages. Thanks Vic Yu. #1159
44
* Cleanup the Connection's selector correctly. Thanks Bruce Merry. #1153
@@ -17,6 +17,9 @@
1717
cause the connection to be disconnected and cleaned up appropriately.
1818
#923
1919
* Add READONLY and READWRITE commands. Thanks @theodesp. #1114
20+
* Remove selectors in favor of nonblocking sockets. Selectors had
21+
issues in some environments including eventlet and gevent. This should
22+
resolve those issues with no other side effects.
2023
* 3.2.1
2124
* Fix SentinelConnectionPool to work in multiprocess/forked environments.
2225
* 3.2.0

redis/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def int_or_str(value):
2929
return value
3030

3131

32-
__version__ = '3.2.1'
32+
__version__ = '3.3.dev2'
3333
VERSION = tuple(map(int_or_str, __version__.split('.')))
3434

3535
__all__ = [

redis/_compat.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Internal module for Python 2 backwards compatibility."""
22
import errno
3+
import socket
34
import sys
45

56
# For Python older than 3.5, retry EINTR.
67
if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and
78
sys.version_info[1] < 5):
89
# Adapted from https://bugs.python.org/review/23863/patch/14532/54418
9-
import socket
1010
import time
1111

1212
# Wrapper for handling interruptable system calls.
@@ -100,6 +100,7 @@ def byte_to_chr(x):
100100
basestring = basestring
101101
unicode = unicode
102102
long = long
103+
BlockingIOError = socket.error
103104
else:
104105
from urllib.parse import parse_qs, unquote, urlparse
105106
from string import ascii_letters
@@ -129,6 +130,7 @@ def nativestr(x):
129130
unicode = str
130131
safe_unicode = str
131132
long = int
133+
BlockingIOError = BlockingIOError
132134

133135
try: # Python 3
134136
from queue import LifoQueue, Empty, Full

redis/connection.py

Lines changed: 105 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import unicode_literals
22
from distutils.version import StrictVersion
3+
from errno import EWOULDBLOCK
34
from itertools import chain
45
import io
56
import os
@@ -17,7 +18,7 @@
1718
from redis._compat import (xrange, imap, byte_to_chr, unicode, long,
1819
nativestr, basestring, iteritems,
1920
LifoQueue, Empty, Full, urlparse, parse_qs,
20-
recv, recv_into, unquote)
21+
recv, recv_into, unquote, BlockingIOError)
2122
from redis.exceptions import (
2223
DataError,
2324
RedisError,
@@ -31,7 +32,6 @@
3132
ExecAbortError,
3233
ReadOnlyError
3334
)
34-
from redis.selector import DefaultSelector
3535
from redis.utils import HIREDIS_AVAILABLE
3636
if HIREDIS_AVAILABLE:
3737
import hiredis
@@ -61,6 +61,8 @@
6161

6262
SERVER_CLOSED_CONNECTION_ERROR = "Connection closed by server."
6363

64+
SENTINEL = object()
65+
6466

6567
class Encoder(object):
6668
"Encode strings to bytes and decode bytes to strings"
@@ -126,9 +128,10 @@ def parse_error(self, response):
126128

127129

128130
class SocketBuffer(object):
129-
def __init__(self, socket, socket_read_size):
131+
def __init__(self, socket, socket_read_size, socket_timeout):
130132
self._sock = socket
131133
self.socket_read_size = socket_read_size
134+
self.socket_timeout = socket_timeout
132135
self._buffer = io.BytesIO()
133136
# number of bytes written to the buffer from the socket
134137
self.bytes_written = 0
@@ -139,25 +142,51 @@ def __init__(self, socket, socket_read_size):
139142
def length(self):
140143
return self.bytes_written - self.bytes_read
141144

142-
def _read_from_socket(self, length=None):
145+
def _read_from_socket(self, length=None, timeout=SENTINEL,
146+
raise_on_timeout=True):
147+
sock = self._sock
143148
socket_read_size = self.socket_read_size
144149
buf = self._buffer
145150
buf.seek(self.bytes_written)
146151
marker = 0
152+
custom_timeout = timeout is not SENTINEL
147153

148-
while True:
149-
data = recv(self._sock, socket_read_size)
150-
# an empty string indicates the server shutdown the socket
151-
if isinstance(data, bytes) and len(data) == 0:
152-
raise socket.error(SERVER_CLOSED_CONNECTION_ERROR)
153-
buf.write(data)
154-
data_length = len(data)
155-
self.bytes_written += data_length
156-
marker += data_length
157-
158-
if length is not None and length > marker:
159-
continue
160-
break
154+
try:
155+
if custom_timeout:
156+
sock.settimeout(timeout)
157+
while True:
158+
data = recv(self._sock, socket_read_size)
159+
# an empty string indicates the server shutdown the socket
160+
if isinstance(data, bytes) and len(data) == 0:
161+
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
162+
buf.write(data)
163+
data_length = len(data)
164+
self.bytes_written += data_length
165+
marker += data_length
166+
167+
if length is not None and length > marker:
168+
continue
169+
return True
170+
except BlockingIOError as ex:
171+
# if we're in nonblocking mode and the recv raises a
172+
# blocking error, simply return False indicating that
173+
# there's no data to be read. otherwise raise the
174+
# original exception.
175+
if raise_on_timeout or ex.errno != EWOULDBLOCK:
176+
raise
177+
return False
178+
except socket.timeout:
179+
if raise_on_timeout:
180+
raise
181+
return False
182+
finally:
183+
if custom_timeout:
184+
sock.settimeout(self.socket_timeout)
185+
186+
def can_read(self, timeout):
187+
return bool(self.length) or \
188+
self._read_from_socket(timeout=timeout,
189+
raise_on_timeout=False)
161190

162191
def read(self, length):
163192
length = length + 2 # make sure to read the \r\n terminator
@@ -233,7 +262,9 @@ def __del__(self):
233262
def on_connect(self, connection):
234263
"Called when the socket connects"
235264
self._sock = connection._sock
236-
self._buffer = SocketBuffer(self._sock, self.socket_read_size)
265+
self._buffer = SocketBuffer(self._sock,
266+
self.socket_read_size,
267+
connection.socket_timeout)
237268
self.encoder = connection.encoder
238269

239270
def on_disconnect(self):
@@ -244,8 +275,8 @@ def on_disconnect(self):
244275
self._buffer = None
245276
self.encoder = None
246277

247-
def can_read(self):
248-
return self._buffer and bool(self._buffer.length)
278+
def can_read(self, timeout):
279+
return self._buffer and self._buffer.can_read(timeout)
249280

250281
def read_response(self):
251282
response = self._buffer.readline()
@@ -312,6 +343,7 @@ def __del__(self):
312343

313344
def on_connect(self, connection):
314345
self._sock = connection._sock
346+
self._socket_timeout = connection.socket_timeout
315347
kwargs = {
316348
'protocolError': InvalidResponse,
317349
'replyError': self.parse_error,
@@ -333,13 +365,52 @@ def on_disconnect(self):
333365
self._reader = None
334366
self._next_response = False
335367

336-
def can_read(self):
368+
def can_read(self, timeout):
337369
if not self._reader:
338370
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
339371

340372
if self._next_response is False:
341373
self._next_response = self._reader.gets()
342-
return self._next_response is not False
374+
if self._next_response is False:
375+
return self.read_from_socket(timeout=timeout,
376+
raise_on_timeout=False)
377+
return True
378+
379+
def read_from_socket(self, timeout=SENTINEL, raise_on_timeout=True):
380+
sock = self._sock
381+
custom_timeout = timeout is not SENTINEL
382+
try:
383+
if custom_timeout:
384+
sock.settimeout(timeout)
385+
if HIREDIS_USE_BYTE_BUFFER:
386+
bufflen = recv_into(self._sock, self._buffer)
387+
if bufflen == 0:
388+
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
389+
self._reader.feed(self._buffer, 0, bufflen)
390+
else:
391+
buffer = recv(self._sock, self.socket_read_size)
392+
# an empty string indicates the server shutdown the socket
393+
if not isinstance(buffer, bytes) or len(buffer) == 0:
394+
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
395+
self._reader.feed(buffer)
396+
# data was read from the socket and added to the buffer.
397+
# return True to indicate that data was read.
398+
return True
399+
except BlockingIOError as ex:
400+
# if we're in nonblocking mode and the recv raises a
401+
# blocking error, simply return False indicating that
402+
# there's no data to be read. otherwise raise the
403+
# original exception.
404+
if raise_on_timeout or ex.errno != EWOULDBLOCK:
405+
raise
406+
return False
407+
except socket.timeout:
408+
if not raise_on_timeout:
409+
raise
410+
return False
411+
finally:
412+
if custom_timeout:
413+
sock.settimeout(self._socket_timeout)
343414

344415
def read_response(self):
345416
if not self._reader:
@@ -352,21 +423,8 @@ def read_response(self):
352423
return response
353424

354425
response = self._reader.gets()
355-
socket_read_size = self.socket_read_size
356426
while response is False:
357-
if HIREDIS_USE_BYTE_BUFFER:
358-
bufflen = recv_into(self._sock, self._buffer)
359-
if bufflen == 0:
360-
raise socket.error(SERVER_CLOSED_CONNECTION_ERROR)
361-
else:
362-
buffer = recv(self._sock, socket_read_size)
363-
# an empty string indicates the server shutdown the socket
364-
if not isinstance(buffer, bytes) or len(buffer) == 0:
365-
raise socket.error(SERVER_CLOSED_CONNECTION_ERROR)
366-
if HIREDIS_USE_BYTE_BUFFER:
367-
self._reader.feed(self._buffer, 0, bufflen)
368-
else:
369-
self._reader.feed(buffer)
427+
self.read_from_socket()
370428
response = self._reader.gets()
371429
# if an older version of hiredis is installed, we need to attempt
372430
# to convert ResponseErrors to their appropriate types.
@@ -416,7 +474,6 @@ def __init__(self, host='localhost', port=6379, db=0, password=None,
416474
self.retry_on_timeout = retry_on_timeout
417475
self.encoder = Encoder(encoding, encoding_errors, decode_responses)
418476
self._sock = None
419-
self._selector = None
420477
self._parser = parser_class(socket_read_size=socket_read_size)
421478
self._description_args = {
422479
'host': self.host,
@@ -454,7 +511,6 @@ def connect(self):
454511
raise ConnectionError(self._error_message(e))
455512

456513
self._sock = sock
457-
self._selector = DefaultSelector(sock)
458514
try:
459515
self.on_connect()
460516
except RedisError:
@@ -538,9 +594,6 @@ def disconnect(self):
538594
self._parser.on_disconnect()
539595
if self._sock is None:
540596
return
541-
if self._selector is not None:
542-
self._selector.close()
543-
self._selector = None
544597
try:
545598
if os.getpid() == self.pid:
546599
self._sock.shutdown(socket.SHUT_RDWR)
@@ -585,11 +638,7 @@ def can_read(self, timeout=0):
585638
if not sock:
586639
self.connect()
587640
sock = self._sock
588-
return self._parser.can_read() or self._selector.can_read(timeout)
589-
590-
def is_ready_for_command(self):
591-
"Check if the connection is ready for a command"
592-
return self._selector.is_ready_for_command()
641+
return self._parser.can_read(timeout)
593642

594643
def read_response(self):
595644
"Read the response from a previously sent command"
@@ -963,10 +1012,13 @@ def get_connection(self, command_name, *keys, **options):
9631012
# a command. if not, the connection was either returned to the
9641013
# pool before all data has been read or the socket has been
9651014
# closed. either way, reconnect and verify everything is good.
966-
if not connection.is_ready_for_command():
1015+
try:
1016+
if connection.can_read():
1017+
raise ConnectionError('Connection has data')
1018+
except ConnectionError:
9671019
connection.disconnect()
9681020
connection.connect()
969-
if not connection.is_ready_for_command():
1021+
if connection.can_read():
9701022
raise ConnectionError('Connection not ready')
9711023
except: # noqa: E722
9721024
# release the connection back to the pool so that we don't leak it
@@ -1111,10 +1163,13 @@ def get_connection(self, command_name, *keys, **options):
11111163
# a command. if not, the connection was either returned to the
11121164
# pool before all data has been read or the socket has been
11131165
# closed. either way, reconnect and verify everything is good.
1114-
if not connection.is_ready_for_command():
1166+
try:
1167+
if connection.can_read():
1168+
raise ConnectionError('Connection has data')
1169+
except ConnectionError:
11151170
connection.disconnect()
11161171
connection.connect()
1117-
if not connection.is_ready_for_command():
1172+
if connection.can_read():
11181173
raise ConnectionError('Connection not ready')
11191174
except: # noqa: E722
11201175
# release the connection back to the pool so that we don't leak it

0 commit comments

Comments
 (0)