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.
- CONTRIBUTING.md +63 -0
- aws_advanced_python_wrapper/__init__.py +28 -0
- aws_advanced_python_wrapper/aurora_connection_tracker_plugin.py +228 -0
- aws_advanced_python_wrapper/aurora_initial_connection_strategy_plugin.py +240 -0
- aws_advanced_python_wrapper/aws_secrets_manager_plugin.py +218 -0
- aws_advanced_python_wrapper/connect_time_plugin.py +69 -0
- aws_advanced_python_wrapper/connection_provider.py +232 -0
- aws_advanced_python_wrapper/database_dialect.py +708 -0
- aws_advanced_python_wrapper/default_plugin.py +144 -0
- aws_advanced_python_wrapper/developer_plugin.py +163 -0
- aws_advanced_python_wrapper/driver_configuration_profiles.py +44 -0
- aws_advanced_python_wrapper/driver_dialect.py +165 -0
- aws_advanced_python_wrapper/driver_dialect_codes.py +19 -0
- aws_advanced_python_wrapper/driver_dialect_manager.py +121 -0
- aws_advanced_python_wrapper/driver_info.py +18 -0
- aws_advanced_python_wrapper/errors.py +47 -0
- aws_advanced_python_wrapper/exception_handling.py +73 -0
- aws_advanced_python_wrapper/execute_time_plugin.py +58 -0
- aws_advanced_python_wrapper/failover_plugin.py +517 -0
- aws_advanced_python_wrapper/failover_result.py +42 -0
- aws_advanced_python_wrapper/fastest_response_strategy_plugin.py +345 -0
- aws_advanced_python_wrapper/federated_plugin.py +382 -0
- aws_advanced_python_wrapper/host_availability.py +86 -0
- aws_advanced_python_wrapper/host_list_provider.py +645 -0
- aws_advanced_python_wrapper/host_monitoring_plugin.py +728 -0
- aws_advanced_python_wrapper/host_selector.py +190 -0
- aws_advanced_python_wrapper/hostinfo.py +138 -0
- aws_advanced_python_wrapper/iam_plugin.py +195 -0
- aws_advanced_python_wrapper/mysql_driver_dialect.py +175 -0
- aws_advanced_python_wrapper/pep249.py +196 -0
- aws_advanced_python_wrapper/pg_driver_dialect.py +176 -0
- aws_advanced_python_wrapper/plugin.py +148 -0
- aws_advanced_python_wrapper/plugin_service.py +949 -0
- aws_advanced_python_wrapper/read_write_splitting_plugin.py +363 -0
- aws_advanced_python_wrapper/reader_failover_handler.py +252 -0
- aws_advanced_python_wrapper/resources/aws_advanced_python_wrapper_messages.properties +315 -0
- aws_advanced_python_wrapper/sql_alchemy_connection_provider.py +196 -0
- aws_advanced_python_wrapper/sqlalchemy_driver_dialect.py +127 -0
- aws_advanced_python_wrapper/stale_dns_plugin.py +209 -0
- aws_advanced_python_wrapper/states/__init__.py +13 -0
- aws_advanced_python_wrapper/states/session_state.py +94 -0
- aws_advanced_python_wrapper/states/session_state_service.py +221 -0
- aws_advanced_python_wrapper/utils/__init__.py +13 -0
- aws_advanced_python_wrapper/utils/atomic.py +51 -0
- aws_advanced_python_wrapper/utils/cache_map.py +99 -0
- aws_advanced_python_wrapper/utils/concurrent.py +100 -0
- aws_advanced_python_wrapper/utils/decorators.py +70 -0
- aws_advanced_python_wrapper/utils/failover_mode.py +39 -0
- aws_advanced_python_wrapper/utils/iamutils.py +75 -0
- aws_advanced_python_wrapper/utils/log.py +75 -0
- aws_advanced_python_wrapper/utils/messages.py +36 -0
- aws_advanced_python_wrapper/utils/mysql_exception_handler.py +73 -0
- aws_advanced_python_wrapper/utils/notifications.py +37 -0
- aws_advanced_python_wrapper/utils/pg_exception_handler.py +115 -0
- aws_advanced_python_wrapper/utils/properties.py +492 -0
- aws_advanced_python_wrapper/utils/rds_url_type.py +36 -0
- aws_advanced_python_wrapper/utils/rdsutils.py +226 -0
- aws_advanced_python_wrapper/utils/sliding_expiration_cache.py +146 -0
- aws_advanced_python_wrapper/utils/telemetry/default_telemetry_factory.py +82 -0
- aws_advanced_python_wrapper/utils/telemetry/null_telemetry.py +55 -0
- aws_advanced_python_wrapper/utils/telemetry/open_telemetry.py +189 -0
- aws_advanced_python_wrapper/utils/telemetry/telemetry.py +85 -0
- aws_advanced_python_wrapper/utils/telemetry/xray_telemetry.py +126 -0
- aws_advanced_python_wrapper/utils/utils.py +89 -0
- aws_advanced_python_wrapper/wrapper.py +322 -0
- aws_advanced_python_wrapper/writer_failover_handler.py +347 -0
- aws_advanced_python_wrapper-1.0.0.dist-info/LICENSE +201 -0
- aws_advanced_python_wrapper-1.0.0.dist-info/METADATA +261 -0
- aws_advanced_python_wrapper-1.0.0.dist-info/RECORD +70 -0
- 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)
|