diff --git a/README.md b/README.md index fd0c3215..08cb3936 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,10 @@ Support for Blue/Green deployments using the AWS Advanced Python Wrapper require Please note that Aurora Global Database and RDS Multi-AZ clusters with Blue/Green deployments is currently not supported. For detailed information on supported database versions, refer to the [Blue/Green Deployment Plugin Documentation](docs/using-the-python-wrapper/using-plugins/UsingTheBlueGreenPlugin.md). +#### Enhanced Failure Monitoring with MySQL + +Enhanced Failure Monitoring (both the `host_monitoring` and `host_monitoring_v2` plugins) is not supported with the MySQL Connector/Python driver. Both plugins rely on being able to abort an active connection from a separate monitoring thread when a host is determined to be unhealthy, and the MySQL Connector/Python driver does not support aborting connections from a separate thread. For more information, see the [Host Monitoring Plugin Documentation](docs/using-the-python-wrapper/using-plugins/UsingTheHostMonitoringPlugin.md). + #### MySQL Connector/Python C Extension When connecting to Aurora MySQL clusters, it is recommended to use the Python implementation of the MySQL Connector/Python driver by setting the `use_pure` connection argument to `True`. diff --git a/aws_advanced_python_wrapper/aurora_initial_connection_strategy_plugin.py b/aws_advanced_python_wrapper/aurora_initial_connection_strategy_plugin.py index 112c7c9b..51f214af 100644 --- a/aws_advanced_python_wrapper/aurora_initial_connection_strategy_plugin.py +++ b/aws_advanced_python_wrapper/aurora_initial_connection_strategy_plugin.py @@ -230,7 +230,7 @@ def _get_reader(self, props: Properties) -> Optional[HostInfo]: def init_host_provider(self, props: Properties, host_list_provider_service: HostListProviderService, init_host_provider_func: Callable): self._host_list_provider_service = host_list_provider_service - init_host_provider_func(props) + init_host_provider_func() def _has_no_readers(self) -> bool: if len(self._plugin_service.all_hosts) == 0: diff --git a/aws_advanced_python_wrapper/blue_green_plugin.py b/aws_advanced_python_wrapper/blue_green_plugin.py index 386caa53..e8618a05 100644 --- a/aws_advanced_python_wrapper/blue_green_plugin.py +++ b/aws_advanced_python_wrapper/blue_green_plugin.py @@ -638,7 +638,8 @@ def __init__(self, plugin_service: PluginService, props: Properties): self._telemetry_factory = plugin_service.get_telemetry_factory() self._provider_supplier: Callable[[PluginService, Properties, str], BlueGreenStatusProvider] = \ lambda _plugin_service, _props, bg_id: BlueGreenStatusProvider(_plugin_service, _props, bg_id) - self._bg_id = WrapperProperties.BG_ID.get_or_default(props).strip().lower() + bg_id = WrapperProperties.BG_ID.get(props) + self._bg_id = bg_id.strip().lower() if bg_id is not None else "1" self._rds_utils = RdsUtils() self._bg_status: Optional[BlueGreenStatus] = None self._is_iam_in_use = False diff --git a/aws_advanced_python_wrapper/plugin_service.py b/aws_advanced_python_wrapper/plugin_service.py index 22834646..82189c3d 100644 --- a/aws_advanced_python_wrapper/plugin_service.py +++ b/aws_advanced_python_wrapper/plugin_service.py @@ -914,9 +914,12 @@ def get_plugins(self) -> List[Plugin]: Messages.get_formatted("PluginManager.ConfigurationProfileNotFound", profile_name)) plugin_factories = DriverConfigurationProfiles.get_plugin_factories(profile_name) else: - plugin_codes = WrapperProperties.PLUGINS.get(self._props) + plugin_codes = WrapperProperties.PLUGINS.get(self._props, False) if plugin_codes is None: - plugin_codes = WrapperProperties.DEFAULT_PLUGINS + driver_dialect = self._container.plugin_service.driver_dialect + plugin_codes = WrapperProperties.MYSQL_CONNECTOR_DEFAULT_PLUGINS \ + if driver_dialect.dialect_code == "mysql-connector-python" \ + else WrapperProperties.DEFAULT_PLUGINS if plugin_codes != "": plugin_factories = self.create_plugin_factories_from_list(plugin_codes.split(",")) diff --git a/aws_advanced_python_wrapper/utils/properties.py b/aws_advanced_python_wrapper/utils/properties.py index 9181d833..efec458f 100644 --- a/aws_advanced_python_wrapper/utils/properties.py +++ b/aws_advanced_python_wrapper/utils/properties.py @@ -40,8 +40,18 @@ def __init__( def __str__(self): return f"WrapperProperty(name={self.name}, default_value={self.default_value})" - def get(self, props: Properties) -> Optional[str]: - if self.default_value: + def get(self, props: Properties, return_default: bool = True) -> Optional[str]: + """Retrieve this property's value from the given properties. + + :param props: the :class:`Properties` collection to read the value from. + :param return_default: if ``True``, fall back to this property's + ``default_value`` when the property is not present in ``props``. + If ``False``, the default value is ignored. + :return: the property's value, the default value when missing and + ``return_default`` is ``True``, or ``None`` if the property is + absent and no default applies. + """ + if self.default_value and return_default: return props.get(self.name, self.default_value) return props.get(self.name) @@ -62,11 +72,6 @@ def get_type(self, props: Properties, type_class: Type[T]) -> T: return value.lower() == "true" if isinstance(value, str) else bool(value) # type: ignore return type_class(value) # type: ignore - def get_or_default(self, props: Properties) -> str: - if not self.default_value: - raise ValueError(f"No default value found for property {self}") - return props.get(self.name, self.default_value) - def get_int(self, props: Properties) -> int: return self.get_type(props, int) @@ -81,7 +86,8 @@ def set(self, props: Properties, value: Any): class WrapperProperties: - DEFAULT_PLUGINS = "aurora_connection_tracker,failover,host_monitoring_v2" + DEFAULT_PLUGINS = "initial_connection,aurora_connection_tracker,failover_v2,host_monitoring_v2" + MYSQL_CONNECTOR_DEFAULT_PLUGINS = "initial_connection,aurora_connection_tracker,failover_v2" _DEFAULT_TOKEN_EXPIRATION_SEC = 15 * 60 PROFILE_NAME = WrapperProperty( diff --git a/docs/using-the-python-wrapper/using-plugins/UsingTheHostMonitoringPlugin.md b/docs/using-the-python-wrapper/using-plugins/UsingTheHostMonitoringPlugin.md index 50b828d1..5d49b4fd 100644 --- a/docs/using-the-python-wrapper/using-plugins/UsingTheHostMonitoringPlugin.md +++ b/docs/using-the-python-wrapper/using-plugins/UsingTheHostMonitoringPlugin.md @@ -16,7 +16,7 @@ One use case is to pair EFM with the [Failover Connection Plugin](./UsingTheFail The Host Monitoring Connection Plugin will be loaded by default if the [`plugins`](../UsingThePythonWrapper.md#connection-plugin-manager-parameters) parameter is not specified. The Host Monitoring Connection Plugin can also be explicitly loaded by adding the plugin code `host_monitoring` to the [`plugins`](../UsingThePythonWrapper.md#aws-advanced-python-wrapper-parameters) parameter. Enhanced Failure Monitoring is enabled by default when the Host Monitoring Connection Plugin is loaded, but it can be disabled by setting the `failure_detection_enabled` parameter to `False`. -This plugin only works with drivers that support aborting connections from a separate thread. At this moment, this plugin is incompatible with the MySQL Connector/Python driver. +This plugin only works with drivers that support aborting connections from a separate thread. At this moment, this plugin is incompatible with the MySQL Connector/Python driver because the driver does not support aborting connections from a separate thread. > [IMPORTANT]\ > The Host Monitoring Plugin creates monitoring threads in the background to monitor all connections established to each cluster instance. The monitoring threads can be cleaned up in two ways: @@ -88,6 +88,9 @@ finally: Host Monitoring Plugin v2, also known as `host_monitoring_v2`, is an alternative implementation of enhanced failure monitoring and it is functionally equivalent to the Host Monitoring Plugin described above. Both plugins share the same set of [configuration parameters](#enhanced-failure-monitoring-parameters). The `host_monitoring_v2` plugin is designed to be a drop-in replacement for the `host_monitoring` plugin. The `host_monitoring_v2` plugin can be used in any scenario where the `host_monitoring` plugin is mentioned. This plugin is enabled by default. The original EFM plugin can still be used by specifying `host_monitoring` in the `plugins` parameter. +> [!NOTE]\ +> Like the `host_monitoring` plugin, the `host_monitoring_v2` plugin only works with drivers that support aborting connections from a separate thread. At this moment, it is incompatible with the MySQL Connector/Python driver because the driver does not support aborting connections from a separate thread. + > [!NOTE]\ > Since these two plugins are separate plugins, users may decide to use them together with a single connection. While this should not have any negative side effects, it is not recommended. It is recommended to use either the `host_monitoring_v2` plugin, or the `host_monitoring` plugin where it's needed. diff --git a/tests/unit/test_iam_plugin.py b/tests/unit/test_iam_plugin.py index 9bf9e3af..3f528560 100644 --- a/tests/unit/test_iam_plugin.py +++ b/tests/unit/test_iam_plugin.py @@ -385,7 +385,8 @@ def test_connect_with_specified_host(iam_host: str, mocker, mock_plugin_service, def test_aws_supported_regions_url_exists(): url = "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html" - assert 200 == urllib.request.urlopen(url).getcode() + request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + assert 200 == urllib.request.urlopen(request).getcode() @pytest.mark.parametrize("host", [ diff --git a/tests/unit/test_limitless_plugin.py b/tests/unit/test_limitless_plugin.py index c54038a6..42431882 100644 --- a/tests/unit/test_limitless_plugin.py +++ b/tests/unit/test_limitless_plugin.py @@ -39,6 +39,7 @@ def mock_plugin_service(mocker, mock_driver_dialect, mock_conn, host_info): service_mock.current_host_info = host_info # Use a real AuroraPgDialect to pass isinstance checks in Python 3.12+ service_mock.database_dialect = AuroraPgDialect() + service_mock.props = Properties() type(service_mock).driver_dialect = mocker.PropertyMock(return_value=mock_driver_dialect) return service_mock