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
@@ -0,0 +1,345 @@
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 copy import copy
18
+ from dataclasses import dataclass
19
+ from datetime import datetime
20
+ from threading import Event, Lock, Thread
21
+ from time import sleep
22
+ from typing import (TYPE_CHECKING, Callable, ClassVar, Dict, List, Optional,
23
+ Set, Tuple)
24
+
25
+ from aws_advanced_python_wrapper.errors import AwsWrapperError
26
+ from aws_advanced_python_wrapper.host_selector import RandomHostSelector
27
+ from aws_advanced_python_wrapper.plugin import Plugin
28
+ from aws_advanced_python_wrapper.utils.cache_map import CacheMap
29
+ from aws_advanced_python_wrapper.utils.log import Logger
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.sliding_expiration_cache import \
34
+ SlidingExpirationCacheWithCleanupThread
35
+ from aws_advanced_python_wrapper.utils.telemetry.telemetry import (
36
+ TelemetryContext, TelemetryFactory, TelemetryGauge, TelemetryTraceLevel)
37
+
38
+ if TYPE_CHECKING:
39
+ from aws_advanced_python_wrapper.driver_dialect import DriverDialect
40
+ from aws_advanced_python_wrapper.hostinfo import HostInfo, HostRole
41
+ from aws_advanced_python_wrapper.pep249 import Connection
42
+ from aws_advanced_python_wrapper.plugin_service import PluginService
43
+ from aws_advanced_python_wrapper.utils.notifications import HostEvent
44
+
45
+ logger = Logger(__name__)
46
+
47
+ MAX_VALUE = 2147483647
48
+
49
+
50
+ class FastestResponseStrategyPlugin(Plugin):
51
+ _FASTEST_RESPONSE_STRATEGY_NAME = "fastest_response"
52
+ _SUBSCRIBED_METHODS: Set[str] = {"accepts_strategy",
53
+ "get_host_info_by_strategy",
54
+ "notify_host_list_changed"}
55
+
56
+ def __init__(self, plugin_service: PluginService, props: Properties):
57
+ self._plugin_service = plugin_service
58
+ self._properties = props
59
+ self._host_response_time_service: HostResponseTimeService = \
60
+ HostResponseTimeService(plugin_service, props, WrapperProperties.RESPONSE_MEASUREMENT_INTERVAL_MILLIS.get_int(props))
61
+ self._cache_expiration_nanos = WrapperProperties.RESPONSE_MEASUREMENT_INTERVAL_MILLIS.get_int(props) * 10 ^ 6
62
+ self._random_host_selector = RandomHostSelector()
63
+ self._cached_fastest_response_host_by_role: CacheMap[str, HostInfo] = CacheMap()
64
+ self._hosts: Tuple[HostInfo, ...] = ()
65
+
66
+ @property
67
+ def subscribed_methods(self) -> Set[str]:
68
+ return self._SUBSCRIBED_METHODS
69
+
70
+ def connect(
71
+ self,
72
+ target_driver_func: Callable,
73
+ driver_dialect: DriverDialect,
74
+ host_info: HostInfo,
75
+ props: Properties,
76
+ is_initial_connection: bool,
77
+ connect_func: Callable) -> Connection:
78
+ return self._connect(host_info, props, is_initial_connection, connect_func)
79
+
80
+ def force_connect(
81
+ self,
82
+ target_driver_func: Callable,
83
+ driver_dialect: DriverDialect,
84
+ host_info: HostInfo,
85
+ props: Properties,
86
+ is_initial_connection: bool,
87
+ force_connect_func: Callable) -> Connection:
88
+ return self._connect(host_info, props, is_initial_connection, force_connect_func)
89
+
90
+ def _connect(
91
+ self,
92
+ host: HostInfo,
93
+ properties: Properties,
94
+ is_initial_connection: bool,
95
+ connect_func: Callable) -> Connection:
96
+ conn = connect_func()
97
+
98
+ if is_initial_connection:
99
+ self._plugin_service.refresh_host_list(conn)
100
+
101
+ return conn
102
+
103
+ def accepts_strategy(self, role: HostRole, strategy: str) -> bool:
104
+ return strategy == FastestResponseStrategyPlugin._FASTEST_RESPONSE_STRATEGY_NAME
105
+
106
+ def get_host_info_by_strategy(self, role: HostRole, strategy: str) -> HostInfo:
107
+ if not self.accepts_strategy(role, strategy):
108
+ logger.error("FastestResponseStrategyPlugin.UnsupportedHostSelectorStrategy", strategy)
109
+ raise AwsWrapperError(Messages.get_formatted("FastestResponseStrategyPlugin.UnsupportedHostSelectorStrategy", strategy))
110
+
111
+ fastest_response_host: Optional[HostInfo] = self._cached_fastest_response_host_by_role.get(role.name)
112
+ if fastest_response_host is not None:
113
+
114
+ # Found a fastest host. Let's find it in the the latest topology.
115
+ for host in self._plugin_service.hosts:
116
+ if host == fastest_response_host:
117
+ # found the fastest host in the topology
118
+ return host
119
+ # It seems that the fastest cached host isn't in the latest topology.
120
+ # Let's ignore cached results and find the fastest host.
121
+
122
+ # Cached result isn't available. Need to find the fastest response time host.
123
+ eligible_hosts: List[FastestResponseStrategyPlugin.ResponseTimeTuple] = []
124
+ for host in self._plugin_service.hosts:
125
+ if role == host.role:
126
+ response_time_tuple = FastestResponseStrategyPlugin.ResponseTimeTuple(host,
127
+ self._host_response_time_service.get_response_time(host))
128
+ eligible_hosts.append(response_time_tuple)
129
+
130
+ # Sort by response time then retrieve the first host
131
+ sorted_eligible_hosts: List[FastestResponseStrategyPlugin.ResponseTimeTuple] = \
132
+ sorted(eligible_hosts, key=lambda x: x.response_time)
133
+
134
+ calculated_fastest_response_host = sorted_eligible_hosts[0].host_info
135
+ if calculated_fastest_response_host is None or \
136
+ self._host_response_time_service.get_response_time(calculated_fastest_response_host) == MAX_VALUE:
137
+ logger.debug("FastestResponseStrategyPlugin.RandomHostSelected")
138
+ return self._random_host_selector.get_host(self._plugin_service.hosts, role, self._properties)
139
+
140
+ self._cached_fastest_response_host_by_role.put(role.name,
141
+ calculated_fastest_response_host,
142
+ self._cache_expiration_nanos)
143
+
144
+ return calculated_fastest_response_host
145
+
146
+ def notify_host_list_changed(self, changes: Dict[str, Set[HostEvent]]):
147
+ self._hosts = self._plugin_service.hosts
148
+ if self._host_response_time_service is not None:
149
+ self._host_response_time_service.set_hosts(self._hosts)
150
+
151
+ @dataclass
152
+ class ResponseTimeTuple:
153
+ host_info: HostInfo
154
+ response_time: int
155
+
156
+
157
+ class FastestResponseStrategyPluginFactory:
158
+
159
+ def get_instance(self, plugin_service: PluginService, props: Properties) -> Plugin:
160
+ return FastestResponseStrategyPlugin(plugin_service, props)
161
+
162
+
163
+ class HostResponseTimeMonitor:
164
+
165
+ _MONITORING_PROPERTY_PREFIX: str = "frt-"
166
+ _NUM_OF_MEASURES: int = 5
167
+ _DEFAULT_CONNECT_TIMEOUT_SEC = 10
168
+
169
+ def __init__(self, plugin_service: PluginService, host_info: HostInfo, props: Properties, interval_ms: int):
170
+ self._plugin_service = plugin_service
171
+ self._host_info = host_info
172
+ self._properties = props
173
+ self._interval_ms = interval_ms
174
+
175
+ self._telemetry_factory: TelemetryFactory = self._plugin_service.get_telemetry_factory()
176
+ self._response_time: int = MAX_VALUE
177
+ self._lock: Lock = Lock()
178
+ self._monitoring_conn: Optional[Connection] = None
179
+ self._is_stopped: Event = Event()
180
+
181
+ self._host_id: Optional[str] = self._host_info.host_id
182
+ if self._host_id is None or self._host_id == "":
183
+ self._host_id = self._host_info.host
184
+
185
+ self._daemon_thread: Thread = Thread(daemon=True, target=self.run)
186
+
187
+ # Report current response time (in milliseconds) to telemetry engine.
188
+ # Report -1 if response time couldn't be measured.
189
+ self._response_time_gauge: TelemetryGauge = \
190
+ self._telemetry_factory.create_gauge("frt.response.time." + self._host_id,
191
+ lambda: self._response_time if self._response_time != MAX_VALUE else -1)
192
+ self._daemon_thread.start()
193
+
194
+ @property
195
+ def response_time(self):
196
+ return self._response_time
197
+
198
+ @response_time.setter
199
+ def response_time(self, response_time: int):
200
+ self._response_time = response_time
201
+
202
+ @property
203
+ def host_info(self):
204
+ return self._host_info
205
+
206
+ @property
207
+ def is_stopped(self):
208
+ return self._is_stopped.is_set()
209
+
210
+ def close(self):
211
+ self._is_stopped.set()
212
+ self._daemon_thread.join(5)
213
+ logger.debug("HostResponseTimeMonitor.Stopped", self._host_info.host)
214
+
215
+ def _get_current_time(self):
216
+ return datetime.now().microsecond / 1000 # milliseconds
217
+
218
+ def run(self):
219
+ context: TelemetryContext = self._telemetry_factory.open_telemetry_context(
220
+ "host response time thread", TelemetryTraceLevel.TOP_LEVEL)
221
+ context.set_attribute("url", self._host_info.url)
222
+ try:
223
+ while not self.is_stopped:
224
+ self._open_connection()
225
+
226
+ if self._monitoring_conn is not None:
227
+
228
+ response_time_sum = 0
229
+ count = 0
230
+ for i in range(self._NUM_OF_MEASURES):
231
+ if self.is_stopped:
232
+ break
233
+ start_time = self._get_current_time()
234
+ if self._plugin_service.driver_dialect.ping(self._monitoring_conn):
235
+ calculated_response_time = self._get_current_time() - start_time
236
+ response_time_sum = response_time_sum + calculated_response_time
237
+ count = count + 1
238
+
239
+ if count > 0:
240
+ self.response_time = response_time_sum / count
241
+ else:
242
+ self.response_time = MAX_VALUE
243
+ logger.debug("HostResponseTimeMonitor.ResponseTime", self._host_info.host, self._response_time)
244
+
245
+ sleep(self._interval_ms / 1000)
246
+
247
+ except InterruptedError:
248
+ # exit thread
249
+ logger.debug("HostResponseTimeMonitor.InterruptedExceptionDuringMonitoring", self._host_info.host)
250
+ except Exception as e:
251
+ # this should not be reached; log and exit thread
252
+ logger.debug("HostResponseTimeMonitor.ExceptionDuringMonitoringStop",
253
+ self._host_info.host,
254
+ e) # print full trace stack of the exception.
255
+ finally:
256
+ self._is_stopped.set()
257
+ if self._monitoring_conn is not None:
258
+ try:
259
+ self._monitoring_conn.close()
260
+ except Exception:
261
+ # Do nothing
262
+ pass
263
+
264
+ if context is not None:
265
+ context.close_context()
266
+
267
+ def _open_connection(self):
268
+ try:
269
+ driver_dialect = self._plugin_service.driver_dialect
270
+ if self._monitoring_conn is None or driver_dialect.is_closed(self._monitoring_conn):
271
+ monitoring_conn_properties: Properties = copy(self._properties)
272
+ for key, value in self._properties.items():
273
+ if key.startswith(self._MONITORING_PROPERTY_PREFIX):
274
+ monitoring_conn_properties[key[len(self._MONITORING_PROPERTY_PREFIX):len(key)]] = value
275
+ monitoring_conn_properties.pop(key, None)
276
+
277
+ # Set a default connect timeout if the user hasn't configured one
278
+ if monitoring_conn_properties.get(WrapperProperties.CONNECT_TIMEOUT_SEC.name, None) is None:
279
+ monitoring_conn_properties[WrapperProperties.CONNECT_TIMEOUT_SEC.name] = HostResponseTimeMonitor._DEFAULT_CONNECT_TIMEOUT_SEC
280
+
281
+ logger.debug("HostResponseTimeMonitor.OpeningConnection", self._host_info.url)
282
+ self._monitoring_conn = self._plugin_service.force_connect(self._host_info, monitoring_conn_properties, None)
283
+ logger.debug("HostResponseTimeMonitor.OpenedConnection", self._host_info.url)
284
+
285
+ except Exception:
286
+ if self._monitoring_conn is not None:
287
+ try:
288
+ self._monitoring_conn.close()
289
+ except Exception:
290
+ pass # ignore
291
+
292
+ self._monitoring_conn = None
293
+
294
+
295
+ class HostResponseTimeService:
296
+ _CACHE_EXPIRATION_NS: int = 6 * 10 ^ 11 # 10 minutes
297
+ _CACHE_CLEANUP_NS: int = 6 * 10 ^ 10 # 1 minute
298
+ _lock: Lock = Lock()
299
+ _monitoring_hosts: ClassVar[SlidingExpirationCacheWithCleanupThread[str, HostResponseTimeMonitor]] = \
300
+ SlidingExpirationCacheWithCleanupThread(_CACHE_CLEANUP_NS,
301
+ should_dispose_func=lambda monitor: True,
302
+ item_disposal_func=lambda monitor: HostResponseTimeService._monitor_close(monitor))
303
+
304
+ def __init__(self, plugin_service: PluginService, props: Properties, interval_ms: int):
305
+ self._plugin_service = plugin_service
306
+ self._properties = props
307
+ self._interval_ms = interval_ms
308
+ self._hosts: Tuple[HostInfo, ...] = ()
309
+ self._telemetry_factory: TelemetryFactory = self._plugin_service.get_telemetry_factory()
310
+ self._host_count_gauge: TelemetryGauge = self._telemetry_factory.create_gauge("frt.hosts.count", lambda: len(self._monitoring_hosts))
311
+
312
+ @property
313
+ def hosts(self) -> Tuple[HostInfo, ...]:
314
+ return self._hosts
315
+
316
+ @hosts.setter
317
+ def hosts(self, new_hosts: Tuple[HostInfo, ...]):
318
+ self._hosts = new_hosts
319
+
320
+ @staticmethod
321
+ def _monitor_close(monitor: HostResponseTimeMonitor):
322
+ try:
323
+ monitor.close()
324
+ except Exception:
325
+ pass
326
+
327
+ def get_response_time(self, host_info: HostInfo) -> int:
328
+ monitor: Optional[HostResponseTimeMonitor] = HostResponseTimeService._monitoring_hosts.get(host_info.url)
329
+ if monitor is None:
330
+ return MAX_VALUE
331
+ return monitor.response_time
332
+
333
+ def set_hosts(self, new_hosts: Tuple[HostInfo, ...]) -> None:
334
+ old_hosts_dict = {x.url: x for x in self.hosts}
335
+ self.hosts = new_hosts
336
+
337
+ for host in self.hosts:
338
+ if host.url not in old_hosts_dict:
339
+ with self._lock:
340
+ self._monitoring_hosts.compute_if_absent(host.url,
341
+ lambda _: HostResponseTimeMonitor(
342
+ self._plugin_service,
343
+ host,
344
+ self._properties,
345
+ self._interval_ms), self._CACHE_EXPIRATION_NS)