Skip to content

Commit 003f2ad

Browse files
[DPE-4118] Add support for rescanning cluster for unit rejoin after node drain (#462)
* Add support for rescanning cluster for unit rejoin after node drain * Add get_cluster_name method to MySQL helpers class * Delete unnecessary/unused method in MySQL helpers class * Update data_interface charm lib + fix lint warnings * Add unittest
1 parent cafbb5c commit 003f2ad

File tree

4 files changed

+102
-5
lines changed

4 files changed

+102
-5
lines changed

lib/charms/data_platform_libs/v0/data_interfaces.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent):
331331

332332
# Increment this PATCH version before using `charmcraft publish-lib` or reset
333333
# to 0 if you are raising the major API version
334-
LIBPATCH = 37
334+
LIBPATCH = 38
335335

336336
PYDEPS = ["ops>=2.0.0"]
337337

@@ -2606,6 +2606,14 @@ def set_version(self, relation_id: int, version: str) -> None:
26062606
"""
26072607
self.update_relation_data(relation_id, {"version": version})
26082608

2609+
def set_subordinated(self, relation_id: int) -> None:
2610+
"""Raises the subordinated flag in the application relation databag.
2611+
2612+
Args:
2613+
relation_id: the identifier for a particular relation.
2614+
"""
2615+
self.update_relation_data(relation_id, {"subordinated": "true"})
2616+
26092617

26102618
class DatabaseProviderEventHandlers(EventHandlers):
26112619
"""Provider-side of the database relation handlers."""
@@ -2842,6 +2850,21 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:
28422850

28432851
def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
28442852
"""Event emitted when the database relation has changed."""
2853+
is_subordinate = False
2854+
remote_unit_data = None
2855+
for key in event.relation.data.keys():
2856+
if isinstance(key, Unit) and not key.name.startswith(self.charm.app.name):
2857+
remote_unit_data = event.relation.data[key]
2858+
elif isinstance(key, Application) and key.name != self.charm.app.name:
2859+
is_subordinate = event.relation.data[key].get("subordinated") == "true"
2860+
2861+
if is_subordinate:
2862+
if not remote_unit_data:
2863+
return
2864+
2865+
if remote_unit_data.get("state") != "ready":
2866+
return
2867+
28452868
# Check which data has changed to emit customs events.
28462869
diff = self._diff(event)
28472870

lib/charms/mysql/v0/mysql.py

+41-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 = 61
133+
LIBPATCH = 62
134134

135135
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
136136
UNIT_ADD_LOCKNAME = "unit-add"
@@ -402,6 +402,10 @@ class MySQLRejoinClusterError(Error):
402402
"""Exception raised when there is an issue trying to rejoin a cluster to the cluster set."""
403403

404404

405+
class MySQLGetClusterNameError(Error):
406+
"""Exception raised when there is an issue getting cluster name."""
407+
408+
405409
@dataclasses.dataclass
406410
class RouterUser:
407411
"""MySQL Router user."""
@@ -1792,7 +1796,10 @@ def is_instance_in_cluster(self, unit_label: str) -> bool:
17921796
logger.debug(f"Checking existence of unit {unit_label} in cluster {self.cluster_name}")
17931797

17941798
output = self._run_mysqlsh_script("\n".join(commands))
1795-
return MySQLMemberState.ONLINE in output.lower()
1799+
return (
1800+
MySQLMemberState.ONLINE in output.lower()
1801+
or MySQLMemberState.RECOVERING in output.lower()
1802+
)
17961803
except MySQLClientError:
17971804
# confirmation can fail if the clusteradmin user does not yet exist on the instance
17981805
logger.debug(
@@ -1805,7 +1812,9 @@ def is_instance_in_cluster(self, unit_label: str) -> bool:
18051812
stop=stop_after_attempt(3),
18061813
retry=retry_if_exception_type(TimeoutError),
18071814
)
1808-
def get_cluster_status(self, extended: Optional[bool] = False) -> Optional[dict]:
1815+
def get_cluster_status(
1816+
self, from_instance: Optional[str] = None, extended: Optional[bool] = False
1817+
) -> Optional[dict]:
18091818
"""Get the cluster status.
18101819
18111820
Executes script to retrieve cluster status.
@@ -1817,7 +1826,7 @@ def get_cluster_status(self, extended: Optional[bool] = False) -> Optional[dict]
18171826
"""
18181827
options = {"extended": extended}
18191828
status_commands = (
1820-
f"shell.connect('{self.cluster_admin_user}:{self.cluster_admin_password}@{self.instance_address}')",
1829+
f"shell.connect('{self.cluster_admin_user}:{self.cluster_admin_password}@{from_instance or self.instance_address}')",
18211830
f"cluster = dba.get_cluster('{self.cluster_name}')",
18221831
f"print(cluster.status({options}))",
18231832
)
@@ -2265,6 +2274,34 @@ def get_cluster_set_global_primary_address(
22652274

22662275
return matches.group(1)
22672276

2277+
def get_cluster_name(self, connect_instance_address: Optional[str]) -> Optional[str]:
2278+
"""Get the cluster name from instance.
2279+
2280+
Uses the mysql_innodb_cluster_metadata.clusters table in case instance connecting
2281+
to is offline and thus cannot use mysqlsh.dba.get_cluster().
2282+
"""
2283+
if not connect_instance_address:
2284+
connect_instance_address = self.instance_address
2285+
2286+
logger.debug(f"Getting cluster name from {connect_instance_address}")
2287+
get_cluster_name_commands = (
2288+
f"shell.connect('{self.cluster_admin_user}:{self.cluster_admin_password}@{connect_instance_address}')",
2289+
'cluster_name = session.run_sql("SELECT cluster_name FROM mysql_innodb_cluster_metadata.clusters;")',
2290+
"print(f'<CLUSTER_NAME>{cluster_name.fetch_one()[0]}</CLUSTER_NAME>')",
2291+
)
2292+
2293+
try:
2294+
output = self._run_mysqlsh_script("\n".join(get_cluster_name_commands))
2295+
except MySQLClientError as e:
2296+
logger.warning("Failed to get cluster name")
2297+
raise MySQLGetClusterNameError(e.message)
2298+
2299+
matches = re.search(r"<CLUSTER_NAME>(.+)</CLUSTER_NAME>", output)
2300+
if not matches:
2301+
return None
2302+
2303+
return matches.group(1)
2304+
22682305
def get_primary_label(self) -> Optional[str]:
22692306
"""Get the label of the cluster's primary."""
22702307
status = self.get_cluster_status()

