hiredis.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import asyncio
  2. import socket
  3. import sys
  4. from typing import Callable, List, Optional, Union
  5. if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
  6. from asyncio import timeout as async_timeout
  7. else:
  8. from async_timeout import timeout as async_timeout
  9. from redis.compat import TypedDict
  10. from ..exceptions import ConnectionError, InvalidResponse, RedisError
  11. from ..typing import EncodableT
  12. from ..utils import HIREDIS_AVAILABLE
  13. from .base import AsyncBaseParser, BaseParser
  14. from .socket import (
  15. NONBLOCKING_EXCEPTION_ERROR_NUMBERS,
  16. NONBLOCKING_EXCEPTIONS,
  17. SENTINEL,
  18. SERVER_CLOSED_CONNECTION_ERROR,
  19. )
  20. class _HiredisReaderArgs(TypedDict, total=False):
  21. protocolError: Callable[[str], Exception]
  22. replyError: Callable[[str], Exception]
  23. encoding: Optional[str]
  24. errors: Optional[str]
  25. class _HiredisParser(BaseParser):
  26. "Parser class for connections using Hiredis"
  27. def __init__(self, socket_read_size):
  28. if not HIREDIS_AVAILABLE:
  29. raise RedisError("Hiredis is not installed")
  30. self.socket_read_size = socket_read_size
  31. self._buffer = bytearray(socket_read_size)
  32. def __del__(self):
  33. try:
  34. self.on_disconnect()
  35. except Exception:
  36. pass
  37. def on_connect(self, connection, **kwargs):
  38. import hiredis
  39. self._sock = connection._sock
  40. self._socket_timeout = connection.socket_timeout
  41. kwargs = {
  42. "protocolError": InvalidResponse,
  43. "replyError": self.parse_error,
  44. "errors": connection.encoder.encoding_errors,
  45. }
  46. if connection.encoder.decode_responses:
  47. kwargs["encoding"] = connection.encoder.encoding
  48. self._reader = hiredis.Reader(**kwargs)
  49. self._next_response = False
  50. def on_disconnect(self):
  51. self._sock = None
  52. self._reader = None
  53. self._next_response = False
  54. def can_read(self, timeout):
  55. if not self._reader:
  56. raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
  57. if self._next_response is False:
  58. self._next_response = self._reader.gets()
  59. if self._next_response is False:
  60. return self.read_from_socket(timeout=timeout, raise_on_timeout=False)
  61. return True
  62. def read_from_socket(self, timeout=SENTINEL, raise_on_timeout=True):
  63. sock = self._sock
  64. custom_timeout = timeout is not SENTINEL
  65. try:
  66. if custom_timeout:
  67. sock.settimeout(timeout)
  68. bufflen = self._sock.recv_into(self._buffer)
  69. if bufflen == 0:
  70. raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
  71. self._reader.feed(self._buffer, 0, bufflen)
  72. # data was read from the socket and added to the buffer.
  73. # return True to indicate that data was read.
  74. return True
  75. except socket.timeout:
  76. if raise_on_timeout:
  77. raise TimeoutError("Timeout reading from socket")
  78. return False
  79. except NONBLOCKING_EXCEPTIONS as ex:
  80. # if we're in nonblocking mode and the recv raises a
  81. # blocking error, simply return False indicating that
  82. # there's no data to be read. otherwise raise the
  83. # original exception.
  84. allowed = NONBLOCKING_EXCEPTION_ERROR_NUMBERS.get(ex.__class__, -1)
  85. if not raise_on_timeout and ex.errno == allowed:
  86. return False
  87. raise ConnectionError(f"Error while reading from socket: {ex.args}")
  88. finally:
  89. if custom_timeout:
  90. sock.settimeout(self._socket_timeout)
  91. def read_response(self, disable_decoding=False):
  92. if not self._reader:
  93. raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
  94. # _next_response might be cached from a can_read() call
  95. if self._next_response is not False:
  96. response = self._next_response
  97. self._next_response = False
  98. return response
  99. if disable_decoding:
  100. response = self._reader.gets(False)
  101. else:
  102. response = self._reader.gets()
  103. while response is False:
  104. self.read_from_socket()
  105. if disable_decoding:
  106. response = self._reader.gets(False)
  107. else:
  108. response = self._reader.gets()
  109. # if the response is a ConnectionError or the response is a list and
  110. # the first item is a ConnectionError, raise it as something bad
  111. # happened
  112. if isinstance(response, ConnectionError):
  113. raise response
  114. elif (
  115. isinstance(response, list)
  116. and response
  117. and isinstance(response[0], ConnectionError)
  118. ):
  119. raise response[0]
  120. return response
  121. class _AsyncHiredisParser(AsyncBaseParser):
  122. """Async implementation of parser class for connections using Hiredis"""
  123. __slots__ = ("_reader",)
  124. def __init__(self, socket_read_size: int):
  125. if not HIREDIS_AVAILABLE:
  126. raise RedisError("Hiredis is not available.")
  127. super().__init__(socket_read_size=socket_read_size)
  128. self._reader = None
  129. def on_connect(self, connection):
  130. import hiredis
  131. self._stream = connection._reader
  132. kwargs: _HiredisReaderArgs = {
  133. "protocolError": InvalidResponse,
  134. "replyError": self.parse_error,
  135. }
  136. if connection.encoder.decode_responses:
  137. kwargs["encoding"] = connection.encoder.encoding
  138. kwargs["errors"] = connection.encoder.encoding_errors
  139. self._reader = hiredis.Reader(**kwargs)
  140. self._connected = True
  141. def on_disconnect(self):
  142. self._connected = False
  143. async def can_read_destructive(self):
  144. if not self._connected:
  145. raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
  146. if self._reader.gets():
  147. return True
  148. try:
  149. async with async_timeout(0):
  150. return await self.read_from_socket()
  151. except asyncio.TimeoutError:
  152. return False
  153. async def read_from_socket(self):
  154. buffer = await self._stream.read(self._read_size)
  155. if not buffer or not isinstance(buffer, bytes):
  156. raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
  157. self._reader.feed(buffer)
  158. # data was read from the socket and added to the buffer.
  159. # return True to indicate that data was read.
  160. return True
  161. async def read_response(
  162. self, disable_decoding: bool = False
  163. ) -> Union[EncodableT, List[EncodableT]]:
  164. # If `on_disconnect()` has been called, prohibit any more reads
  165. # even if they could happen because data might be present.
  166. # We still allow reads in progress to finish
  167. if not self._connected:
  168. raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
  169. response = self._reader.gets()
  170. while response is False:
  171. await self.read_from_socket()
  172. response = self._reader.gets()
  173. # if the response is a ConnectionError or the response is a list and
  174. # the first item is a ConnectionError, raise it as something bad
  175. # happened
  176. if isinstance(response, ConnectionError):
  177. raise response
  178. elif (
  179. isinstance(response, list)
  180. and response
  181. and isinstance(response[0], ConnectionError)
  182. ):
  183. raise response[0]
  184. return response