aws-advanced-python-wrapper 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. CONTRIBUTING.md +63 -0
  2. aws_advanced_python_wrapper/__init__.py +28 -0
  3. aws_advanced_python_wrapper/aurora_connection_tracker_plugin.py +228 -0
  4. aws_advanced_python_wrapper/aurora_initial_connection_strategy_plugin.py +240 -0
  5. aws_advanced_python_wrapper/aws_secrets_manager_plugin.py +218 -0
  6. aws_advanced_python_wrapper/connect_time_plugin.py +69 -0
  7. aws_advanced_python_wrapper/connection_provider.py +232 -0
  8. aws_advanced_python_wrapper/database_dialect.py +708 -0
  9. aws_advanced_python_wrapper/default_plugin.py +144 -0
  10. aws_advanced_python_wrapper/developer_plugin.py +163 -0
  11. aws_advanced_python_wrapper/driver_configuration_profiles.py +44 -0
  12. aws_advanced_python_wrapper/driver_dialect.py +165 -0
  13. aws_advanced_python_wrapper/driver_dialect_codes.py +19 -0
  14. aws_advanced_python_wrapper/driver_dialect_manager.py +121 -0
  15. aws_advanced_python_wrapper/driver_info.py +18 -0
  16. aws_advanced_python_wrapper/errors.py +47 -0
  17. aws_advanced_python_wrapper/exception_handling.py +73 -0
  18. aws_advanced_python_wrapper/execute_time_plugin.py +58 -0
  19. aws_advanced_python_wrapper/failover_plugin.py +517 -0
  20. aws_advanced_python_wrapper/failover_result.py +42 -0
  21. aws_advanced_python_wrapper/fastest_response_strategy_plugin.py +345 -0
  22. aws_advanced_python_wrapper/federated_plugin.py +382 -0
  23. aws_advanced_python_wrapper/host_availability.py +86 -0
  24. aws_advanced_python_wrapper/host_list_provider.py +645 -0
  25. aws_advanced_python_wrapper/host_monitoring_plugin.py +728 -0
  26. aws_advanced_python_wrapper/host_selector.py +190 -0
  27. aws_advanced_python_wrapper/hostinfo.py +138 -0
  28. aws_advanced_python_wrapper/iam_plugin.py +195 -0
  29. aws_advanced_python_wrapper/mysql_driver_dialect.py +175 -0
  30. aws_advanced_python_wrapper/pep249.py +196 -0
  31. aws_advanced_python_wrapper/pg_driver_dialect.py +176 -0
  32. aws_advanced_python_wrapper/plugin.py +148 -0
  33. aws_advanced_python_wrapper/plugin_service.py +949 -0
  34. aws_advanced_python_wrapper/read_write_splitting_plugin.py +363 -0
  35. aws_advanced_python_wrapper/reader_failover_handler.py +252 -0
  36. aws_advanced_python_wrapper/resources/aws_advanced_python_wrapper_messages.properties +315 -0
  37. aws_advanced_python_wrapper/sql_alchemy_connection_provider.py +196 -0
  38. aws_advanced_python_wrapper/sqlalchemy_driver_dialect.py +127 -0
  39. aws_advanced_python_wrapper/stale_dns_plugin.py +209 -0
  40. aws_advanced_python_wrapper/states/__init__.py +13 -0
  41. aws_advanced_python_wrapper/states/session_state.py +94 -0
  42. aws_advanced_python_wrapper/states/session_state_service.py +221 -0
  43. aws_advanced_python_wrapper/utils/__init__.py +13 -0
  44. aws_advanced_python_wrapper/utils/atomic.py +51 -0
  45. aws_advanced_python_wrapper/utils/cache_map.py +99 -0
  46. aws_advanced_python_wrapper/utils/concurrent.py +100 -0
  47. aws_advanced_python_wrapper/utils/decorators.py +70 -0
  48. aws_advanced_python_wrapper/utils/failover_mode.py +39 -0
  49. aws_advanced_python_wrapper/utils/iamutils.py +75 -0
  50. aws_advanced_python_wrapper/utils/log.py +75 -0
  51. aws_advanced_python_wrapper/utils/messages.py +36 -0
  52. aws_advanced_python_wrapper/utils/mysql_exception_handler.py +73 -0
  53. aws_advanced_python_wrapper/utils/notifications.py +37 -0
  54. aws_advanced_python_wrapper/utils/pg_exception_handler.py +115 -0
  55. aws_advanced_python_wrapper/utils/properties.py +492 -0
  56. aws_advanced_python_wrapper/utils/rds_url_type.py +36 -0
  57. aws_advanced_python_wrapper/utils/rdsutils.py +226 -0
  58. aws_advanced_python_wrapper/utils/sliding_expiration_cache.py +146 -0
  59. aws_advanced_python_wrapper/utils/telemetry/default_telemetry_factory.py +82 -0
  60. aws_advanced_python_wrapper/utils/telemetry/null_telemetry.py +55 -0
  61. aws_advanced_python_wrapper/utils/telemetry/open_telemetry.py +189 -0
  62. aws_advanced_python_wrapper/utils/telemetry/telemetry.py +85 -0
  63. aws_advanced_python_wrapper/utils/telemetry/xray_telemetry.py +126 -0
  64. aws_advanced_python_wrapper/utils/utils.py +89 -0
  65. aws_advanced_python_wrapper/wrapper.py +322 -0
  66. aws_advanced_python_wrapper/writer_failover_handler.py +347 -0
  67. aws_advanced_python_wrapper-1.0.0.dist-info/LICENSE +201 -0
  68. aws_advanced_python_wrapper-1.0.0.dist-info/METADATA +261 -0
  69. aws_advanced_python_wrapper-1.0.0.dist-info/RECORD +70 -0
  70. aws_advanced_python_wrapper-1.0.0.dist-info/WHEEL +4 -0
