Skip to content

Commit 547aa59

Browse files
authored
DPE-3706 Fix max_connections calculation and expose experimental max_connections field (#429)
* fix max_connections calculation * Add experimental max-connections configuration * use config and allow restart for single unit * missing comment * clearer variable name * change calculation logic * libpatch bump
1 parent ccd5922 commit 547aa59

File tree

6 files changed

+129
-9
lines changed

6 files changed

+129
-9
lines changed

config.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ options:
3030
mysql-interface-database:
3131
description: "The database name for the legacy 'mysql' relation"
3232
type: "string"
33+
# Experimental features
34+
experimental-max-connections:
35+
type: int
36+
description: |
37+
Maximum number of connections allowed to the MySQL server.
38+
When set max-connections value take precedence over the memory utilizations
39+
againts innodb_buffer_pool_size.
40+
This is an experimental feature and may be removed in future releases.

lib/charms/mysql/v0/mysql.py

+29-4
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def wait_until_mysql_connection(self) -> None:
130130

131131
# Increment this PATCH version before using `charmcraft publish-lib` or reset
132132
# to 0 if you are raising the major API version
133-
LIBPATCH = 59
133+
LIBPATCH = 60
134134

135135
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
136136
UNIT_ADD_LOCKNAME = "unit-add"
@@ -141,7 +141,8 @@ def wait_until_mysql_connection(self) -> None:
141141
BYTES_1MiB = 1048576 # 1 mebibyte
142142
RECOVERY_CHECK_TIME = 10 # seconds
143143
GET_MEMBER_STATE_TIME = 10 # seconds
144-
MIN_MAX_CONNECTIONS = 100
144+
MAX_CONNECTIONS_FLOOR = 10
145+
MIM_MEM_BUFFERS = 200 * BYTES_1MiB
145146

146147
SECRET_INTERNAL_LABEL = "secret-id"
147148
SECRET_DELETED_LABEL = "None"
@@ -868,36 +869,60 @@ def render_mysqld_configuration(
868869
*,
869870
profile: str,
870871
memory_limit: Optional[int] = None,
872+
experimental_max_connections: Optional[int] = None,
871873
snap_common: str = "",
872874
) -> tuple[str, dict]:
873875
"""Render mysqld ini configuration file.
874876
875877
Args:
876878
profile: profile to use for the configuration (testing, production)
877879
memory_limit: memory limit to use for the configuration in bytes
880+
experimental_max_connections: explicit max connections to use for the configuration
878881
snap_common: snap common directory (for log files locations in vm)
879882
880883
Returns: a tuple with mysqld ini file string content and a the config dict
881884
"""
885+
max_connections = None
882886
performance_schema_instrument = ""
883887
if profile == "testing":
884888
innodb_buffer_pool_size = 20 * BYTES_1MiB
885889
innodb_buffer_pool_chunk_size = 1 * BYTES_1MiB
886890
group_replication_message_cache_size = 128 * BYTES_1MiB
887-
max_connections = MIN_MAX_CONNECTIONS
891+
max_connections = 100
888892
performance_schema_instrument = "'memory/%=OFF'"
889893
else:
890894
available_memory = self.get_available_memory()
891895
if memory_limit:
892896
# when memory limit is set, we need to use the minimum
893897
# between the available memory and the limit
894898
available_memory = min(available_memory, memory_limit)
899+
900+
if experimental_max_connections:
901+
# when set, we use the experimental max connections
902+
# and it takes precedence over buffers usage
903+
max_connections = experimental_max_connections
904+
# we reserve 200MiB for memory buffers
905+
# even when there's some overcommittment
906+
available_memory = max(
907+
available_memory - max_connections * 12 * BYTES_1MiB, 200 * BYTES_1MiB
908+
)
909+
895910
(
896911
innodb_buffer_pool_size,
897912
innodb_buffer_pool_chunk_size,
898913
group_replication_message_cache_size,
899914
) = self.get_innodb_buffer_pool_parameters(available_memory)
900-
max_connections = max(self.get_max_connections(available_memory), MIN_MAX_CONNECTIONS)
915+
916+
# constrain max_connections based on the available memory
917+
# after innodb_buffer_pool_size calculation
918+
available_memory -= innodb_buffer_pool_size + (
919+
group_replication_message_cache_size or 0
920+
)
921+
if not max_connections:
922+
max_connections = max(
923+
self.get_max_connections(available_memory), MAX_CONNECTIONS_FLOOR
924+
)
925+
901926
if available_memory < 2 * BYTES_1GiB:
902927
# disable memory instruments if we have less than 2GiB of RAM
903928
performance_schema_instrument = "'memory/%=OFF'"

src/charm.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ def _on_config_changed(self, event: EventBase) -> None:
254254
profile=self.config.profile,
255255
snap_common=CHARMED_MYSQL_COMMON_DIRECTORY,
256256
memory_limit=memory_limit_bytes,
257+
experimental_max_connections=self.config.experimental_max_connections,
257258
)
258259

