Skip to content

Commit 1d0e563

Browse files
Fixed retry count handling to work in cases where the listener is
running but the service is down; added new exception class oracledb.ConnectionError in order to aid the handling of connection errors during creation or use (where the connection is no longer usable or could not be established). This addresses issue #3.
1 parent 73573c8 commit 1d0e563

File tree

6 files changed

+141
-58
lines changed

6 files changed

+141
-58
lines changed

doc/src/release_notes.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ oracledb 1.0.1 (TBD)
1616
(`issue 3 <https://github.com/oracle/python-oracledb/issues/3>`__).
1717
#) Ensured the name of wrapped functions are the same as the function being
1818
wrapped in order to improve error messages that reference them.
19+
#) Added exception class (oracledb.ConnectionError) as a subclass of
20+
oracledb.DatabaseError in order to aid the handling of connection errors
21+
during creation or use (where the connection is no longer usable or could
22+
not be established).
23+
#) Thin: fixed retry count handling to work in cases where the listener is
24+
running but the service is down
25+
(`issue 3 <https://github.com/oracle/python-oracledb/issues/3>`__).
26+
#) Thin: if an OS error occurs during the creation of a connection to the
27+
database, the error is wrapped by DPY-6005 as an instance of
28+
oracledb.ConnectionError.
1929

2030

2131
oracledb 1.0.0 (May 2022)

