Skip to content

Commit 7292f40

Browse files
Create Jubilant stop-primary test
1 parent 15c445b commit 7292f40

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Copyright 2025 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
4+
import logging
5+
import random
6+
7+
import jubilant_backports
8+
import pytest
9+
from jubilant_backports import Juju
10+
11+
from constants import CLUSTER_ADMIN_USERNAME, SERVER_CONFIG_USERNAME
12+
13+
from ..helpers import (
14+
execute_queries_on_unit,
15+
generate_random_string,
16+
is_connection_possible,
17+
)
18+
from .high_availability_helpers_new import (
19+
TEST_DATABASE_NAME,
20+
check_mysql_units_writes_increment,
21+
get_app_units,
22+
get_mysql_primary_unit,
23+
get_unit_ip,
24+
remove_mysql_test_data,
25+
start_mysql_process_gracefully,
26+
stop_mysql_process_gracefully,
27+
verify_mysql_test_data,
28+
wait_for_apps_status,
29+
)
30+
31+
MYSQL_APP_NAME = "mysql"
32+
MYSQL_TEST_APP_NAME = "mysql-test-app"
33+
34+
MINUTE_SECS = 60
35+
36+
logging.getLogger("jubilant.wait").setLevel(logging.WARNING)
37+
38+
39+
@pytest.mark.abort_on_fail
40+
def test_deploy_highly_available_cluster(juju: Juju, charm: str) -> None:
41+
"""Simple test to ensure that the MySQL and application charms get deployed."""
42+
logging.info("Deploying MySQL cluster")
43+
juju.deploy(
44+
charm=charm,
45+
app=MYSQL_APP_NAME,
46+
47+
config={"profile": "testing"},
48+
num_units=3,
49+
)
50+
juju.deploy(
51+
charm=MYSQL_TEST_APP_NAME,
52+
app=MYSQL_TEST_APP_NAME,
53+
54+
channel="latest/edge",
55+
config={"sleep_interval": 500},
56+
num_units=1,
57+
)
58+
59+
juju.integrate(
60+
f"{MYSQL_APP_NAME}:database",
61+
f"{MYSQL_TEST_APP_NAME}:database",
62+
)
63+
64+
logging.info("Wait for applications to become active")
65+
juju.wait(
66+
ready=wait_for_apps_status(
67+
jubilant_backports.all_active, MYSQL_APP_NAME, MYSQL_TEST_APP_NAME
68+
),
69+
error=jubilant_backports.any_blocked,
70+
timeout=20 * MINUTE_SECS,
71+
)
72+
73+
74+
@pytest.mark.abort_on_fail
75+
async def test_replicate_data_on_restart(juju: Juju, continuous_writes_new) -> None:
76+
"""Stop server, write data, start and validate replication."""
77+
# Ensure continuous writes still incrementing for all units
78+
await check_mysql_units_writes_increment(juju, MYSQL_APP_NAME)
79+
80+
mysql_units = get_app_units(juju, MYSQL_APP_NAME)
81+
mysql_primary_unit = get_mysql_primary_unit(juju, MYSQL_APP_NAME)
82+
mysql_primary_unit_ip = get_unit_ip(juju, MYSQL_APP_NAME, mysql_primary_unit)
83+
84+
credentials_task = juju.run(
85+
unit=mysql_primary_unit,
86+
action="get-password",
87+
params={"username": CLUSTER_ADMIN_USERNAME},
88+
)
89+
credentials_task.raise_on_failure()
90+
91+
config = {
92+
"username": credentials_task.results["username"],
93+
"password": credentials_task.results["password"],
94+
"host": mysql_primary_unit_ip,
95+
}
96+
97+
# Verify that connection is possible
98+
assert is_connection_possible(config)
99+
100+
# It is necessary to inhibit update-status-hook to stop the service
101+
# since the charm will restart the service on the hook
102+
juju.model_config({"update-status-hook-interval": "60m"})
103+
104+
logging.info(f"Stopping server on unit {mysql_primary_unit}")
105+
stop_mysql_process_gracefully(juju, mysql_primary_unit)
106+
107+
# Verify that connection is gone
108+
assert not is_connection_possible(config)
109+
110+
online_units = set(mysql_units) - {mysql_primary_unit}
111+
online_units = list(online_units)
112+
random_unit = random.choice(online_units)
113+
114+
new_mysql_primary_unit = get_mysql_primary_unit(juju, MYSQL_APP_NAME, random_unit)
115+
116+
logging.info("Write to new primary")
117+
table_name = "data"
118+
table_value = generate_random_string(255)
119+
await insert_mysql_test_data(
120+
juju, MYSQL_APP_NAME, new_mysql_primary_unit, table_name, table_value
121+
)
122+
123+
logging.info(f"Starting server on unit {mysql_primary_unit}")
124+
start_mysql_process_gracefully(juju, mysql_primary_unit)
125+
126+
# Restore standard interval
127+
juju.model_config({"update-status-hook-interval": "5m"})
128+
129+
# Verify that connection is possible
130+
assert is_connection_possible(config, retry_if_not_possible=True)
131+
132+
await verify_mysql_test_data(juju, MYSQL_APP_NAME, table_name, table_value)
133+
await remove_mysql_test_data(juju, MYSQL_APP_NAME, table_name)
134+
135+
136+
async def insert_mysql_test_data(
137+
juju: Juju,
138+
app_name: str,
139+
unit_name: str,
140+
table_name: str,
141+
table_value: str,
142+
) -> None:
143+
"""Insert data into the MySQL database.
144+
145+
Args:
146+
juju: The Juju model.
147+
app_name: The application name.
148+
unit_name: The application unit to insert data into.
149+
table_name: The database table name.
150+
table_value: The value to insert.
151+
"""
152+
credentials_task = juju.run(
153+
unit=unit_name,
154+
action="get-password",
155+
params={"username": SERVER_CONFIG_USERNAME},
156+
)
157+
credentials_task.raise_on_failure()
158+
159+
insert_queries = [
160+
f"CREATE DATABASE IF NOT EXISTS `{TEST_DATABASE_NAME}`",
161+
f"CREATE TABLE IF NOT EXISTS `{TEST_DATABASE_NAME}`.`{table_name}` (id VARCHAR(255), PRIMARY KEY (id))",
162+
f"INSERT INTO `{TEST_DATABASE_NAME}`.`{table_name}` (id) VALUES ('{table_value}')",
163+
]
164+
165+
await execute_queries_on_unit(
166+
get_unit_ip(juju, app_name, unit_name),
167+
credentials_task.results["username"],
168+
credentials_task.results["password"],
169+
insert_queries,
170+
commit=True,
171+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
summary: test_self_healing_stop_primary_new.py
2+
environment:
3+
TEST_MODULE: high_availability/test_self_healing_stop_primary_new.py
4+
execute: |
5+
tox run -e integration -- "tests/integration/$TEST_MODULE" --model testing --alluredir="$SPREAD_TASK/allure-results"
6+
artifacts:
7+
- allure-results

0 commit comments

Comments
 (0)