259260
changed_config = compare_dictionaries(previous_config, new_config_dict)
@@ -612,7 +613,9 @@ def workload_initialise(self) -> None:
612613
Raised errors must be treated on handlers.
613614
"""
614615
self._mysql.write_mysqld_config(
615-
profile=self.config.profile, memory_limit=self.config.profile_limit_memory
616+
profile=self.config.profile,
617+
memory_limit=self.config.profile_limit_memory,
618+
experimental_max_connections=self.config.experimental_max_connections,
616619
)
617620
self._mysql.setup_logrotate_and_cron()
618621
self._mysql.reset_root_password_and_start_mysqld()
@@ -799,19 +802,22 @@ def _restart(self, event: EventBase) -> None:
799802
logger.debug("Deferring restart until all units are in the relation")
800803
event.defer()
801804
return
802-
if self._mysql.is_unit_primary(self.unit_label):
805+
if self.peers.units and self._mysql.is_unit_primary(self.unit_label):
803806
restart_states = {
804807
self.restart_peers.data[unit].get("state", "unset") for unit in self.peers.units
805808
}
806-
if restart_states != {"release"}:
809+
if restart_states == {"unset"}:
810+
logger.debug("Restarting leader")
811+
elif restart_states != {"release"}:
807812
# Wait other units restart first to minimize primary switchover
808813
logger.debug("Primary is waiting for other units to restart")
809814
event.defer()
810815
return
811816

812817
self.unit.status = MaintenanceStatus("restarting MySQL")
813818
self._mysql.restart_mysqld()
814-
self.unit.status = ActiveStatus(self.active_status_message)
819+
sleep(10)
820+
self._on_update_status(None)
815821

816822

817823
if __name__ == "__main__":

src/config.py

+13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Optional
1111

1212
from charms.data_platform_libs.v0.data_models import BaseConfigModel
13+
from charms.mysql.v0.mysql import MAX_CONNECTIONS_FLOOR
1314
from pydantic import validator
1415

1516
logger = logging.getLogger(__name__)
@@ -61,6 +62,7 @@ class CharmConfig(BaseConfigModel):
6162
profile_limit_memory: Optional[int]
6263
mysql_interface_user: Optional[str]
6364
mysql_interface_database: Optional[str]
65+
experimental_max_connections: Optional[int]
6466

6567
@validator("profile")
6668
@classmethod
@@ -103,3 +105,14 @@ def profile_limit_memory_validator(cls, value: int) -> Optional[int]:
103105
raise ValueError("`profile-limit-memory` limited to 7 digits (9999999MB)")
104106

105107
return value
108+
109+
@validator("experimental_max_connections")
110+
@classmethod
111+
def experimental_max_connections_validator(cls, value: int) -> Optional[int]:
112+
"""Check experimental max connections."""
113+
if value < MAX_CONNECTIONS_FLOOR:
114+
raise ValueError(
115+
f"experimental-max-connections must be greater than {MAX_CONNECTIONS_FLOOR}"
116+
)
117+
118+
return value

src/mysql_vm_helpers.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,18 @@ def get_available_memory(self) -> int:
233233
logger.error("Failed to query system memory")
234234
raise MySQLGetAvailableMemoryError
235235

236-
def write_mysqld_config(self, profile: str, memory_limit: Optional[int]) -> None:
236+
def write_mysqld_config(
237+
self,
238+
profile: str,
239+
memory_limit: Optional[int],
240+
experimental_max_connections: Optional[int] = None,
241+
) -> None:
237242
"""Create custom mysql config file.
238243
239244
Args:
240245
profile: profile to use for the mysql config
241246
memory_limit: memory limit to use for the mysql config in MB
247+
experimental_max_connections: experimental max connections to use for the mysql config
242248
243249
Raises: MySQLCreateCustomMySQLDConfigError if there is an error creating the
244250
custom mysqld config
@@ -252,6 +258,7 @@ def write_mysqld_config(self, profile: str, memory_limit: Optional[int]) -> None
252258
profile=profile,
253259
snap_common=CHARMED_MYSQL_COMMON_DIRECTORY,
254260
memory_limit=memory_limit,
261+
experimental_max_connections=experimental_max_connections,
255262
)
256263
except (MySQLGetAvailableMemoryError, MySQLGetAutoTunningParametersError):
257264
logger.exception("Failed to get available memory or auto tuning parameters")

