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,949 @@
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 typing import TYPE_CHECKING, ClassVar, List, Type
18
+
19
+ from aws_advanced_python_wrapper.aurora_initial_connection_strategy_plugin import \
20
+ AuroraInitialConnectionStrategyPluginFactory
21
+ from aws_advanced_python_wrapper.fastest_response_strategy_plugin import \
22
+ FastestResponseStrategyPluginFactory
23
+ from aws_advanced_python_wrapper.federated_plugin import \
24
+ FederatedAuthPluginFactory
25
+ from aws_advanced_python_wrapper.states.session_state_service import (
26
+ SessionStateService, SessionStateServiceImpl)
27
+
28
+ if TYPE_CHECKING:
29
+ from aws_advanced_python_wrapper.driver_dialect import DriverDialect
30
+ from aws_advanced_python_wrapper.driver_dialect_manager import DriverDialectManager
31
+ from aws_advanced_python_wrapper.pep249 import Connection
32
+ from aws_advanced_python_wrapper.plugin import Plugin, PluginFactory
33
+ from threading import Event
34
+
35
+ from abc import abstractmethod
36
+ from concurrent.futures import Executor, ThreadPoolExecutor, TimeoutError
37
+ from contextlib import closing
38
+ from typing import (Any, Callable, Dict, FrozenSet, Optional, Protocol, Set,
39
+ Tuple)
40
+
41
+ from aws_advanced_python_wrapper.aurora_connection_tracker_plugin import \
42
+ AuroraConnectionTrackerPluginFactory
43
+ from aws_advanced_python_wrapper.aws_secrets_manager_plugin import \
44
+ AwsSecretsManagerPluginFactory
45
+ from aws_advanced_python_wrapper.connect_time_plugin import \
46
+ ConnectTimePluginFactory
47
+ from aws_advanced_python_wrapper.connection_provider import (
48
+ ConnectionProvider, ConnectionProviderManager)
49
+ from aws_advanced_python_wrapper.database_dialect import (
50
+ DatabaseDialect, DatabaseDialectManager, TopologyAwareDatabaseDialect,
51
+ UnknownDatabaseDialect)
52
+ from aws_advanced_python_wrapper.default_plugin import DefaultPlugin
53
+ from aws_advanced_python_wrapper.developer_plugin import DeveloperPluginFactory
54
+ from aws_advanced_python_wrapper.driver_configuration_profiles import \
55
+ DriverConfigurationProfiles
56
+ from aws_advanced_python_wrapper.errors import (AwsWrapperError,
57
+ QueryTimeoutError,
58
+ UnsupportedOperationError)
59
+ from aws_advanced_python_wrapper.exception_handling import (ExceptionHandler,
60
+ ExceptionManager)
61
+ from aws_advanced_python_wrapper.execute_time_plugin import \
62
+ ExecuteTimePluginFactory
63
+ from aws_advanced_python_wrapper.failover_plugin import FailoverPluginFactory
64
+ from aws_advanced_python_wrapper.host_availability import HostAvailability
65
+ from aws_advanced_python_wrapper.host_list_provider import (
66
+ ConnectionStringHostListProvider, HostListProvider,
67
+ HostListProviderService, StaticHostListProvider)
68
+ from aws_advanced_python_wrapper.host_monitoring_plugin import \
69
+ HostMonitoringPluginFactory
70
+ from aws_advanced_python_wrapper.hostinfo import HostInfo, HostRole
71
+ from aws_advanced_python_wrapper.iam_plugin import IamAuthPluginFactory
72
+ from aws_advanced_python_wrapper.plugin import CanReleaseResources
73
+ from aws_advanced_python_wrapper.read_write_splitting_plugin import \
74
+ ReadWriteSplittingPluginFactory
75
+ from aws_advanced_python_wrapper.stale_dns_plugin import StaleDnsPluginFactory
76
+ from aws_advanced_python_wrapper.utils.cache_map import CacheMap
77
+ from aws_advanced_python_wrapper.utils.decorators import \
78
+ preserve_transaction_status_with_timeout
79
+ from aws_advanced_python_wrapper.utils.log import Logger
80
+ from aws_advanced_python_wrapper.utils.messages import Messages
81
+ from aws_advanced_python_wrapper.utils.notifications import (
82
+ ConnectionEvent, HostEvent, OldConnectionSuggestedAction)
83
+ from aws_advanced_python_wrapper.utils.properties import (Properties,
84
+ PropertiesUtils,
85
+ WrapperProperties)
86
+ from aws_advanced_python_wrapper.utils.telemetry.telemetry import (
87
+ TelemetryContext, TelemetryFactory, TelemetryTraceLevel)
88
+
89
+ logger = Logger(__name__)
90
+
91
+
92
+ class PluginServiceManagerContainer:
93
+ @property
94
+ def plugin_service(self) -> PluginService:
95
+ return self._plugin_service
96
+
97
+ @plugin_service.setter
98
+ def plugin_service(self, value):
99
+ self._plugin_service = value
100
+
101
+ @property
102
+ def plugin_manager(self) -> PluginManager:
103
+ return self._plugin_manager
104
+
105
+ @plugin_manager.setter
106
+ def plugin_manager(self, value):
107
+ self._plugin_manager = value
108
+
109
+
110
+ class PluginService(ExceptionHandler, Protocol):
111
+ @property
112
+ @abstractmethod
113
+ def hosts(self) -> Tuple[HostInfo, ...]:
114
+ ...
115
+
116
+ @property
117
+ @abstractmethod
118
+ def current_connection(self) -> Optional[Connection]:
119
+ ...
120
+
121
+ def set_current_connection(self, connection: Connection, host_info: HostInfo):
122
+ ...
123
+
124
+ @property
125
+ @abstractmethod
126
+ def current_host_info(self) -> Optional[HostInfo]:
127
+ ...
128
+
129
+ @property
130
+ @abstractmethod
131
+ def initial_connection_host_info(self) -> Optional[HostInfo]:
132
+ ...
133
+
134
+ @property
135
+ @abstractmethod
136
+ def host_list_provider(self) -> HostListProvider:
137
+ ...
138
+
139
+ @host_list_provider.setter
140
+ @abstractmethod
141
+ def host_list_provider(self, provider: HostListProvider):
142
+ ...
143
+
144
+ @property
145
+ @abstractmethod
146
+ def session_state_service(self):
147
+ ...
148
+
149
+ @property
150
+ @abstractmethod
151
+ def is_in_transaction(self) -> bool:
152
+ ...
153
+
154
+ @property
155
+ @abstractmethod
156
+ def driver_dialect(self) -> DriverDialect:
157
+ ...
158
+
159
+ @property
160
+ @abstractmethod
161
+ def database_dialect(self) -> DatabaseDialect:
162
+ ...
163
+
164
+ @property
165
+ @abstractmethod
166
+ def network_bound_methods(self) -> Set[str]:
167
+ ...
168
+
169
+ @property
170
+ @abstractmethod
171
+ def props(self) -> Properties:
172
+ ...
173
+
174
+ def is_network_bound_method(self, method_name: str) -> bool:
175
+ ...
176
+
177
+ def update_in_transaction(self, is_in_transaction: Optional[bool] = None):
178
+ ...
179
+
180
+ def update_dialect(self, connection: Optional[Connection] = None):
181
+ ...
182
+
183
+ def update_driver_dialect(self, connection_provider: ConnectionProvider):
184
+ ...
185
+
186
+ def accepts_strategy(self, role: HostRole, strategy: str) -> bool:
187
+ """
188
+ Returns a boolean indicating if any of the configured :py:class:`ConnectionPlugin` or :py:class:`ConnectionProvider` objects implement the
189
+ specified host selection strategy for the given role in :py:method:`ConnectionPlugin.get_host_info_by_strategy`
190
+ or :py:method:`ConnectionProvider.get_host_info_by_strategy`.
191
+
192
+ :param role: the desired role of the selected host - either a reader host or a writer host.
193
+ :param strategy: the strategy that should be used to pick a host (eg "random").
194
+ :return: `True` if any of the configured :py:class:`ConnectionPlugin` or :py:class:`ConnectionProvider` objects support the selection of a
195
+ host with the requested role and strategy via :py:method:`ConnectionPlugin.get_host_info_by_strategy`
196
+ or :py:method:`ConnectionProvider.get_host_info_by_strategy`. Otherwise, return `False`.
197
+ """
198
+ ...
199
+
200
+ def get_host_info_by_strategy(self, role: HostRole, strategy: str) -> Optional[HostInfo]:
201
+ """
202
+ Selects a :py:class:`HostInfo` with the requested role from available hosts using the requested strategy.
203
+ :py:method:`PluginService.accepts_strategy` should be called first to evaluate if any of the configured :py:class:`ConnectionPlugin`
204
+ or :py:class:`ConnectionProvider` objects support the selection of a host with the requested role and strategy.
205
+
206
+ :param role: the desired role of the selected host - either a reader host or a writer host.
207
+ :param strategy: the strategy that should be used to pick a host (eg "random").
208
+ :return: a py:class:`HostInfo` with the requested role.
209
+ """
210
+ ...
211
+
212
+ def get_host_role(self, connection: Optional[Connection] = None) -> HostRole:
213
+ ...
214
+
215
+ def refresh_host_list(self, connection: Optional[Connection] = None):
216
+ ...
217
+
218
+ def force_refresh_host_list(self, connection: Optional[Connection] = None):
219
+ ...
220
+
221
+ def connect(self, host_info: HostInfo, props: Properties) -> Connection:
222
+ """
223
+ Establishes a connection to the given host using the given driver protocol and properties. If a
224
+ non-default :py:class`ConnectionProvider` has been set with :py:method:`ConnectionProviderManager.set_connection_provider`,
225
+ the connection will be created by the non-default ConnectionProvider.
226
+ Otherwise, the connection will be created by the default :py:class`DriverConnectionProvider`.
227
+
228
+ :param host_info: the host details for the desired connection.
229
+ :param props: the connection properties.
230
+ :return: a :py:class`Connection` to the requested host.
231
+ """
232
+ ...
233
+
234
+ def force_connect(self, host_info: HostInfo, props: Properties, timeout_event: Optional[Event]) -> Connection:
235
+ """
236
+ Establishes a connection to the given host using the given driver protocol and properties.
237
+ This call differs from connect in that the default :py:class`DriverConnectionProvider` will be used to establish the connection even if
238
+ a non-default :py:class`ConnectionProvider` has been set via :py:method:`ConnectionProviderManager.set_connection_provider`.
239
+
240
+ :param host_info: the host details for the desired connection.
241
+ :param props: the connection properties.
242
+ :return: a :py:class`Connection` to the requested host.
243
+ """
244
+ ...
245
+
246
+ def set_availability(self, host_aliases: FrozenSet[str], availability: HostAvailability):
247
+ ...
248
+
249
+ def identify_connection(self, connection: Optional[Connection] = None) -> Optional[HostInfo]:
250
+ ...
251
+
252
+ def fill_aliases(self, connection: Optional[Connection] = None, host_info: Optional[HostInfo] = None):
253
+ ...
254
+
255
+ def get_connection_provider_manager(self) -> ConnectionProviderManager:
256
+ ...
257
+
258
+ def get_telemetry_factory(self) -> TelemetryFactory:
259
+ ...
260
+
261
+
262
+ class PluginServiceImpl(PluginService, HostListProviderService, CanReleaseResources):
263
+ _host_availability_expiring_cache: CacheMap[str, HostAvailability] = CacheMap()
264
+
265
+ _executor: ClassVar[Executor] = ThreadPoolExecutor(thread_name_prefix="PluginServiceImplExecutor")
266
+
267
+ def __init__(
268
+ self,
269
+ container: PluginServiceManagerContainer,
270
+ props: Properties,
271
+ target_func: Callable,
272
+ driver_dialect_manager: DriverDialectManager,
273
+ driver_dialect: DriverDialect,
274
+ session_state_service: Optional[SessionStateService] = None):
275
+ self._container = container
276
+ self._container.plugin_service = self
277
+ self._props = props
278
+ self._original_url = PropertiesUtils.get_url(props)
279
+ self._host_list_provider: HostListProvider = ConnectionStringHostListProvider(self, props)
280
+
281
+ self._hosts: Tuple[HostInfo, ...] = ()
282
+ self._current_connection: Optional[Connection] = None
283
+ self._current_host_info: Optional[HostInfo] = None
284
+ self._initial_connection_host_info: Optional[HostInfo] = None
285
+ self._exception_manager: ExceptionManager = ExceptionManager()
286
+ self._is_in_transaction: bool = False
287
+ self._dialect_provider = DatabaseDialectManager(props)
288
+ self._target_func = target_func
289
+ self._driver_dialect_manager = driver_dialect_manager
290
+ self._driver_dialect = driver_dialect
291
+ self._database_dialect = self._dialect_provider.get_dialect(driver_dialect.dialect_code, props)
292
+ self._session_state_service = session_state_service if session_state_service is not None else SessionStateServiceImpl(self, props)
293
+
294
+ @property
295
+ def hosts(self) -> Tuple[HostInfo, ...]:
296
+ return self._hosts
297
+
298
+ @hosts.setter
299
+ def hosts(self, new_hosts: Tuple[HostInfo, ...]):
300
+ self._hosts = new_hosts
301
+
302
+ @property
303
+ def current_connection(self) -> Optional[Connection]:
304
+ return self._current_connection
305
+
306
+ def set_current_connection(self, connection: Optional[Connection], host_info: Optional[HostInfo]):
307
+ old_connection = self._current_connection
308
+
309
+ if self._current_connection is None:
310
+ self._current_connection = connection
311
+ self._current_host_info = host_info
312
+ self.session_state_service.reset()
313
+
314
+ self._container.plugin_manager.notify_connection_changed({ConnectionEvent.INITIAL_CONNECTION})
315
+ return
316
+
317
+ if connection is not None and old_connection is not None and old_connection != connection:
318
+ # Update an existing connection.
319
+
320
+ is_in_transaction = self._is_in_transaction
321
+ self.session_state_service.begin()
322
+
323
+ try:
324
+ self._current_connection = connection
325
+ self._current_host_info = host_info
326
+
327
+ self.session_state_service.apply_current_session_state(connection)
328
+ self.update_in_transaction(False)
329
+
330
+ if is_in_transaction and WrapperProperties.ROLLBACK_ON_SWITCH.get_bool(self.props):
331
+ try:
332
+ old_connection.rollback()
333
+ except Exception:
334
+ # Ignore any exception.
335
+ pass
336
+
337
+ old_connection_suggested_action = \
338
+ self._container.plugin_manager.notify_connection_changed({ConnectionEvent.CONNECTION_OBJECT_CHANGED})
339
+
340
+ if old_connection_suggested_action != OldConnectionSuggestedAction.PRESERVE and not self.driver_dialect.is_closed(old_connection):
341
+
342
+ try:
343
+ self.session_state_service.apply_pristine_session_state(old_connection)
344
+ except Exception:
345
+ # Ignore any exception.
346
+ pass
347
+
348
+ try:
349
+ old_connection.close()
350
+ except Exception:
351
+ # Ignore any exception.
352
+ pass
353
+ finally:
354
+ self.session_state_service.complete()
355
+
356
+ @property
357
+ def current_host_info(self) -> Optional[HostInfo]:
358
+ return self._current_host_info
359
+
360
+ @property
361
+ def initial_connection_host_info(self) -> Optional[HostInfo]:
362
+ return self._initial_connection_host_info
363
+
364
+ @initial_connection_host_info.setter
365
+ def initial_connection_host_info(self, value: HostInfo):
366
+ self._initial_connection_host_info = value
367
+
368
+ @property
369
+ def host_list_provider(self) -> HostListProvider:
370
+ return self._host_list_provider
371
+
372
+ @host_list_provider.setter
373
+ def host_list_provider(self, value: HostListProvider):
374
+ self._host_list_provider = value
375
+
376
+ @property
377
+ def session_state_service(self) -> SessionStateService:
378
+ return self._session_state_service
379
+
380
+ @property
381
+ def is_in_transaction(self) -> bool:
382
+ return self._is_in_transaction
383
+
384
+ @property
385
+ def driver_dialect(self) -> DriverDialect:
386
+ return self._driver_dialect
387
+
388
+ @property
389
+ def database_dialect(self) -> DatabaseDialect:
390
+ return self._database_dialect
391
+
392
+ @property
393
+ def network_bound_methods(self) -> Set[str]:
394
+ return self._driver_dialect.network_bound_methods
395
+
396
+ @property
397
+ def props(self) -> Properties:
398
+ return self._props
399
+
400
+ def update_in_transaction(self, is_in_transaction: Optional[bool] = None):
401
+ if is_in_transaction is not None:
402
+ self._is_in_transaction = is_in_transaction
403
+ elif self.current_connection is not None:
404
+ self._is_in_transaction = self.driver_dialect.is_in_transaction(self.current_connection)
405
+ else:
406
+ raise AwsWrapperError(Messages.get("PluginServiceImpl.UnableToUpdateTransactionStatus"))
407
+
408
+ def is_network_bound_method(self, method_name: str):
409
+ if len(self.network_bound_methods) == 1 and \
410
+ list(self.network_bound_methods)[0] == "*":
411
+ return True
412
+ return method_name in self.network_bound_methods
413
+
414
+ def update_dialect(self, connection: Optional[Connection] = None):
415
+ # Updates both database dialects and driver dialect
416
+
417
+ connection = self.current_connection if connection is None else connection
418
+ if connection is None:
419
+ raise AwsWrapperError(Messages.get("PluginServiceImpl.UpdateDialectConnectionNone"))
420
+
421
+ original_dialect = self._database_dialect
422
+ self._database_dialect = \
423
+ self._dialect_provider.query_for_dialect(
424
+ self._original_url,
425
+ self._initial_connection_host_info,
426
+ connection,
427
+ self.driver_dialect)
428
+
429
+ if original_dialect != self._database_dialect:
430
+ host_list_provider_init = self._database_dialect.get_host_list_provider_supplier()
431
+ self.host_list_provider = host_list_provider_init(self, self._props)
432
+
433
+ def update_driver_dialect(self, connection_provider: ConnectionProvider):
434
+ self._driver_dialect = self._driver_dialect_manager.get_pool_connection_driver_dialect(
435
+ connection_provider, self._driver_dialect, self._props)
436
+
437
+ def accepts_strategy(self, role: HostRole, strategy: str) -> bool:
438
+ plugin_manager: PluginManager = self._container.plugin_manager
439
+ return plugin_manager.accepts_strategy(role, strategy)
440
+
441
+ def get_host_info_by_strategy(self, role: HostRole, strategy: str) -> Optional[HostInfo]:
442
+ plugin_manager: PluginManager = self._container.plugin_manager
443
+ return plugin_manager.get_host_info_by_strategy(role, strategy)
444
+
445
+ def get_host_role(self, connection: Optional[Connection] = None) -> HostRole:
446
+ connection = connection if connection is not None else self.current_connection
447
+ if connection is None:
448
+ raise AwsWrapperError(Messages.get("PluginServiceImpl.GetHostRoleConnectionNone"))
449
+
450
+ return self._host_list_provider.get_host_role(connection)
451
+
452
+ def refresh_host_list(self, connection: Optional[Connection] = None):
453
+ connection = self.current_connection if connection is None else connection
454
+ updated_host_list: Tuple[HostInfo, ...] = self.host_list_provider.refresh(connection)
455
+ if updated_host_list != self.hosts:
456
+ self._update_host_availability(updated_host_list)
457
+ self._update_hosts(updated_host_list)
458
+
459
+ def force_refresh_host_list(self, connection: Optional[Connection] = None):
460
+ connection = self.current_connection if connection is None else connection
461
+ updated_host_list: Tuple[HostInfo, ...] = self.host_list_provider.force_refresh(connection)
462
+ if updated_host_list != self.hosts:
463
+ self._update_host_availability(updated_host_list)
464
+ self._update_hosts(updated_host_list)
465
+
466
+ def connect(self, host_info: HostInfo, props: Properties) -> Connection:
467
+ plugin_manager: PluginManager = self._container.plugin_manager
468
+ return plugin_manager.connect(
469
+ self._target_func, self._driver_dialect, host_info, props, self.current_connection is None)
470
+
471
+ def force_connect(self, host_info: HostInfo, props: Properties, timeout_event: Optional[Event]) -> Connection:
472
+ plugin_manager: PluginManager = self._container.plugin_manager
473
+ return plugin_manager.force_connect(
474
+ self._target_func, self._driver_dialect, host_info, props, self.current_connection is None)
475
+
476
+ def set_availability(self, host_aliases: FrozenSet[str], availability: HostAvailability):
477
+ ...
478
+
479
+ def identify_connection(self, connection: Optional[Connection] = None) -> Optional[HostInfo]:
480
+ connection = self.current_connection if connection is None else connection
481
+
482
+ if not isinstance(self.database_dialect, TopologyAwareDatabaseDialect):
483
+ return None
484
+
485
+ return self.host_list_provider.identify_connection(connection)
486
+
487
+ def fill_aliases(self, connection: Optional[Connection] = None, host_info: Optional[HostInfo] = None):
488
+ connection = self.current_connection if connection is None else connection
489
+ host_info = self.current_host_info if host_info is None else host_info
490
+ if connection is None or host_info is None:
491
+ return
492
+
493
+ if len(host_info.aliases) > 0:
494
+ logger.debug("PluginServiceImpl.NonEmptyAliases", host_info.aliases)
495
+ return
496
+
497
+ host_info.add_alias(host_info.as_alias())
498
+
499
+ driver_dialect = self._driver_dialect
500
+ try:
501
+ timeout_sec = WrapperProperties.AUXILIARY_QUERY_TIMEOUT_SEC.get(self._props)
502
+ cursor_execute_func_with_timeout = preserve_transaction_status_with_timeout(
503
+ PluginServiceImpl._executor, timeout_sec, driver_dialect, connection)(self._fill_aliases)
504
+ cursor_execute_func_with_timeout(connection, host_info)
505
+ except TimeoutError as e:
506
+ raise QueryTimeoutError(Messages.get("PluginServiceImpl.FillAliasesTimeout")) from e
507
+ except Exception as e:
508
+ # log and ignore
509
+ logger.debug("PluginServiceImpl.FailedToRetrieveHostPort", e)
510
+
511
+ host = self.identify_connection(connection)
512
+ if host:
513
+ host_info.add_alias(*host.as_aliases())
514
+
515
+ def _fill_aliases(self, conn: Connection, host_info: HostInfo) -> bool:
516
+ with closing(conn.cursor()) as cursor:
517
+ if not isinstance(self.database_dialect, UnknownDatabaseDialect):
518
+ cursor.execute(self.database_dialect.host_alias_query)
519
+ for row in cursor.fetchall():
520
+ host_info.add_alias(row[0])
521
+ return True
522
+ return False
523
+
524
+ def is_static_host_list_provider(self) -> bool:
525
+ return self._host_list_provider is StaticHostListProvider
526
+
527
+ def is_network_exception(self, error: Optional[Exception] = None, sql_state: Optional[str] = None) -> bool:
528
+ return self._exception_manager.is_network_exception(
529
+ dialect=self.database_dialect, error=error, sql_state=sql_state)
530
+
531
+ def is_login_exception(self, error: Optional[Exception] = None, sql_state: Optional[str] = None) -> bool:
532
+ return self._exception_manager.is_login_exception(
533
+ dialect=self.database_dialect, error=error, sql_state=sql_state)
534
+
535
+ def get_connection_provider_manager(self) -> ConnectionProviderManager:
536
+ return self._container.plugin_manager.connection_provider_manager
537
+
538
+ def get_telemetry_factory(self) -> TelemetryFactory:
539
+ return self._container.plugin_manager.telemetry_factory
540
+
541
+ def _update_host_availability(self, hosts: Tuple[HostInfo, ...]):
542
+ for host in hosts:
543
+ availability: Optional[HostAvailability] = self._host_availability_expiring_cache.get(host.url)
544
+ if availability:
545
+ host.set_availability(availability)
546
+
547
+ def _update_hosts(self, new_hosts: Tuple[HostInfo, ...]):
548
+ old_hosts_dict = {x.url: x for x in self.hosts}
549
+ new_hosts_dict = {x.url: x for x in new_hosts}
550
+
551
+ changes: Dict[str, Set[HostEvent]] = {}
552
+
553
+ for host in self.hosts:
554
+ corresponding_new_host = new_hosts_dict.get(host.url)
555
+ if corresponding_new_host is None:
556
+ changes[host.url] = {HostEvent.HOST_DELETED}
557
+ else:
558
+ host_changes: Set[HostEvent] = self._compare(host, corresponding_new_host)
559
+ if len(host_changes) > 0:
560
+ changes[host.url] = host_changes
561
+
562
+ for key, value in new_hosts_dict.items():
563
+ if key not in old_hosts_dict:
564
+ changes[key] = {HostEvent.HOST_ADDED}
565
+
566
+ if len(changes) > 0:
567
+ self.hosts = tuple(new_hosts) if new_hosts is not None else ()
568
+ self._container.plugin_manager.notify_host_list_changed(changes)
569
+
570
+ def _compare(self, host_a: HostInfo, host_b: HostInfo) -> Set[HostEvent]:
571
+ changes: Set[HostEvent] = set()
572
+ if host_a.host != host_b.host or host_a.port != host_b.port:
573
+ changes.add(HostEvent.URL_CHANGED)
574
+
575
+ if host_a.role != host_b.role:
576
+ if host_b.role == HostRole.WRITER:
577
+ changes.add(HostEvent.CONVERTED_TO_WRITER)
578
+ elif host_b.role == HostRole.READER:
579
+ changes.add(HostEvent.CONVERTED_TO_READER)
580
+
581
+ if host_a.get_availability() != host_b.get_availability():
582
+ if host_b.get_availability() == HostAvailability.AVAILABLE:
583
+ changes.add(HostEvent.WENT_UP)
584
+ elif host_b.get_availability() == HostAvailability.UNAVAILABLE:
585
+ changes.add(HostEvent.WENT_DOWN)
586
+
587
+ if len(changes) > 0:
588
+ changes.add(HostEvent.HOST_CHANGED)
589
+
590
+ return changes
591
+
592
+ def release_resources(self):
593
+ try:
594
+ if self.current_connection is not None and not self.driver_dialect.is_closed(
595
+ self.current_connection):
596
+ self.current_connection.close()
597
+ except Exception:
598
+ # ignore
599
+ pass
600
+
601
+ host_list_provider = self.host_list_provider
602
+ if host_list_provider is not None and isinstance(host_list_provider, CanReleaseResources):
603
+ host_list_provider.release_resources()
604
+
605
+
606
+ class PluginManager(CanReleaseResources):
607
+ _ALL_METHODS: str = "*"
608
+ _CONNECT_METHOD: str = "connect"
609
+ _FORCE_CONNECT_METHOD: str = "force_connect"
610
+ _NOTIFY_CONNECTION_CHANGED_METHOD: str = "notify_connection_changed"
611
+ _NOTIFY_HOST_LIST_CHANGED_METHOD: str = "notify_host_list_changed"
612
+ _GET_HOST_INFO_BY_STRATEGY_METHOD: str = "get_host_info_by_strategy"
613
+ _INIT_HOST_LIST_PROVIDER_METHOD: str = "init_host_provider"
614
+
615
+ PLUGIN_FACTORIES: Dict[str, Type[PluginFactory]] = {
616
+ "iam": IamAuthPluginFactory,
617
+ "aws_secrets_manager": AwsSecretsManagerPluginFactory,
618
+ "aurora_connection_tracker": AuroraConnectionTrackerPluginFactory,
619
+ "host_monitoring": HostMonitoringPluginFactory,
620
+ "failover": FailoverPluginFactory,
621
+ "read_write_splitting": ReadWriteSplittingPluginFactory,
622
+ "fastest_response_strategy": FastestResponseStrategyPluginFactory,
623
+ "stale_dns": StaleDnsPluginFactory,
624
+ "connect_time": ConnectTimePluginFactory,
625
+ "execute_time": ExecuteTimePluginFactory,
626
+ "dev": DeveloperPluginFactory,
627
+ "federated_auth": FederatedAuthPluginFactory,
628
+ "initial_connection": AuroraInitialConnectionStrategyPluginFactory
629
+ }
630
+
631
+ WEIGHT_RELATIVE_TO_PRIOR_PLUGIN = -1
632
+
633
+ # The final list of plugins will be sorted by weight, starting from the lowest values up to
634
+ # the highest values. The first plugin of the list will have the lowest weight, and the
635
+ # last one will have the highest weight.
636
+ PLUGIN_FACTORY_WEIGHTS: Dict[Type[PluginFactory], int] = {
637
+ AuroraInitialConnectionStrategyPluginFactory: 50,
638
+ AuroraConnectionTrackerPluginFactory: 100,
639
+ StaleDnsPluginFactory: 200,
640
+ ReadWriteSplittingPluginFactory: 300,
641
+ FailoverPluginFactory: 400,
642
+ HostMonitoringPluginFactory: 500,
643
+ FastestResponseStrategyPluginFactory: 600,
644
+ IamAuthPluginFactory: 700,
645
+ AwsSecretsManagerPluginFactory: 800,
646
+ ConnectTimePluginFactory: WEIGHT_RELATIVE_TO_PRIOR_PLUGIN,
647
+ ExecuteTimePluginFactory: WEIGHT_RELATIVE_TO_PRIOR_PLUGIN,
648
+ DeveloperPluginFactory: WEIGHT_RELATIVE_TO_PRIOR_PLUGIN,
649
+ FederatedAuthPluginFactory: WEIGHT_RELATIVE_TO_PRIOR_PLUGIN
650
+ }
651
+
652
+ def __init__(
653
+ self, container: PluginServiceManagerContainer, props: Properties, telemetry_factory: TelemetryFactory):
654
+ self._props: Properties = props
655
+ self._function_cache: Dict[str, Callable] = {}
656
+ self._container = container
657
+ self._container.plugin_manager = self
658
+ self._connection_provider_manager = ConnectionProviderManager()
659
+ self._telemetry_factory = telemetry_factory
660
+ self._plugins = self.get_plugins()
661
+
662
+ @property
663
+ def num_plugins(self) -> int:
664
+ return len(self._plugins)
665
+
666
+ @property
667
+ def connection_provider_manager(self) -> ConnectionProviderManager:
668
+ return self._connection_provider_manager
669
+
670
+ @property
671
+ def telemetry_factory(self) -> TelemetryFactory:
672
+ return self._telemetry_factory
673
+
674
+ def get_current_connection_provider(self, host_info: HostInfo, properties: Properties):
675
+ return self.connection_provider_manager.get_connection_provider(host_info, properties)
676
+
677
+ @staticmethod
678
+ def register_plugin(
679
+ plugin_code: str, plugin_factory: Type[PluginFactory], weight: int = WEIGHT_RELATIVE_TO_PRIOR_PLUGIN):
680
+ PluginManager.PLUGIN_FACTORIES[plugin_code] = plugin_factory
681
+ PluginManager.PLUGIN_FACTORY_WEIGHTS[plugin_factory] = weight
682
+
683
+ def get_plugins(self) -> List[Plugin]:
684
+ plugin_factories: List[PluginFactory] = []
685
+ plugins: List[Plugin] = []
686
+
687
+ profile_name = WrapperProperties.PROFILE_NAME.get(self._props)
688
+ if profile_name is not None:
689
+ if not DriverConfigurationProfiles.contains_profile(profile_name):
690
+ raise AwsWrapperError(
691
+ Messages.get_formatted("PluginManager.ConfigurationProfileNotFound", profile_name))
692
+ plugin_factories = DriverConfigurationProfiles.get_plugin_factories(profile_name)
693
+ else:
694
+ plugin_codes = WrapperProperties.PLUGINS.get(self._props)
695
+ if plugin_codes is None:
696
+ plugin_codes = WrapperProperties.DEFAULT_PLUGINS
697
+
698
+ if plugin_codes != "":
699
+ plugin_factories = self.create_plugin_factories_from_list(plugin_codes.split(","))
700
+
701
+ for factory in plugin_factories:
702
+ plugin = factory.get_instance(self._container.plugin_service, self._props)
703
+ plugins.append(plugin)
704
+
705
+ plugins.append(DefaultPlugin(self._container.plugin_service, self._connection_provider_manager))
706
+
707
+ return plugins
708
+
709
+ def create_plugin_factories_from_list(self, plugin_code_list: List[str]) -> List[PluginFactory]:
710
+ factory_types: List[Type[PluginFactory]] = []
711
+ for plugin_code in plugin_code_list:
712
+ plugin_code = plugin_code.strip()
713
+ if plugin_code not in PluginManager.PLUGIN_FACTORIES:
714
+ raise AwsWrapperError(Messages.get_formatted("PluginManager.InvalidPlugin", plugin_code))
715
+
716
+ factory_types.append(PluginManager.PLUGIN_FACTORIES[plugin_code])
717
+
718
+ if len(factory_types) <= 0:
719
+ return []
720
+
721
+ auto_sort_plugins = WrapperProperties.AUTO_SORT_PLUGIN_ORDER.get(self._props)
722
+ if auto_sort_plugins:
723
+ weights = PluginManager.get_factory_weights(factory_types)
724
+ factory_types.sort(key=lambda factory_type: weights[factory_type])
725
+ plugin_code_list.sort(key=lambda plugin_code: weights[PluginManager.PLUGIN_FACTORIES[plugin_code]])
726
+ logger.debug("PluginManager.ResortedPlugins", plugin_code_list)
727
+
728
+ factories: List[PluginFactory] = []
729
+ for factory_type in factory_types:
730
+ factory = object.__new__(factory_type)
731
+ factories.append(factory)
732
+
733
+ return factories
734
+
735
+ @staticmethod
736
+ def get_factory_weights(factory_types: List[Type[PluginFactory]]) -> Dict[Type[PluginFactory], int]:
737
+ last_weight = 0
738
+ weights: Dict[Type[PluginFactory], int] = {}
739
+ for factory_type in factory_types:
740
+ weight = PluginManager.PLUGIN_FACTORY_WEIGHTS[factory_type]
741
+ if weight is None or weight == PluginManager.WEIGHT_RELATIVE_TO_PRIOR_PLUGIN:
742
+ # This plugin factory is unknown, or it has relative (to prior plugin factory) weight.
743
+ last_weight += 1
744
+ weights[factory_type] = last_weight
745
+ else:
746
+ # Otherwise, use the wight assigned to this plugin factory
747
+ weights[factory_type] = weight
748
+
749
+ # Remember this weight for subsequent factories that may have relative (to this plugin factory) weight.
750
+ last_weight = weight
751
+
752
+ return weights
753
+
754
+ def execute(self, target: object, method_name: str, target_driver_func: Callable, *args, **kwargs) -> Any:
755
+ plugin_service = self._container.plugin_service
756
+ driver_dialect = plugin_service.driver_dialect
757
+ conn: Optional[Connection] = driver_dialect.get_connection_from_obj(target)
758
+ current_conn: Optional[Connection] = driver_dialect.unwrap_connection(plugin_service.current_connection)
759
+
760
+ if method_name not in ["Connection.close", "Cursor.close"] and conn is not None and conn != current_conn:
761
+ raise AwsWrapperError(Messages.get_formatted("PluginManager.MethodInvokedAgainstOldConnection", target))
762
+
763
+ if conn is None and method_name in ["Connection.close", "Cursor.close"]:
764
+ return
765
+
766
+ context: TelemetryContext
767
+ context = self._telemetry_factory.open_telemetry_context(method_name, TelemetryTraceLevel.TOP_LEVEL)
768
+ context.set_attribute("python_call", method_name)
769
+
770
+ try:
771
+ result = self._execute_with_subscribed_plugins(
772
+ method_name,
773
+ # next_plugin_func is defined later in make_pipeline
774
+ lambda plugin, next_plugin_func: plugin.execute(target, method_name, next_plugin_func, *args, **kwargs),
775
+ target_driver_func)
776
+
777
+ context.set_success(True)
778
+
779
+ return result
780
+ except Exception as e:
781
+ context.set_success(False)
782
+ raise e
783
+ finally:
784
+ context.close_context()
785
+
786
+ def _execute_with_telemetry(self, plugin_name: str, func: Callable):
787
+ context = self._telemetry_factory.open_telemetry_context(plugin_name, TelemetryTraceLevel.NESTED)
788
+ try:
789
+ return func()
790
+ finally:
791
+ context.close_context()
792
+
793
+ def _execute_with_subscribed_plugins(self, method_name: str, plugin_func: Callable, target_driver_func: Callable):
794
+ pipeline_func: Optional[Callable] = self._function_cache.get(method_name)
795
+ if pipeline_func is None:
796
+ pipeline_func = self._make_pipeline(method_name)
797
+ self._function_cache[method_name] = pipeline_func
798
+
799
+ return pipeline_func(plugin_func, target_driver_func)
800
+
801
+ # Builds the plugin pipeline function chain. The pipeline is built in a way that allows plugins to perform logic
802
+ # both before and after the target driver function call.
803
+ def _make_pipeline(self, method_name: str) -> Callable:
804
+ pipeline_func: Optional[Callable] = None
805
+ num_plugins: int = len(self._plugins)
806
+
807
+ # Build the pipeline starting at the end and working backwards
808
+ for i in range(num_plugins - 1, -1, -1):
809
+ plugin: Plugin = self._plugins[i]
810
+ subscribed_methods: Set[str] = plugin.subscribed_methods
811
+ is_subscribed: bool = PluginManager._ALL_METHODS in subscribed_methods or method_name in subscribed_methods
812
+
813
+ if is_subscribed:
814
+ if pipeline_func is None:
815
+ # Defines the call to DefaultPlugin, which is the last plugin in the pipeline
816
+ pipeline_func = self._create_base_pipeline_func(plugin)
817
+ else:
818
+ pipeline_func = self._extend_pipeline_func(plugin, pipeline_func)
819
+
820
+ if pipeline_func is None:
821
+ raise AwsWrapperError(Messages.get("PluginManager.PipelineNone"))
822
+ else:
823
+ return pipeline_func
824
+
825
+ def _create_base_pipeline_func(self, plugin: Plugin):
826
+ # The plugin passed here will be the DefaultPlugin, which is the last plugin in the pipeline
827
+ # The second arg to plugin_func is the next call in the pipeline. Here, it is the target driver function
828
+ plugin_name = plugin.__class__.__name__
829
+ return lambda plugin_func, target_driver_func: self._execute_with_telemetry(
830
+ plugin_name, lambda: plugin_func(plugin, target_driver_func))
831
+
832
+ def _extend_pipeline_func(self, plugin: Plugin, pipeline_so_far: Callable):
833
+ # Defines the call to a plugin that precedes the DefaultPlugin in the pipeline
834
+ # The second arg to plugin_func effectively appends the tail end of the pipeline to the current plugin's call
835
+ plugin_name = plugin.__class__.__name__
836
+ return lambda plugin_func, target_driver_func: self._execute_with_telemetry(
837
+ plugin_name, lambda: plugin_func(plugin, lambda: pipeline_so_far(plugin_func, target_driver_func)))
838
+
839
+ def connect(
840
+ self,
841
+ target_func: Callable,
842
+ driver_dialect: DriverDialect,
843
+ host_info: Optional[HostInfo],
844
+ props: Properties,
845
+ is_initial_connection: bool) -> Connection:
846
+ context = self._telemetry_factory.open_telemetry_context("connect", TelemetryTraceLevel.NESTED)
847
+ try:
848
+ return self._execute_with_subscribed_plugins(
849
+ PluginManager._CONNECT_METHOD,
850
+ lambda plugin, func: plugin.connect(
851
+ target_func, driver_dialect, host_info, props, is_initial_connection, func),
852
+ # The final connect action will be handled by the ConnectionProvider, so this lambda will not be called.
853
+ lambda: None)
854
+ finally:
855
+ context.close_context()
856
+
857
+ def force_connect(
858
+ self,
859
+ target_func: Callable,
860
+ driver_dialect: DriverDialect,
861
+ host_info: Optional[HostInfo],
862
+ props: Properties,
863
+ is_initial_connection: bool) -> Connection:
864
+ return self._execute_with_subscribed_plugins(
865
+ PluginManager._FORCE_CONNECT_METHOD,
866
+ lambda plugin, func: plugin.force_connect(
867
+ target_func, driver_dialect, host_info, props, is_initial_connection, func),
868
+ # The final connect action will be handled by the ConnectionProvider, so this lambda will not be called.
869
+ lambda: None)
870
+
871
+ def notify_connection_changed(self, changes: Set[ConnectionEvent]) -> OldConnectionSuggestedAction:
872
+ old_conn_suggestions: Set[OldConnectionSuggestedAction] = set()
873
+ self._notify_subscribed_plugins(
874
+ PluginManager._NOTIFY_CONNECTION_CHANGED_METHOD,
875
+ lambda plugin: self._notify_plugin_conn_changed(plugin, changes, old_conn_suggestions))
876
+
877
+ if OldConnectionSuggestedAction.PRESERVE in old_conn_suggestions:
878
+ return OldConnectionSuggestedAction.PRESERVE
879
+ elif OldConnectionSuggestedAction.DISPOSE in old_conn_suggestions:
880
+ return OldConnectionSuggestedAction.DISPOSE
881
+ else:
882
+ return OldConnectionSuggestedAction.NO_OPINION
883
+
884
+ def _notify_subscribed_plugins(self, method_name: str, notify_plugin_func: Callable):
885
+ for plugin in self._plugins:
886
+ subscribed_methods = plugin.subscribed_methods
887
+ is_subscribed = PluginManager._ALL_METHODS in subscribed_methods or method_name in subscribed_methods
888
+ if is_subscribed:
889
+ notify_plugin_func(plugin)
890
+
891
+ def _notify_plugin_conn_changed(
892
+ self,
893
+ plugin: Plugin,
894
+ changes: Set[ConnectionEvent],
895
+ old_conn_suggestions: Set[OldConnectionSuggestedAction]):
896
+ suggestion = plugin.notify_connection_changed(changes)
897
+ old_conn_suggestions.add(suggestion)
898
+
899
+ def notify_host_list_changed(self, changes: Dict[str, Set[HostEvent]]):
900
+ self._notify_subscribed_plugins(PluginManager._NOTIFY_HOST_LIST_CHANGED_METHOD,
901
+ lambda plugin: plugin.notify_host_list_changed(changes))
902
+
903
+ def accepts_strategy(self, role: HostRole, strategy: str) -> bool:
904
+ for plugin in self._plugins:
905
+ plugin_subscribed_methods = plugin.subscribed_methods
906
+ is_subscribed = \
907
+ self._ALL_METHODS in plugin_subscribed_methods \
908
+ or self._GET_HOST_INFO_BY_STRATEGY_METHOD in plugin_subscribed_methods
909
+ if is_subscribed:
910
+ if plugin.accepts_strategy(role, strategy):
911
+ return True
912
+
913
+ return False
914
+
915
+ def get_host_info_by_strategy(self, role: HostRole, strategy: str) -> Optional[HostInfo]:
916
+ for plugin in self._plugins:
917
+ plugin_subscribed_methods = plugin.subscribed_methods
918
+ is_subscribed = \
919
+ self._ALL_METHODS in plugin_subscribed_methods \
920
+ or self._GET_HOST_INFO_BY_STRATEGY_METHOD in plugin_subscribed_methods
921
+
922
+ if is_subscribed:
923
+ try:
924
+ host: HostInfo = plugin.get_host_info_by_strategy(role, strategy)
925
+ if host is not None:
926
+ return host
927
+ except UnsupportedOperationError:
928
+ # This plugin does not support the requested strategy, ignore exception and try the next plugin
929
+ pass
930
+ return None
931
+
932
+ def init_host_provider(self, props: Properties, host_list_provider_service: HostListProviderService):
933
+ context = self._telemetry_factory.open_telemetry_context("init_host_provider", TelemetryTraceLevel.NESTED)
934
+ try:
935
+ return self._execute_with_subscribed_plugins(
936
+ PluginManager._INIT_HOST_LIST_PROVIDER_METHOD,
937
+ lambda plugin, func: plugin.init_host_provider(props, host_list_provider_service, func),
938
+ lambda: None)
939
+ finally:
940
+ context.close_context()
941
+
942
+ def release_resources(self):
943
+ """
944
+ Allows all connection plugins a chance to clean up any dangling resources
945
+ or perform any last tasks before shutting down.
946
+ """
947
+ for plugin in self._plugins:
948
+ if isinstance(plugin, CanReleaseResources):
949
+ plugin.release_resources()