CONTRIBUTING.md ADDED
@@ -0,0 +1,63 @@
1
+ # Contributing Guidelines
2
+
3
+ Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4
+ documentation, we greatly value feedback and contributions from our community.
5
+
6
+ Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7
+ information to effectively respond to your bug report or contribution.
8
+
9
+
10
+ ## Reporting Bugs/Feature Requests
11
+
12
+ We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13
+
14
+ When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15
+ reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16
+
17
+ * A reproducible test case or series of steps
18
+ * The version of our code being used
19
+ * Any modifications you've made relevant to the bug
20
+ * Anything unusual about your environment or deployment
21
+
22
+
23
+ ## Contributing via Pull Requests
24
+
25
+ Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
26
+
27
+ 1. You are working against the latest source on the *main* branch.
28
+ 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
29
+ 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
30
+
31
+ To send us a pull request, please:
32
+
33
+ 1. Fork the repository.
34
+ 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
35
+ 3. Ensure local tests pass.
36
+ 4. Commit to your fork using clear commit messages.
37
+ 5. Send us a pull request, answering any default questions in the pull request interface.
38
+ 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
39
+
40
+ GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
41
+ [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
42
+
43
+
44
+ ## Finding contributions to work on
45
+
46
+ Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
47
+
48
+
49
+ ## Code of Conduct
50
+
51
+ This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
52
+ For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
53
+ opensource-codeofconduct@amazon.com with any additional questions or comments.
54
+
55
+
56
+ ## Security issue notifications
57
+
58
+ If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue.
59
+
60
+
61
+ ## Licensing
62
+
63
+ See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
@@ -0,0 +1,28 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License").
4
+ # You may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from logging import DEBUG, getLogger
16
+
17
+ from .utils.utils import LogUtils
18
+ from .wrapper import AwsWrapperConnection
19
+
20
+ # PEP249 compliance
21
+ connect = AwsWrapperConnection.connect
22
+ apilevel = "2.0"
23
+ threadsafety = 2
24
+ paramstyle = "pyformat"
25
+
26
+
27
+ def set_logger(name='aws_advanced_python_wrapper', level=DEBUG, format_string=None):
28
+ LogUtils.setup_logger(getLogger(name), level, format_string)
@@ -0,0 +1,228 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License").
4
+ # You may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from threading import Thread
18
+ from typing import (TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Optional,
19
+ Set, Tuple)
20
+
21
+ if TYPE_CHECKING:
22
+ from aws_advanced_python_wrapper.driver_dialect import DriverDialect
23
+ from aws_advanced_python_wrapper.plugin_service import PluginService
24
+ from aws_advanced_python_wrapper.pep249 import Connection
25
+
26
+ from aws_advanced_python_wrapper.utils.rds_url_type import RdsUrlType
27
+ from aws_advanced_python_wrapper.utils.properties import Properties
28
+
29
+ from _weakrefset import WeakSet
30
+
31
+ from aws_advanced_python_wrapper.errors import FailoverError
32
+ from aws_advanced_python_wrapper.hostinfo import HostInfo, HostRole
33
+ from aws_advanced_python_wrapper.plugin import Plugin, PluginFactory
34
+ from aws_advanced_python_wrapper.utils.log import Logger
35
+ from aws_advanced_python_wrapper.utils.rdsutils import RdsUtils
36
+
37
+ logger = Logger(__name__)
38
+
39
+
40
+ class OpenedConnectionTracker:
41
+ _opened_connections: Dict[str, WeakSet] = {}
42
+ _rds_utils = RdsUtils()
43
+
44
+ def populate_opened_connection_set(self, host_info: HostInfo, conn: Connection):
45
+ """
46
+ Add the given connection to the set of tracked connections.
47
+
48
+ :param host_info: host information of the given connection.
49
+ :param conn: currently opened connection.
50
+ """
51
+
52
+ aliases: FrozenSet[str] = host_info.as_aliases()
53
+ host: str = host_info.as_alias()
54
+
55
+ if self._rds_utils.is_rds_instance(host):
56
+ self._track_connection(host, conn)
57
+ return
58
+
59
+ instance_endpoint: Optional[str] = next((alias for alias in aliases if self._rds_utils.is_rds_instance(alias)),
60
+ None)
61
+ if not instance_endpoint:
62
+ logger.debug("OpenedConnectionTracker.UnableToPopulateOpenedConnectionSet")
63
+ return
64
+
65
+ self._track_connection(instance_endpoint, conn)
66
+
67
+ def invalidate_all_connections(self, host_info: Optional[HostInfo] = None, host: Optional[FrozenSet[str]] = None):
68
+ """
69
+ Invalidates all opened connections pointing to the same host in a daemon thread.
70
+
71
+ :param host_info: the :py:class:`HostInfo` object containing the URL of the host.
72
+ :param host: the set of aliases representing a specific host.
73
+ """
74
+
75
+ if host_info:
76
+ self.invalidate_all_connections(host=frozenset(host_info.as_alias()))
77
+ self.invalidate_all_connections(host=host_info.as_aliases())
78
+ return
79
+
80
+ instance_endpoint: Optional[str] = None
81
+ if host is None:
82
+ return
83
+
84
+ for instance in host:
85
+ if instance is not None and self._rds_utils.is_rds_instance(instance):
86
+ instance_endpoint = instance
87
+ break
88
+
89
+ if not instance_endpoint:
90
+ return
91
+
92
+ connection_set: Optional[WeakSet] = self._opened_connections.get(instance_endpoint)
93
+ if connection_set is not None:
94
+ self._log_connection_set(instance_endpoint, connection_set)
95
+ self._invalidate_connections(connection_set)
96
+
97
+ def _track_connection(self, instance_endpoint: str, conn: Connection):
98
+ connection_set: Optional[WeakSet] = self._opened_connections.get(instance_endpoint)
99
+ if connection_set is None:
100
+ connection_set = WeakSet()
101
+ connection_set.add(conn)
102
+ self._opened_connections[instance_endpoint] = connection_set
103
+ else:
104
+ connection_set.add(conn)
105
+
106
+ self.log_opened_connections()
107
+
108
+ @staticmethod
109
+ def _task(connection_set: WeakSet):
110
+ while connection_set is not None and len(connection_set) > 0:
111
+ conn_reference = connection_set.pop()
112
+
113
+ if conn_reference is None:
114
+ continue
115
+
116
+ try:
117
+ conn_reference.close()
118
+ except Exception:
119
+ # Swallow this exception, current connection should be useless anyway
120
+ pass
121
+
122
+ def _invalidate_connections(self, connection_set: WeakSet):
123
+ invalidate_connection_thread: Thread = Thread(daemon=True, target=self._task,
124
+ args=[connection_set]) # type: ignore
125
+ invalidate_connection_thread.start()
126
+
127
+ def log_opened_connections(self):
128
+ msg = ""
129
+ for key, conn_set in self._opened_connections.items():
130
+ conn = ""
131
+ for item in list(conn_set):
132
+ conn += f"\n\t\t{item}"
133
+
134
+ msg += f"\t[{key} : {conn}]"
135
+
136
+ return logger.debug("OpenedConnectionTracker.OpenedConnectionsTracked", msg)
137
+
138
+ def _log_connection_set(self, host: str, conn_set: Optional[WeakSet]):
139
+ if conn_set is None or len(conn_set) == 0:
140
+ return
141
+
142
+ conn = ""
143
+ for item in list(conn_set):
144
+ conn += f"\n\t\t{item}"
145
+
146
+ msg = host + f"[{conn}\n]"
147
+ logger.debug("OpenedConnectionTracker.InvalidatingConnections", msg)
148
+
149
+
150
+ class AuroraConnectionTrackerPlugin(Plugin):
151
+ _SUBSCRIBED_METHODS: Set[str] = {"*"}
152
+ _current_writer: Optional[HostInfo] = None
153
+ _need_update_current_writer: bool = False
154
+
155
+ @property
156
+ def subscribed_methods(self) -> Set[str]:
157
+ return self._SUBSCRIBED_METHODS
158
+
159
+ def __init__(self,
160
+ plugin_service: PluginService,
161
+ props: Properties,
162
+ rds_utils: RdsUtils = RdsUtils(),
163
+ tracker: OpenedConnectionTracker = OpenedConnectionTracker()):
164
+ self._plugin_service = plugin_service
165
+ self._props = props
166
+ self._rds_utils = rds_utils
167
+ self._tracker = tracker
168
+
169
+ def connect(
170
+ self,
171
+ target_driver_func: Callable,
172
+ driver_dialect: DriverDialect,
173
+ host_info: HostInfo,
174
+ props: Properties,
175
+ is_initial_connection: bool,
176
+ connect_func: Callable) -> Connection:
177
+ return self._connect(host_info, connect_func)
178
+
179
+ def force_connect(
180
+ self,
181
+ target_driver_func: Callable,
182
+ driver_dialect: DriverDialect,
183
+ host_info: HostInfo,
184
+ props: Properties,
185
+ is_initial_connection: bool,
186
+ force_connect_func: Callable) -> Connection:
187
+ return self._connect(host_info, force_connect_func)
188
+
189
+ def _connect(self, host_info: HostInfo, connect_func: Callable):
190
+ conn = connect_func()
191
+
192
+ if conn:
193
+ url_type: RdsUrlType = self._rds_utils.identify_rds_type(host_info.host)
194
+ if url_type.is_rds_cluster:
195
+ host_info.reset_aliases()
196
+ self._plugin_service.fill_aliases(conn, host_info)
197
+
198
+ self._tracker.populate_opened_connection_set(host_info, conn)
199
+ self._tracker.log_opened_connections()
200
+
201
+ return conn
202
+
203
+ def execute(self, target: object, method_name: str, execute_func: Callable, *args: Any, **kwargs: Any) -> Any:
204
+ if self._current_writer is None or self._need_update_current_writer:
205
+ self._current_writer = self._get_writer(self._plugin_service.hosts)
206
+ self._need_update_current_writer = False
207
+
208
+ try:
209
+ return execute_func()
210
+
211
+ except Exception as e:
212
+ # Check that e is a FailoverError and that the writer has changed
213
+ if isinstance(e, FailoverError) and self._get_writer(self._plugin_service.hosts) != self._current_writer:
214
+ self._tracker.invalidate_all_connections(host_info=self._current_writer)
215
+ self._tracker.log_opened_connections()
216
+ self._need_update_current_writer = True
217
+ raise e
218
+
219
+ def _get_writer(self, hosts: Tuple[HostInfo, ...]) -> Optional[HostInfo]:
220
+ for host in hosts:
221
+ if host.role == HostRole.WRITER:
222
+ return host
223
+ return None
224
+
225
+
226
+ class AuroraConnectionTrackerPluginFactory(PluginFactory):
227
+ def get_instance(self, plugin_service: PluginService, props: Properties) -> Plugin:
228
+ return AuroraConnectionTrackerPlugin(plugin_service, props)
@@ -0,0 +1,240 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License").
4
+ # You may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from time import perf_counter_ns, sleep
18
+ from typing import TYPE_CHECKING, Callable, Optional, Set
19
+
20
+ if TYPE_CHECKING:
21
+ from aws_advanced_python_wrapper.driver_dialect import DriverDialect
22
+ from aws_advanced_python_wrapper.host_list_provider import HostListProviderService
23
+ from aws_advanced_python_wrapper.pep249 import Connection
24
+ from aws_advanced_python_wrapper.plugin_service import PluginService
25
+
26
+ from aws_advanced_python_wrapper.errors import AwsWrapperError
27
+ from aws_advanced_python_wrapper.host_availability import HostAvailability
28
+ from aws_advanced_python_wrapper.hostinfo import HostInfo, HostRole
29
+ from aws_advanced_python_wrapper.plugin import Plugin, PluginFactory
30
+ from aws_advanced_python_wrapper.utils.messages import Messages
31
+ from aws_advanced_python_wrapper.utils.properties import (Properties,
32
+ WrapperProperties)
33
+ from aws_advanced_python_wrapper.utils.rds_url_type import RdsUrlType
34
+ from aws_advanced_python_wrapper.utils.rdsutils import RdsUtils
35
+
36
+
37
+ class AuroraInitialConnectionStrategyPlugin(Plugin):
38
+ _SUBSCRIBED_METHODS: Set[str] = {"init_host_provider",
39
+ "connect",
40
+ "force_connect"}
41
+
42
+ _host_list_provider_service: Optional[HostListProviderService] = None
43
+
44
+ @property
45
+ def subscribed_methods(self) -> Set[str]:
46
+ return AuroraInitialConnectionStrategyPlugin._SUBSCRIBED_METHODS
47
+
48
+ def __init__(self, plugin_service: PluginService):
49
+ super()
50
+ self._plugin_service = plugin_service
51
+ self._rds_utils = RdsUtils()
52
+
53
+ def connect(self, target_driver_func: Callable, driver_dialect: DriverDialect, host_info: HostInfo, props: Properties,
54
+ is_initial_connection: bool, connect_func: Callable) -> Connection:
55
+ return self._connect(host_info, props, is_initial_connection, connect_func)
56
+
57
+ def force_connect(self, target_driver_func: Callable, driver_dialect: DriverDialect, host_info: HostInfo, props: Properties,
58
+ is_initial_connection: bool, force_connect_func: Callable) -> Connection:
59
+ return self._connect(host_info, props, is_initial_connection, force_connect_func)
60
+
61
+ def _connect(self, host_info: HostInfo, props: Properties, is_initial_connection: bool, connect_func: Callable):
62
+ type: RdsUrlType = self._rds_utils.identify_rds_type(host_info.host)
63
+ if not type.is_rds_cluster:
64
+ return connect_func()
65
+
66
+ if type == RdsUrlType.RDS_WRITER_CLUSTER:
67
+ writer_candidate_conn: Optional[Connection] = self._get_verified_writer_connection(props, is_initial_connection, connect_func)
68
+ if writer_candidate_conn is None:
69
+ return connect_func()
70
+ return writer_candidate_conn
71
+
72
+ if type == RdsUrlType.RDS_READER_CLUSTER:
73
+ reader_candidate_conn: Optional[Connection] = self._get_verified_reader_connection(props, is_initial_connection, connect_func)
74
+ if reader_candidate_conn is None:
75
+ return connect_func()
76
+ return reader_candidate_conn
77
+
78
+ def _get_verified_writer_connection(self, props: Properties, is_initial_connection: bool, connect_func: Callable) -> Connection | None:
79
+ retry_delay_ms: int = WrapperProperties.OPEN_CONNECTION_RETRY_INTERVAL_MS.get_int(props)
80
+ end_time_nano = perf_counter_ns() + (WrapperProperties.OPEN_CONNECTION_RETRY_INTERVAL_MS.get_int(props) * 1000000)
81
+
82
+ writer_candidate_conn: Optional[Connection]
83
+ writer_candidate: Optional[HostInfo]
84
+
85
+ while perf_counter_ns() < end_time_nano:
86
+ writer_candidate_conn = None
87
+ writer_candidate = None
88
+
89
+ try:
90
+ writer_candidate = self._get_writer()
91
+ if writer_candidate is None or self._rds_utils.is_rds_cluster_dns(writer_candidate.host):
92
+ # Writer is not found. Topology is outdated.
93
+ writer_candidate_conn = connect_func()
94
+ self._plugin_service.force_refresh_host_list(writer_candidate_conn)
95
+ writer_candidate = self._plugin_service.identify_connection(writer_candidate_conn)
96
+
97
+ if writer_candidate is not None and writer_candidate.role != HostRole.WRITER:
98
+ self._close_connection(writer_candidate_conn)
99
+ self._delay(retry_delay_ms)
100
+ continue
101
+
102
+ if is_initial_connection and self._host_list_provider_service is not None:
103
+ self._host_list_provider_service.initial_connection_host_info = writer_candidate
104
+
105
+ return writer_candidate_conn
106
+
107
+ writer_candidate_conn = self._plugin_service.connect(writer_candidate, props)
108
+
109
+ if self._plugin_service.get_host_role(writer_candidate_conn) != HostRole.WRITER:
110
+ self._plugin_service.force_refresh_host_list(writer_candidate_conn)
111
+ self._close_connection(writer_candidate_conn)
112
+ self._delay(retry_delay_ms)
113
+ continue
114
+
115
+ # Writer connection is valid and verified.
116
+ if is_initial_connection and self._host_list_provider_service is not None:
117
+ self._host_list_provider_service.initial_connection_host_info = writer_candidate
118
+ return writer_candidate_conn
119
+ except Exception as e:
120
+ self._close_connection(writer_candidate_conn)
121
+ raise e
122
+
123
+ return None
124
+
125
+ def _get_verified_reader_connection(self, props: Properties, is_initial_connection: bool, connect_func: Callable) -> Optional[Connection]:
126
+ retry_delay_ms: int = WrapperProperties.OPEN_CONNECTION_RETRY_INTERVAL_MS.get_int(props)
127
+ end_time_nano = perf_counter_ns() + (WrapperProperties.OPEN_CONNECTION_RETRY_INTERVAL_MS.get_int(props) * 1000000)
128
+
129
+ reader_candidate_conn: Optional[Connection]
130
+ reader_candidate: Optional[HostInfo]
131
+
132
+ while perf_counter_ns() < end_time_nano:
133
+ reader_candidate_conn = None
134
+ reader_candidate = None
135
+
136
+ try:
137
+ reader_candidate = self._get_reader(props)
138
+ if reader_candidate is None or self._rds_utils.is_rds_cluster_dns(reader_candidate.host):
139
+ # READER is not found. Topology is outdated.
140
+ reader_candidate_conn = connect_func()
141
+ self._plugin_service.force_refresh_host_list(reader_candidate_conn)
142
+ reader_candidate = self._plugin_service.identify_connection(reader_candidate_conn)
143
+
144
+ if reader_candidate is not None and reader_candidate.role != HostRole.READER:
145
+ if self._has_no_readers():
146
+ # Cluster has no readers. Simulate Aurora reader cluster endpoint logic and return the current writer connection.
147
+ if is_initial_connection and self._host_list_provider_service is not None:
148
+ self._host_list_provider_service.initial_connection_host_info = reader_candidate
149
+ return reader_candidate_conn
150
+
151
+ self._close_connection(reader_candidate_conn)
152
+ self._delay(retry_delay_ms)
153
+ continue
154
+
155
+ if is_initial_connection and self._host_list_provider_service is not None:
156
+ self._host_list_provider_service.initial_connection_host_info = reader_candidate
157
+ return reader_candidate_conn
158
+
159
+ reader_candidate_conn = self._plugin_service.connect(reader_candidate, props)
160
+
161
+ if self._plugin_service.get_host_role(reader_candidate_conn) != HostRole.READER:
162
+ # If the new connection resolves to a writer instance, the topology is outdated.
163
+ # Force refresh to update the topology.
164
+ self._plugin_service.force_refresh_host_list(reader_candidate_conn)
165
+
166
+ if self._has_no_readers():
167
+ # Cluster has no readers. Simulate Aurora reader cluster endpoint logic and return the current writer connection.
168
+ if is_initial_connection and self._host_list_provider_service is not None:
169
+ self._host_list_provider_service.initial_connection_host_info = reader_candidate
170
+ return reader_candidate_conn
171
+
172
+ self._close_connection(reader_candidate_conn)
173
+ self._delay(retry_delay_ms)
174
+ continue
175
+
176
+ # Reader connection is valid and verified.
177
+ if is_initial_connection and self._host_list_provider_service is not None:
178
+ self._host_list_provider_service.initial_connection_host_info = reader_candidate
179
+ return reader_candidate_conn
180
+ except Exception as e:
181
+ self._close_connection(reader_candidate_conn)
182
+ if not self._plugin_service.is_login_exception(e) and reader_candidate is not None:
183
+ self._plugin_service.set_availability(reader_candidate.as_aliases(), HostAvailability.UNAVAILABLE)
184
+
185
+ raise e
186
+
187
+ return None
188
+
189
+ def _close_connection(self, connection: Optional[Connection]):
190
+ if connection is not None:
191
+ try:
192
+ connection.close()
193
+ except Exception:
194
+ # ignore
195
+ pass
196
+
197
+ def _delay(self, delay_ms: int):
198
+ sleep(delay_ms / 1000)
199
+
200
+ def _get_writer(self) -> Optional[HostInfo]:
201
+ for host in self._plugin_service.hosts:
202
+ if host.role == HostRole.WRITER:
203
+ return host
204
+
205
+ return None
206
+
207
+ def _get_reader(self, props: Properties) -> Optional[HostInfo]:
208
+ strategy = WrapperProperties.READER_INITIAL_HOST_SELECTOR_STRATEGY.get(props)
209
+ if (self._plugin_service is not None
210
+ and strategy is not None
211
+ and self._plugin_service.accepts_strategy(HostRole.READER, strategy)):
212
+ try:
213
+ return self._plugin_service.get_host_info_by_strategy(HostRole.READER, strategy)
214
+ except Exception:
215
+ # Host isn't found.
216
+ return None
217
+
218
+ raise AwsWrapperError(Messages.get_formatted("AuroraInitialConnectionStrategyPlugin.UnsupportedStrategy", strategy))
219
+
220
+ def init_host_provider(self, props: Properties, host_list_provider_service: HostListProviderService, init_host_provider_func: Callable):
221
+ self._host_list_provider_service = host_list_provider_service
222
+ if host_list_provider_service.is_static_host_list_provider():
223
+ raise AwsWrapperError(Messages.get("AuroraInitialConnectionStrategyPlugin.RequireDynamicProvider"))
224
+
225
+ init_host_provider_func(props)
226
+
227
+ def _has_no_readers(self) -> bool:
228
+ if len(self._plugin_service.hosts) == 0:
229
+ return False
230
+
231
+ for host in self._plugin_service.hosts:
232
+ if host.role == HostRole.READER:
233
+ return False
234
+
235
+ return True
236
+
237
+
238
+ class AuroraInitialConnectionStrategyPluginFactory(PluginFactory):
239
+ def get_instance(self, plugin_service: PluginService, props: Properties) -> Plugin:
240
+ return AuroraInitialConnectionStrategyPlugin(plugin_service)