poetry.lock

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/unit/test_mysql.py

+17
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,23 @@ def test_get_cluster_endpoints(self, _, _is_cluster_replica):
979979
),
980980
)
981981

982+
@patch("charms.mysql.v0.mysql.MySQLBase._run_mysqlsh_script")
983+
def test_get_cluster_name(self, _run_mysqlsh_script):
984+
"""Test get_cluster_name() method."""
985+
commands = "\n".join(
986+
(
987+
"shell.connect('clusteradmin:[email protected]')",
988+
'cluster_name = session.run_sql("SELECT cluster_name FROM mysql_innodb_cluster_metadata.clusters;")',
989+
"print(f'<CLUSTER_NAME>{cluster_name.fetch_one()[0]}</CLUSTER_NAME>')",
990+
)
991+
)
992+
993+
_run_mysqlsh_script.return_value = "<CLUSTER_NAME>test_cluster</CLUSTER_NAME>"
994+
995+
cluster_name = self.mysql.get_cluster_name("1.2.3.4")
996+
self.assertEqual(cluster_name, "test_cluster")
997+
_run_mysqlsh_script.assert_called_once_with(commands)
998+
982999
@patch("charms.mysql.v0.mysql.MySQLBase._run_mysqlsh_script")
9831000
def test_offline_mode_and_hidden_instance_exists(self, _run_mysqlsh_script):
9841001
"""Test the offline_mode_and_hidden_instance_exists() method."""

0 commit comments

Comments
 (0)