src/oracledb/errors.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def __init__(self, message: str=None, context: str=None,
6868
args = re.search(pattern, message).groupdict()
6969
else:
7070
driver_error_num = driver_error_info
71-
if driver_error_num == ERR_CONNECTION_CLOSED:
71+
if driver_error_num in ERR_CONNECTION_ERROR_SET:
7272
self.is_session_dead = True
7373
driver_error = _get_error_text(driver_error_num, **args)
7474
self.message = f"{driver_error}\n{self.message}"
@@ -99,7 +99,10 @@ def _raise_err(error_num: int, context_error_message: str=None,
9999
message = _get_error_text(error_num, **args)
100100
if context_error_message is not None:
101101
message = f"{message}\n{context_error_message}"
102-
exc_type = ERR_EXCEPTION_TYPES[error_num // 1000]
102+
if error_num in ERR_CONNECTION_ERROR_SET:
103+
exc_type = exceptions.ConnectionError
104+
else:
105+
exc_type = ERR_EXCEPTION_TYPES[error_num // 1000]
103106
raise exc_type(_Error(message)) from cause
104107

105108

@@ -205,13 +208,15 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
205208
ERR_INTEGER_TOO_LARGE = 5002
206209
ERR_UNEXPECTED_NEGATIVE_INTEGER = 5003
207210
ERR_UNEXPECTED_DATA = 5004
211+
ERR_UNEXPECTED_REFUSE = 5005
208212

209213
# error numbers that result in OperationalError
210214
ERR_LISTENER_REFUSED_CONNECTION = 6000
211215
ERR_INVALID_SERVICE_NAME = 6001
212216
ERR_INVALID_SERVER_CERT_DN = 6002
213217
ERR_INVALID_SID = 6003
214218
ERR_PROXY_FAILURE = 6004
219+
ERR_CONNECTION_FAILED = 6005
215220

216221
# Oracle error number cross reference
217222
ERR_ORACLE_ERROR_XREF = {
@@ -234,6 +239,15 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
234239
1080: ERR_CONNECTION_CLOSED,
235240
}
236241

242+
# dead connection errors
243+
ERR_CONNECTION_ERROR_SET = set([
244+
ERR_CONNECTION_CLOSED,
245+
ERR_CONNECTION_FAILED,
246+
ERR_INVALID_SERVICE_NAME,
247+
ERR_INVALID_SID,
248+
ERR_LISTENER_REFUSED_CONNECTION
249+
])
250+
237251
# error message exception types (multiples of 1000)
238252
ERR_EXCEPTION_TYPES = {
239253
1: exceptions.InterfaceError,
@@ -264,6 +278,8 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
264278
ERR_COLUMN_TRUNCATED:
265279
'column truncated to {col_value_len} {unit}. '
266280
'Untruncated was {actual_len}',
281+
ERR_CONNECTION_FAILED:
282+
'cannot connect to database. Connection failed with "{exception}"',
267283
ERR_CONTENT_INVALID_AFTER_NUMBER:
268284
'invalid number (content after number)',
269285
ERR_CURSOR_NOT_OPEN:
@@ -417,6 +433,9 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
417433
ERR_UNEXPECTED_NEGATIVE_INTEGER:
418434
'internal error: read a negative integer when expecting a '
419435
'positive integer',
436+
ERR_UNEXPECTED_REFUSE:
437+
'the listener refused the connection but an unexpected error '
438+
'format was returned',
420439
ERR_UNSUPPORTED_INBAND_NOTIFICATION:
421440
'unsupported in-band notification with error number {err_num}',
422441
ERR_UNSUPPORTED_PYTHON_TYPE_FOR_VAR:

src/oracledb/exceptions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
#------------------------------------------------------------------------------
2626
# exceptions.py
2727
#
28-
# Contains the exception classes mandated by the Python Database API.
28+
# Contains the exception classes mandated by the Python Database API and one
29+
# additional one (ConnectionError) for aiding in the handling of connection
30+
# failures when the connection is being created or used (and the connection is
31+
# no longer usable).
2932
#------------------------------------------------------------------------------
3033

3134
class Warning(Exception):
@@ -40,6 +43,10 @@ class DatabaseError(Error):
4043
pass
4144

4245

46+
class ConnectionError(DatabaseError):
47+
pass
48+
49+
4350
class DataError(DatabaseError):
4451
pass
4552

src/oracledb/impl/thin/connection.pyx

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ cdef class ThinConnImpl(BaseConnImpl):
8181
elif stmt._cursor_id != 0:
8282
self._add_cursor_to_close(stmt)
8383

84-
cdef object _connect_with_description(self, Description description):
84+
cdef object _connect_with_description(self, Description description,
85+
ConnectParamsImpl params,
86+
bint final_desc):
8587
cdef:
86-
double timeout = description.tcp_connect_timeout
87-
bint load_balance = description.load_balance, raise_exc
88+
bint load_balance = description.load_balance
89+
bint raise_exc = False
8890
list address_lists = description.address_lists
8991
uint32_t i, j, k, num_addresses, idx1, idx2
9092
uint32_t num_attempts = description.retry_count + 1
@@ -116,13 +118,19 @@ cdef class ThinConnImpl(BaseConnImpl):
116118
else:
117119
idx2 = k
118120
address = address_list.addresses[idx2]
119-
raise_exc = i == num_attempts - 1
120-
sock = self._get_socket(address, timeout, raise_exc)
121-
if sock is None:
121+
if final_desc:
122+
raise_exc = i == num_attempts - 1
123+
redirect_params = self._connect_with_address(address,
124+
description,
125+
params,
126+
raise_exc)
127+
if redirect_params is not None:
128+
return redirect_params
129+
if self._protocol._in_connect:
122130
continue
123131
address_list.lru_index = (idx1 + 1) % num_addresses
124132
description.lru_index = (idx2 + 1) % num_lists
125-
return (sock, address)
133+
return
126134
time.sleep(description.retry_delay)
127135

128136
cdef ConnectParamsImpl _connect_with_params(self,
@@ -137,36 +145,23 @@ cdef class ThinConnImpl(BaseConnImpl):
137145
list descriptions = description_list.descriptions
138146
ssize_t i, idx, num_descriptions = len(descriptions)
139147
Description description
140-
Address address
141-
tuple ret_tuple
148+
bint final_desc = False
142149
for i in range(num_descriptions):
150+
if i == num_descriptions - 1:
151+
final_desc = True
143152
if description_list.load_balance:
144153
idx = (i + description_list.lru_index) % num_descriptions
145154
else:
146155
idx = i
147156
description = descriptions[idx]
148-
ret_tuple = self._connect_with_description(description)
149-
if ret_tuple is not None:
150-
sock, address = ret_tuple
157+
redirect_params = self._connect_with_description(description,
158+
params,
159+
final_desc)
160+
if redirect_params is not None \
161+
or not self._protocol._in_connect:
151162
description_list.lru_index = (idx + 1) % num_descriptions
152163
break
153-
if description.expire_time > 0:
154-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
155-
if hasattr(socket, "TCP_KEEPIDLE") \
156-
and hasattr(socket, "TCP_KEEPINTVL") \
157-
and hasattr(socket, "TCP_KEEPCNT"):
158-
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE,
159-
description.expire_time * 60)
160-
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 6)
161-
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 10)
162-
sock.settimeout(None)
163-
if address.protocol == "tcps":
164-
sock = get_ssl_socket(sock, params, description, address)
165-
self._drcp_enabled = description.server_type == "pooled"
166-
if self._cclass is None:
167-
self._cclass = description.cclass
168-
self._protocol = Protocol(sock)
169-
return self._protocol._connect(self, params, description, address)
164+
return redirect_params
170165

171166
cdef Message _create_message(self, type typ):
172167
"""
@@ -182,36 +177,71 @@ cdef class ThinConnImpl(BaseConnImpl):
182177
self._pool = None
183178
self._protocol._force_close()
184179

185-
cdef object _get_socket(self, object address,
186-
double tcp_connect_timeout,
187-
bint raise_exception):
180+
cdef object _connect_with_address(self, Address address,
181+
Description description,
182+
ConnectParamsImpl params,
183+
bint raise_exception):
188184
"""
189-
Get a socket on which to communicate using the provided parameters. If
190-
a proxy is configured, a connection to the proxy is established and the
191-
target host and port is forwarded to the proxy before the socket is
192-
returned.
185+
Creates a socket on which to communicate using the provided parameters.
186+
If a proxy is configured, a connection to the proxy is established and
187+
the target host and port is forwarded to the proxy. The socket is used
188+
to establish a connection with the database. If a redirect is
189+
required, the redirect parameters are returned.
193190
"""
194-
cdef bint use_proxy = (address.https_proxy is not None)
191+
cdef:
192+
bint use_proxy = (address.https_proxy is not None)
193+
double timeout = description.tcp_connect_timeout
195194
if use_proxy:
196195
connect_info = (address.https_proxy, address.https_proxy_port)
197196
else:
198197
connect_info = (address.host, address.port)
199198
try:
200-
sock = socket.create_connection(connect_info,
201-
tcp_connect_timeout)
202-
except (socket.gaierror, ConnectionRefusedError):
199+
sock = socket.create_connection(connect_info, timeout)
200+
if use_proxy:
201+
data = f"CONNECT {address.host}:{address.port} HTTP/1.0\r\n\r\n"
202+
sock.send(data.encode())
203+
reply = sock.recv(1024)
204+
match = re.search('HTTP/1.[01]\\s+(\\d+)\\s+', reply.decode())
205+
if match is None or match.groups()[0] != '200':
206+
errors._raise_err(errors.ERR_PROXY_FAILURE,
207+
response=reply.decode())
208+
if description.expire_time > 0:
209+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
210+
if hasattr(socket, "TCP_KEEPIDLE") \
211+
and hasattr(socket, "TCP_KEEPINTVL") \
212+
and hasattr(socket, "TCP_KEEPCNT"):
213+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE,
214+
description.expire_time * 60)
215+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL,
216+
6)
217+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT,
218+
10)
219+
sock.settimeout(None)
220+
if address.protocol == "tcps":
221+
sock = get_ssl_socket(sock, params, description, address)
222+
self._drcp_enabled = description.server_type == "pooled"
223+
if self._cclass is None:
224+
self._cclass = description.cclass
225+
self._protocol = Protocol(sock)
226+
redirect_params = self._protocol._connect_phase_one(self, params,
227+
description,
228+
address)
229+
if redirect_params is not None:
230+
return redirect_params
231+
except exceptions.ConnectionError:
203232
if raise_exception:
204233
raise
205-
return None
206-
if use_proxy:
207-
data = f"CONNECT {address.host}:{address.port} HTTP/1.0\r\n\r\n"
208-
sock.send(data.encode())
209-
reply = sock.recv(1024)
210-
match = re.search('HTTP/1.[01]\\s+(\\d+)\\s+', reply.decode())
211-
if match is None or match.groups()[0] != '200':
212-
errors._raise_err(errors.ERR_PROXY_FAILURE,
213-
response=reply.decode())
214-
return sock
234+
return
235+
except (socket.gaierror, ConnectionRefusedError) as e:
236+
if raise_exception:
237+
errors._raise_err(errors.ERR_CONNECTION_FAILED, cause=e,
238+
exception=str(e))
239+
return
240+
except Exception as e:
241+
errors._raise_err(errors.ERR_CONNECTION_FAILED, cause=e,
242+
exception=str(e))
243+
return
244+
self._protocol._connect_phase_two(self, description, params)
215245

216246
cdef Statement _get_statement(self, str sql, bint cache_statement):
217247
"""

src/oracledb/impl/thin/messages.pyx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,6 +1538,8 @@ cdef class ConnectMessage(Message):
15381538
if end_pos > 0:
15391539
error_code = response[pos + 5:end_pos]
15401540
error_code_int = int(error_code)
1541+
if error_code_int == 0:
1542+
errors._raise_err(errors.ERR_UNEXPECTED_REFUSE)
15411543
if error_code_int == TNS_ERR_INVALID_SERVICE_NAME:
15421544
errors._raise_err(errors.ERR_INVALID_SERVICE_NAME,
15431545
service_name=self.description.service_name,

src/oracledb/impl/thin/protocol.pyx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,18 @@ cdef class Protocol:
118118
self._process_message(message)
119119
self._final_close(self._write_buf)
120120

121-
cdef ConnectParamsImpl _connect(self, ThinConnImpl conn_impl,
122-
ConnectParamsImpl params,
123-
Description description, Address address):
121+
cdef ConnectParamsImpl _connect_phase_one(self, ThinConnImpl conn_impl,
122+
ConnectParamsImpl params,
123+
Description description,
124+
Address address):
125+
"""
126+
Method for performing the required steps for establishing a connection
127+
within the scope of a retry. If the listener refuses the connection, a
128+
retry will be performed, if retry_count is set.
129+
"""
124130
cdef:
125131
ConnectMessage connect_message
126-
object temp_sock, ssl_context
127-
AuthMessage auth_message
132+
object ssl_context
128133
uint8_t packet_type
129134
str connect_string
130135

@@ -172,6 +177,16 @@ cdef class Protocol:
172177
sock = ssl_context.wrap_socket(sock)
173178
self.__set_socket(sock)
174179

180+
cdef int _connect_phase_two(self, ThinConnImpl conn_impl,
181+
Description description,
182+
ConnectParamsImpl params) except -1:
183+
""""
184+
Method for perfoming the required steps for establishing a connection
185+
oustide the scope of a retry. If any of the steps in this method fail,
186+
an exception will be raised.
187+
"""
188+
cdef:
189+
AuthMessage auth_message
175190
# check if the protocol version supported by the database is high
176191
# enough; if not, reject the connection immediately
177192
if self._caps.protocol_version < TNS_VERSION_MIN_ACCEPTED:

0 commit comments

Comments
 (0)