tests/unit/test_mysql.py

+61
Original file line numberDiff line numberDiff line change
@@ -1805,6 +1805,67 @@ def test_hold_if_recovering(self, mock_get_member_state):
18051805
self.mysql.hold_if_recovering()
18061806
self.assertEqual(mock_get_member_state.call_count, 1)
18071807

1808+
@patch("charms.mysql.v0.mysql.MySQLBase.get_available_memory")
1809+
def test_render_mysqld_configuration(self, _get_available_memory):
1810+
"""Test render_mysqld_configuration."""
1811+
# 32GB of memory, production profile
1812+
_get_available_memory.return_value = 32341442560
1813+
1814+
expected_config = {
1815+
"bind-address": "0.0.0.0",
1816+
"mysqlx-bind-address": "0.0.0.0",
1817+
"report_host": "127.0.0.1",
1818+
"max_connections": "724",
1819+
"innodb_buffer_pool_size": "23219666944",
1820+
"log_error_services": "log_filter_internal;log_sink_internal",
1821+
"log_error": "/var/log/mysql/error.log",
1822+
"general_log": "ON",
1823+
"general_log_file": "/var/log/mysql/general.log",
1824+
"slow_query_log_file": "/var/log/mysql/slowquery.log",
1825+
"innodb_buffer_pool_chunk_size": "2902458368",
1826+
}
1827+
1828+
_, rendered_config = self.mysql.render_mysqld_configuration(profile="production")
1829+
self.assertEqual(rendered_config, expected_config)
1830+
1831+
# < 2GB of memory, production profile
1832+
memory_limit = 2147483600
1833+
1834+
expected_config["innodb_buffer_pool_size"] = "536870912"
1835+
del expected_config["innodb_buffer_pool_chunk_size"]
1836+
expected_config["performance-schema-instrument"] = "'memory/%=OFF'"
1837+
expected_config["max_connections"] = "127"
1838+
1839+
_, rendered_config = self.mysql.render_mysqld_configuration(
1840+
profile="production", memory_limit=memory_limit
1841+
)
1842+
self.assertEqual(rendered_config, expected_config)
1843+
1844+
# testing profile
1845+
expected_config["innodb_buffer_pool_size"] = "20971520"
1846+
expected_config["innodb_buffer_pool_chunk_size"] = "1048576"
1847+
expected_config["loose-group_replication_message_cache_size"] = "134217728"
1848+
expected_config["max_connections"] = "100"
1849+
1850+
_, rendered_config = self.mysql.render_mysqld_configuration(profile="testing")
1851+
self.assertEqual(rendered_config, expected_config)
1852+
1853+
# 10GB, max connections set by value
1854+
memory_limit = 10106700800
1855+
# max_connections set
1856+
_, rendered_config = self.mysql.render_mysqld_configuration(
1857+
profile="production", experimental_max_connections=500, memory_limit=memory_limit
1858+
)
1859+
1860+
self.assertEqual(rendered_config["max_connections"], "500")
1861+
1862+
# max_connections set,constrained by memory, but enforced
1863+
_, rendered_config = self.mysql.render_mysqld_configuration(
1864+
profile="production", experimental_max_connections=800, memory_limit=memory_limit
1865+
)
1866+
1867+
self.assertEqual(rendered_config["max_connections"], "800")
1868+
18081869
@patch("charms.mysql.v0.mysql.MySQLBase._run_mysqlsh_script")
18091870
def test_create_replica_cluster(self, _run_mysqlsh_script):
18101871
"""Test create_replica_cluster."""

0 commit comments

Comments
 (0)