Skip to content

Commit 165d04c

Browse files
Speed up bootstrap (#413)
* Speed up bootstrap * Add unit tests * Sync poetry.lock from main * Add install and active log messages and set primary status message earlier * Enable and fix existing unit test Signed-off-by: Marcelo Henrique Neppel <[email protected]>
1 parent dbd46b5 commit 165d04c

File tree

6 files changed

+107
-26
lines changed

6 files changed

+107
-26
lines changed

src/charm.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import platform
1111
import subprocess
1212
import sys
13+
from datetime import datetime
1314
from pathlib import Path
1415
from typing import Dict, List, Literal, Optional, Set, get_args
1516

@@ -352,7 +353,7 @@ def primary_endpoint(self) -> Optional[str]:
352353
logger.debug("primary endpoint early exit: Peer relation not joined yet.")
353354
return None
354355
try:
355-
for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(3)):
356+
for attempt in Retrying(stop=stop_after_delay(5), wait=wait_fixed(3)):
356357
with attempt:
357358
primary = self._patroni.get_primary()
358359
if primary is None and (standby_leader := self._patroni.get_standby_leader()):
@@ -855,6 +856,7 @@ def _on_cluster_topology_change(self, _):
855856

856857
def _on_install(self, event: InstallEvent) -> None:
857858
"""Install prerequisites for the application."""
859+
logger.debug("Install start time: %s", datetime.now())
858860
if not self._is_storage_attached():
859861
self._reboot_on_detached_storage(event)
860862
return
@@ -1162,7 +1164,8 @@ def _start_primary(self, event: StartEvent) -> None:
11621164
# was fully initialised.
11631165
self.enable_disable_extensions()
11641166

1165-
self.unit.status = ActiveStatus()
1167+
logger.debug("Active workload time: %s", datetime.now())
1168+
self._set_primary_status_message()
11661169

11671170
def _start_replica(self, event) -> None:
11681171
"""Configure the replica if the cluster was already initialised."""

src/cluster.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ def are_all_members_ready(self) -> bool:
362362

