1import errno
2import io
3import socket
4from io import SEEK_END
5from typing import Optional, Union
6
7from ..exceptions import ConnectionError, TimeoutError
8from ..utils import SENTINEL, SSL_AVAILABLE
9
10NONBLOCKING_EXCEPTION_ERROR_NUMBERS = {BlockingIOError: errno.EWOULDBLOCK}
11
12if SSL_AVAILABLE:
13 import ssl
14
15 if hasattr(ssl, "SSLWantReadError"):
16 NONBLOCKING_EXCEPTION_ERROR_NUMBERS[ssl.SSLWantReadError] = 2
17 NONBLOCKING_EXCEPTION_ERROR_NUMBERS[ssl.SSLWantWriteError] = 2
18 else:
19 NONBLOCKING_EXCEPTION_ERROR_NUMBERS[ssl.SSLError] = 2
20
21NONBLOCKING_EXCEPTIONS = tuple(NONBLOCKING_EXCEPTION_ERROR_NUMBERS.keys())
22
23SERVER_CLOSED_CONNECTION_ERROR = "Connection closed by server."
24
25SYM_CRLF = b"\r\n"
26
27
28class SocketBuffer:
29 def __init__(
30 self, socket: socket.socket, socket_read_size: int, socket_timeout: float
31 ):
32 self._sock = socket
33 self.socket_read_size = socket_read_size
34 self.socket_timeout = socket_timeout
35 self._buffer = io.BytesIO()
36
37 def _live_buffer(self) -> io.BytesIO:
38 """
39 The read buffer, or a ConnectionError when the connection is already gone.
40
41 ``close()`` closes the buffer before dropping it, and it can run while another
42 thread reads: the multi-database client closes connections from its health
43 check thread when a database is taken out of service, so a reader finds either
44 a closed ``BytesIO`` or ``None``. The connection is gone either way, so report
45 it the way every other teardown here is reported and let the retry layers act
46 on it, instead of surfacing ``ValueError: I/O operation on closed file`` to the
47 caller of the command.
48 """
49 buffer = self._buffer
50
51 if buffer is None or buffer.closed:
52 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
53
54 return buffer
55
56 def unread_bytes(self) -> int:
57 """
58 Remaining unread length of buffer
59 """
60 buffer = self._live_buffer()
61
62 try:
63 pos = buffer.tell()
64 end = buffer.seek(0, SEEK_END)
65 buffer.seek(pos)
66 except ValueError:
67 # Closed between the check above and here.
68 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
69
70 return end - pos
71
72 def _read_from_socket(
73 self,
74 length: Optional[int] = None,
75 timeout: Union[float, object] = SENTINEL,
76 raise_on_timeout: Optional[bool] = True,
77 ) -> bool:
78 sock = self._sock
79 socket_read_size = self.socket_read_size
80 marker = 0
81 custom_timeout = timeout is not SENTINEL
82
83 buf = self._live_buffer()
84
85 if sock is None:
86 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
87
88 try:
89 current_pos = buf.tell()
90 buf.seek(0, SEEK_END)
91 except ValueError:
92 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
93
94 if custom_timeout:
95 sock.settimeout(timeout)
96 try:
97 while True:
98 data = sock.recv(socket_read_size)
99 # an empty string indicates the server shutdown the socket
100 if isinstance(data, bytes) and len(data) == 0:
101 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
102 buf.write(data)
103 data_length = len(data)
104 marker += data_length
105
106 if length is not None and length > marker:
107 continue
108 return True
109 except socket.timeout:
110 if raise_on_timeout:
111 raise TimeoutError("Timeout reading from socket")
112 return False
113 except NONBLOCKING_EXCEPTIONS as ex:
114 # if we're in nonblocking mode and the recv raises a
115 # blocking error, simply return False indicating that
116 # there's no data to be read. otherwise raise the
117 # original exception.
118 allowed = NONBLOCKING_EXCEPTION_ERROR_NUMBERS.get(ex.__class__, -1)
119 if ex.errno == allowed:
120 if not raise_on_timeout:
121 return False
122 if timeout == 0:
123 raise TimeoutError("Timeout reading from socket")
124 raise ConnectionError(f"Error while reading from socket: {ex.args}")
125 except ValueError:
126 # The buffer was closed by another thread while this read was in flight.
127 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
128 finally:
129 try:
130 buf.seek(current_pos)
131 except ValueError:
132 # Closed by another thread while recv was blocked. Whatever the body
133 # of the read raised is the outcome to report, so this stays quiet
134 # rather than replacing it - the next read raises ConnectionError.
135 pass
136 if custom_timeout:
137 try:
138 sock.settimeout(self.socket_timeout)
139 except OSError:
140 # Same window as the seek above: the close that dropped the
141 # buffer closed the socket too, so there is nothing left to
142 # restore the timeout on. Staying quiet keeps the outcome the
143 # body reported instead of replacing it with EBADF.
144 pass
145
146 def can_read(self, timeout: float = 0) -> bool:
147 return bool(self.unread_bytes()) or self._read_from_socket(
148 timeout=timeout, raise_on_timeout=False
149 )
150
151 def read(self, length: int, timeout: Union[float, object] = SENTINEL) -> bytes:
152 length = length + 2 # make sure to read the \r\n terminator
153 buf = self._live_buffer()
154 try:
155 # BufferIO will return less than requested if buffer is short
156 data = buf.read(length)
157 missing = length - len(data)
158 if missing:
159 # fill up the buffer and read the remainder
160 self._read_from_socket(length=missing, timeout=timeout)
161 data += buf.read(missing)
162 except ValueError:
163 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
164 return data[:-2]
165
166 def readline(self, timeout: Union[float, object] = SENTINEL) -> bytes:
167 buf = self._live_buffer()
168 try:
169 data = buf.readline()
170 while not data.endswith(SYM_CRLF):
171 # there's more data in the socket that we need
172 self._read_from_socket(timeout=timeout)
173 data += buf.readline()
174 except ValueError:
175 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
176
177 return data[:-2]
178
179 def get_pos(self) -> int:
180 """
181 Get current read position
182 """
183 try:
184 return self._live_buffer().tell()
185 except ValueError:
186 raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
187
188 def rewind(self, pos: int) -> None:
189 """
190 Rewind the buffer to a specific position, to re-start reading
191 """
192 buffer = self._buffer
193
194 # Best effort: the caller is unwinding a read that already failed, and a
195 # buffer closed by another thread has nothing to rewind. Raising here would
196 # replace the exception the caller is propagating.
197 if buffer is None:
198 return
199
200 try:
201 buffer.seek(pos)
202 except ValueError:
203 pass
204
205 def purge(self) -> None:
206 """
207 After a successful read, purge the read part of buffer
208 """
209 try:
210 self._purge()
211 except (ConnectionError, ValueError):
212 # Closed by another thread while the response was being read. The
213 # response is already parsed, so there is nothing to report and nothing
214 # left to purge.
215 return
216
217 def _purge(self) -> None:
218 unread = self.unread_bytes()
219
220 # Only if we have read all of the buffer do we truncate, to
221 # reduce the amount of memory thrashing. This heuristic
222 # can be changed or removed later.
223 if unread > 0:
224 return
225
226 # Bind the buffer once: another thread's ``close()`` can drop
227 # ``self._buffer`` to None between the read above and the truncate below,
228 # and the resulting AttributeError would escape ``purge()``'s best effort
229 # wrapper. A local reference to an already closed buffer raises
230 # ValueError, which ``purge()`` handles.
231 buffer = self._live_buffer()
232
233 if unread > 0:
234 # move unread data to the front
235 view = buffer.getbuffer()
236 view[:unread] = view[-unread:]
237 buffer.truncate(unread)
238 buffer.seek(0)
239
240 def close(self) -> None:
241 try:
242 self._buffer.close()
243 except Exception:
244 # issue #633 suggests the purge/close somehow raised a
245 # BadFileDescriptor error. Perhaps the client ran out of
246 # memory or something else? It's probably OK to ignore
247 # any error being raised from purge/close since we're
248 # removing the reference to the instance below.
249 pass
250 self._buffer = None
251 self._sock = None