Skip to content

Commit fbac62d

Browse files
authored
feat: gdb failover support (#1246)
1 parent 0a20546 commit fbac62d

14 files changed

Lines changed: 744 additions & 58 deletions

.github/workflows/integration_tests.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ on:
55
push:
66
branches:
77
- main
8-
- feat/gdb-rw # temporary
98

109
permissions:
1110
id-token: write # This is required for requesting the JWT

aws_advanced_python_wrapper/aurora_connection_tracker_plugin.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,14 @@ def invalidate_all_connections(self, host_info: Optional[HostInfo] = None, host:
165165

166166
with self._lock:
167167
connection_set: Optional[WeakSet] = self._opened_connections.get(instance_endpoint)
168-
connections_list = list(connection_set) if connection_set is not None else None
169-
170-
if connections_list is not None:
171-
self._log_connection_set(instance_endpoint, connection_set)
168+
if connection_set is not None:
169+
connections_list = list(connection_set)
170+
connection_set.clear()
171+
else:
172+
connections_list = None
173+
174+
if connections_list:
175+
self._log_connection_set(instance_endpoint, connections_list)
172176
self._invalidate_connections(connections_list)
173177

174178
def remove_connection_tracking(self, host_info: HostInfo, connection: Connection | None):
@@ -228,11 +232,11 @@ def log_opened_connections(self):
228232
msg = "".join(msg_parts)
229233
return logger.debug("OpenedConnectionTracker.OpenedConnectionsTracked", msg)
230234

231-
def _log_connection_set(self, host: str, conn_set: Optional[WeakSet]):
232-
if conn_set is None or len(conn_set) == 0:
235+
def _log_connection_set(self, host: str, connections: Optional[list]):
236+
if not connections:
233237
return
234238

235-
conn_parts = [f"\n\t\t{item}" for item in list(conn_set)]
239+
conn_parts = [f"\n\t\t{item}" for item in connections]
236240
conn = "".join(conn_parts)
237241
msg = host + f"[{conn}\n]"
238242
logger.debug("OpenedConnectionTracker.InvalidatingConnections", msg)

aws_advanced_python_wrapper/failover_v2_plugin.py

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Set
1818

1919
from aws_advanced_python_wrapper.pep249_methods import DbApiMethod
20-
from aws_advanced_python_wrapper.utils.utils import LogUtils
2120

2221
if TYPE_CHECKING:
2322
from aws_advanced_python_wrapper.host_list_provider import HostListProviderService
@@ -41,6 +40,7 @@
4140
WrapperProperties)
4241
from aws_advanced_python_wrapper.utils.rds_url_type import RdsUrlType
4342
from aws_advanced_python_wrapper.utils.rds_utils import RdsUtils
43+
from aws_advanced_python_wrapper.utils.retry_util import RetryUtil
4444
from aws_advanced_python_wrapper.utils.telemetry.telemetry import \
4545
TelemetryTraceLevel
4646

@@ -326,55 +326,32 @@ def _failover_writer(self) -> None:
326326
"failover to writer host", TelemetryTraceLevel.NESTED)
327327

328328
failover_start_time = time.time()
329+
failover_end_time = failover_start_time + self._failover_timeout_sec
330+
retry_util = RetryUtil()
331+
result = None
329332
try:
330333
if not self._plugin_service.force_monitoring_refresh_host_list(True, self._failover_timeout_sec):
331334
raise FailoverFailedError(Messages.get("FailoverPlugin.UnableToRefreshHostList"))
332335

333-
updated_hosts = self._plugin_service.all_hosts
334-
writer_candidate = next((host for host in updated_hosts if host.role == HostRole.WRITER), None)
336+
result = retry_util.get_writer_connection(
337+
self._plugin_service, self._properties, self, True, failover_end_time)
335338