363363
def get_patroni_health(self) -> Dict[str, str]:
364364
"""Gets, retires and parses the Patroni health endpoint."""
365-
for attempt in Retrying(stop=stop_after_delay(90), wait=wait_fixed(3)):
365+
for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(7)):
366366
with attempt:
367367
r = requests.get(
368368
f"{self._patroni_url}/health",

src/upgrade.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,10 @@ def _on_upgrade_charm_check_legacy(self) -> None:
115115

116116
peers_state = list(filter(lambda state: state != "", self.unit_states))
117117

118-
if len(peers_state) == len(self.peer_relation.units) and (
119-
set(peers_state) == {"ready"} or len(peers_state) == 0
118+
if (
119+
len(peers_state) == len(self.peer_relation.units)
120+
and (set(peers_state) == {"ready"} or len(peers_state) == 0)
121+
and self.charm.is_cluster_initialised
120122
):
121123
if self.charm._patroni.member_started:
122124
# All peers have set the state to ready

tests/unit/test_charm.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import platform
66
import subprocess
7+
from unittest import TestCase
78
from unittest.mock import MagicMock, Mock, PropertyMock, call, mock_open, patch, sentinel
89

910
import pytest
@@ -37,6 +38,9 @@
3738

3839
CREATE_CLUSTER_CONF_PATH = "/etc/postgresql-common/createcluster.d/pgcharm.conf"
3940

41+
# used for assert functions
42+
tc = TestCase()
43+
4044

4145
@pytest.fixture(autouse=True)
4246
def harness():
@@ -165,7 +169,9 @@ def test_patroni_scrape_config_tls(harness):
165169

166170

167171
def test_primary_endpoint(harness):
168-
with patch(
172+
with patch("charm.stop_after_delay", new_callable=PropertyMock) as _stop_after_delay, patch(
173+
"charm.wait_fixed", new_callable=PropertyMock
174+
) as _wait_fixed, patch(
169175
"charm.PostgresqlOperatorCharm._units_ips",
170176
new_callable=PropertyMock,
171177
return_value={"1.1.1.1", "1.1.1.2"},
@@ -174,6 +180,10 @@ def test_primary_endpoint(harness):
174180
_patroni.return_value.get_primary.return_value = sentinel.primary
175181
assert harness.charm.primary_endpoint == "1.1.1.1"
176182

183+
# Check needed to ensure a fast charm deployment.
184+
_stop_after_delay.assert_called_once_with(5)
185+
_wait_fixed.assert_called_once_with(3)
186+
177187
_patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary)
178188
_patroni.return_value.get_primary.assert_called_once_with()
179189

@@ -547,6 +557,9 @@ def test_enable_disable_extensions(harness, caplog):
547557
@patch_network_get(private_address="1.1.1.1")
548558
def test_on_start(harness):
549559
with (
560+
patch(
561+
"charm.PostgresqlOperatorCharm._set_primary_status_message"
562+
) as _set_primary_status_message,
550563
patch(
551564
"charm.PostgresqlOperatorCharm.enable_disable_extensions"
552565
) as _enable_disable_extensions,
@@ -622,7 +635,7 @@ def test_on_start(harness):
622635
assert _postgresql.create_user.call_count == 4 # Considering the previous failed call.
623636
_oversee_users.assert_called_once()
624637
_enable_disable_extensions.assert_called_once()
625-
assert isinstance(harness.model.unit.status, ActiveStatus)
638+
_set_primary_status_message.assert_called_once()
626639

627640

628641
@patch_network_get(private_address="1.1.1.1")
@@ -2256,16 +2269,21 @@ def test_update_new_unit_status(harness):
22562269
handle_read_only_mode.assert_not_called()
22572270
assert isinstance(harness.charm.unit.status, WaitingStatus)
22582271

2259-
@patch("charm.Patroni.member_started", new_callable=PropertyMock)
2260-
@patch("charm.PostgresqlOperatorCharm.is_standby_leader", new_callable=PropertyMock)
2261-
@patch("charm.Patroni.get_primary")
2262-
def test_set_active_status(self, _get_primary, _is_standby_leader, _member_started):
2272+
2273+
def test_set_primary_status_message(harness):
2274+
with (
2275+
patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started,
2276+
patch(
2277+
"charm.PostgresqlOperatorCharm.is_standby_leader", new_callable=PropertyMock
2278+
) as _is_standby_leader,
2279+
patch("charm.Patroni.get_primary") as _get_primary,
2280+
):
22632281
for values in itertools.product(
22642282
[
22652283
RetryError(last_attempt=1),
22662284
ConnectionError,
2267-
self.charm.unit.name,
2268-
f"{self.charm.app.name}/2",
2285+
harness.charm.unit.name,
2286+
f"{harness.charm.app.name}/2",
22692287
],
22702288
[
22712289
RetryError(last_attempt=1),
@@ -2275,34 +2293,34 @@ def test_set_active_status(self, _get_primary, _is_standby_leader, _member_start
22752293
],
22762294
[True, False],
22772295
):
2278-
self.charm.unit.status = MaintenanceStatus("fake status")
2296+
harness.charm.unit.status = MaintenanceStatus("fake status")
22792297
_member_started.return_value = values[2]
22802298
if isinstance(values[0], str):
22812299
_get_primary.side_effect = None
22822300
_get_primary.return_value = values[0]
2283-
if values[0] != self.charm.unit.name and not isinstance(values[1], bool):
2301+
if values[0] != harness.charm.unit.name and not isinstance(values[1], bool):
22842302
_is_standby_leader.side_effect = values[1]
22852303
_is_standby_leader.return_value = None
2286-
self.charm._set_active_status()
2287-
self.assertIsInstance(self.charm.unit.status, MaintenanceStatus)
2304+
harness.charm._set_primary_status_message()
2305+
tc.assertIsInstance(harness.charm.unit.status, MaintenanceStatus)
22882306
else:
22892307
_is_standby_leader.side_effect = None
22902308
_is_standby_leader.return_value = values[1]
2291-
self.charm._set_active_status()
2292-
self.assertIsInstance(
2293-
self.charm.unit.status,
2309+
harness.charm._set_primary_status_message()
2310+
tc.assertIsInstance(
2311+
harness.charm.unit.status,
22942312
ActiveStatus
2295-
if values[0] == self.charm.unit.name or values[1] or values[2]
2313+
if values[0] == harness.charm.unit.name or values[1] or values[2]
22962314
else MaintenanceStatus,
22972315
)
2298-
self.assertEqual(
2299-
self.charm.unit.status.message,
2316+
tc.assertEqual(
2317+
harness.charm.unit.status.message,
23002318
"Primary"
2301-
if values[0] == self.charm.unit.name
2319+
if values[0] == harness.charm.unit.name
23022320
else ("Standby" if values[1] else ("" if values[2] else "fake status")),
23032321
)
23042322
else:
23052323
_get_primary.side_effect = values[0]
23062324
_get_primary.return_value = None
2307-
self.charm._set_active_status()
2308-
self.assertIsInstance(self.charm.unit.status, MaintenanceStatus)
2325+
harness.charm._set_primary_status_message()
2326+
tc.assertIsInstance(harness.charm.unit.status, MaintenanceStatus)

tests/unit/test_cluster.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def json(self):
4242
"http://server1/cluster": {
4343
"members": [{"name": "postgresql-0", "host": "1.1.1.1", "role": "leader", "lag": "1"}]
4444
},
45+
"http://server1/health": {"state": "running"},
4546
"http://server4/cluster": {"members": []},
4647
}
4748
if args[0] in data:
@@ -128,6 +129,28 @@ def test_get_member_ip(peers_ips, patroni):
128129
tc.assertIsNone(ip)
129130

130131

132+
def test_get_patroni_health(peers_ips, patroni):
133+
with patch("cluster.stop_after_delay", new_callable=PropertyMock) as _stop_after_delay, patch(
134+
"cluster.wait_fixed", new_callable=PropertyMock
135+
) as _wait_fixed, patch(
136+
"charm.Patroni._patroni_url", new_callable=PropertyMock
137+
) as _patroni_url, patch("requests.get", side_effect=mocked_requests_get) as _get:
138+
# Test when the Patroni API is reachable.
139+
_patroni_url.return_value = "http://server1"
140+
health = patroni.get_patroni_health()
141+
142+
# Check needed to ensure a fast charm deployment.
143+
_stop_after_delay.assert_called_once_with(60)
144+
_wait_fixed.assert_called_once_with(7)
145+
146+
tc.assertEqual(health, {"state": "running"})
147+
148+
# Test when the Patroni API is not reachable.
149+
_patroni_url.return_value = "http://server2"
150+
with tc.assertRaises(tenacity.RetryError):
151+
patroni.get_patroni_health()
152+
153+
131154
def test_get_postgresql_version(peers_ips, patroni):
132155
with patch("charm.snap.SnapClient") as _snap_client:
133156
# TODO test a real implementation

tests/unit/test_upgrade.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,41 @@ def test_log_rollback(harness):
6060
)
6161

6262

63+
@pytest.mark.parametrize(
64+
"unit_states,is_cluster_initialised,call",
65+
[
66+
(["ready"], False, False),
67+
(["ready", "ready"], True, False),
68+
(["idle"], False, False),
69+
(["idle"], True, False),
70+
(["ready"], True, True),
71+
],
72+
)
73+
def test_on_upgrade_charm_check_legacy(harness, unit_states, is_cluster_initialised, call):
74+
with (
75+
patch(
76+
"charms.data_platform_libs.v0.upgrade.DataUpgrade.state",
77+
new_callable=PropertyMock(return_value=None),
78+
) as _state,
79+
patch(
80+
"charms.data_platform_libs.v0.upgrade.DataUpgrade.unit_states",
81+
new_callable=PropertyMock(return_value=unit_states),
82+
) as _unit_states,
83+
patch(
84+
"charm.PostgresqlOperatorCharm.is_cluster_initialised",
85+
new_callable=PropertyMock(return_value=is_cluster_initialised),
86+
) as _is_cluster_initialised,
87+
patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started,
88+
patch(
89+
"upgrade.PostgreSQLUpgrade._prepare_upgrade_from_legacy"
90+
) as _prepare_upgrade_from_legacy,
91+
):
92+
with harness.hooks_disabled():
93+
harness.set_leader(True)
94+
harness.charm.upgrade._on_upgrade_charm_check_legacy()
95+
_member_started.assert_called_once() if call else _member_started.assert_not_called()
96+
97+
6398
@patch_network_get(private_address="1.1.1.1")
6499
def test_on_upgrade_granted(harness):
65100
with (

0 commit comments

Comments
 (0)