336-
if writer_candidate is None:
337-
raise FailoverFailedError(Messages.get_formatted(
338-
"FailoverPlugin.NoWriterHostInTopology",
339-
LogUtils.log_topology(updated_hosts)))
340-
341-
logger.info("FailoverPlugin.FoundWriterCandidate", writer_candidate)
342-
343-
allowed_hosts = self._plugin_service.hosts
344-
if not any(host.host == writer_candidate.host and host.port == writer_candidate.port
345-
for host in allowed_hosts):
346-
raise FailoverFailedError(
347-
Messages.get_formatted(
348-
"FailoverPlugin.NewWriterNotAllowed",
349-
"<null>" if writer_candidate is None else writer_candidate.host,
350-
LogUtils.log_topology(allowed_hosts)))
351-
352-
try:
353-
writer_candidate_conn = self._plugin_service.connect(writer_candidate, self._properties, self)
354-
except Exception as e:
355-
raise FailoverFailedError(Messages.get_formatted(
356-
"FailoverPlugin.ExceptionConnectingToWriter", e))
357-
358-
role = self._plugin_service.get_host_role(writer_candidate_conn)
359-
if role != HostRole.WRITER:
360-
try:
361-
self._plugin_service.driver_dialect.execute(
362-
DbApiMethod.CONNECTION_CLOSE.method_name, lambda: writer_candidate_conn.close())
363-
except Exception:
364-
pass
365-
raise FailoverFailedError(Messages.get_formatted(
366-
"FailoverPlugin.WriterFailoverConnectedToReader",
367-
writer_candidate.host))
368-
369-
self._plugin_service.set_current_connection(writer_candidate_conn, writer_candidate)
370-
logger.info("FailoverPlugin.EstablishedConnection", self._plugin_service.current_host_info)
371-
self._throw_failover_success_exception()
339+
if result.connection is not None and result.host_info is not None:
340+
self._plugin_service.set_current_connection(result.connection, result.host_info)
341+
result = None # Prevents closing the returned connection in the finally block.
342+
logger.info("FailoverPlugin.EstablishedConnection", self._plugin_service.current_host_info)
343+
self._throw_failover_success_exception()
372344

373345
except FailoverSuccessError as ex:
374346
if telemetry_context:
375347
telemetry_context.set_success(True)
376348
telemetry_context.set_exception(ex)
377349
raise ex
350+
except TimeoutError as ex:
351+
if telemetry_context:
352+
telemetry_context.set_success(False)
353+
telemetry_context.set_exception(ex)
354+
raise FailoverFailedError(str(ex))
378355
except Exception as ex:
379356
if telemetry_context:
380357
telemetry_context.set_success(False)
@@ -383,6 +360,8 @@ def _failover_writer(self) -> None:
383360
finally:
384361
elapsed_time = (time.time() - failover_start_time) * 1000
385362
logger.info("FailoverPlugin.WriterFailoverTime", elapsed_time)
363+
if result is not None and result.connection is not self._plugin_service.current_connection:
364+
RetryUtil.close_connection(self._plugin_service, result.connection)
386365
if telemetry_context:
387366
telemetry_context.close_context()
388367
if self._telemetry_failover_additional_top_trace:
@@ -423,9 +402,12 @@ def _should_exception_trigger_connection_switch(self, exception: Exception) -> b
423402

424403
# For STRICT_WRITER failover mode when connection exception indicate that the connection's in read-only mode,
425404
# initiate a failover by returning true.
426-
return self._failover_mode == FailoverMode.STRICT_WRITER and \
405+
return self._is_strict_writer_failover_mode() and \
427406
self._plugin_service.is_read_only_connection_exception(exception)
428407

408+
def _is_strict_writer_failover_mode(self) -> bool:
409+
return self._failover_mode == FailoverMode.STRICT_WRITER
410+
429411
def _can_direct_execute(self, method_name: str) -> bool:
430412
return method_name == DbApiMethod.CONNECTION_CLOSE.method_name or \
431413
method_name == DbApiMethod.CONNECTION_IS_CLOSED.method_name or \

0 commit comments

Comments
 (0)