Skip to content

Commit def06a8

Browse files
committed
Switch to generator
1 parent e51d76f commit def06a8

File tree

4 files changed

+99
-100
lines changed

4 files changed

+99
-100
lines changed

Diff for: src/charm.py

+15-28
Original file line numberDiff line numberDiff line change
@@ -385,35 +385,22 @@ def primary_endpoint(self) -> str | None:
385385
logger.debug("primary endpoint early exit: Peer relation not joined yet.")
386386
return None
387387
try:
388-
alternative_endpoints = (
389-
None if not self.is_cluster_initialised else list(self._peer_members_ips)
390-
)
391-
for attempt in Retrying(stop=stop_after_delay(5), wait=wait_fixed(3)):
392-
with attempt:
393-
primary = self._patroni.get_primary(
394-
alternative_endpoints=alternative_endpoints
388+
primary = self._patroni.get_primary()
389+
if primary is None and (standby_leader := self._patroni.get_standby_leader()):
390+
primary = standby_leader
391+
primary_endpoint = self._patroni.get_member_ip(primary)
392+
# Force a retry if there is no primary or the member that was
393+
# returned is not in the list of the current cluster members
394+
# (like when the cluster was not updated yet after a failed switchover).
395+
if not primary_endpoint or primary_endpoint not in self._units_ips:
396+
# TODO figure out why peer data is not available
397+
if primary_endpoint and len(self._units_ips) == 1 and len(self._peers.units) > 1:
398+
logger.warning(
399+
"Possibly incoplete peer data: Will not map primary IP to unit IP"
395400
)
396-
if primary is None and (standby_leader := self._patroni.get_standby_leader()):
397-
primary = standby_leader
398-
primary_endpoint = self._patroni.get_member_ip(primary)
399-
# Force a retry if there is no primary or the member that was
400-
# returned is not in the list of the current cluster members
401-
# (like when the cluster was not updated yet after a failed switchover).
402-
if not primary_endpoint or primary_endpoint not in self._units_ips:
403-
# TODO figure out why peer data is not available
404-
if (
405-
primary_endpoint
406-
and len(self._units_ips) == 1
407-
and len(self._peers.units) > 1
408-
):
409-
logger.warning(
410-
"Possibly incoplete peer data: Will not map primary IP to unit IP"
411-
)
412-
return primary_endpoint
413-
logger.debug(
414-
"primary endpoint early exit: Primary IP not in cached peer list."
415-
)
416-
primary_endpoint = None
401+
return primary_endpoint
402+
logger.debug("primary endpoint early exit: Primary IP not in cached peer list.")
403+
primary_endpoint = None
417404
except RetryError:
418405
return None
419406
else:

Diff for: src/cluster.py

+39-36
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from ops import BlockedStatus
2222
from pysyncobj.utility import TcpUtility, UtilityException
2323
from tenacity import (
24-
AttemptManager,
2524
RetryError,
2625
Retrying,
2726
retry,
@@ -233,11 +232,12 @@ def get_member_ip(self, member_name: str) -> str:
233232
IP address of the cluster member.
234233
"""
235234
# Request info from cluster endpoint (which returns all members of the cluster).
236-
for attempt in Retrying(stop=stop_after_attempt(2 * len(self.peers_ips) + 1)):
235+
attempts = 2 * len(self.peers_ips) + 2
236+
urls = self._get_alternative_patroni_url()
237+
for attempt in Retrying(stop=stop_after_attempt(attempts)):
237238
with attempt:
238-
url = self._get_alternative_patroni_url(attempt)
239239
cluster_status = requests.get(
240-
f"{url}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
240+
f"{next(urls)}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
241241
verify=self.verify,
242242
timeout=API_REQUEST_TIMEOUT,
243243
auth=self._patroni_auth,
@@ -257,11 +257,12 @@ def get_member_status(self, member_name: str) -> str:
257257
couldn't be retrieved yet.
258258
"""
259259
# Request info from cluster endpoint (which returns all members of the cluster).
260-
for attempt in Retrying(stop=stop_after_attempt(2 * len(self.peers_ips) + 1)):
260+
attempts = 2 * len(self.peers_ips) + 2
261+
urls = self._get_alternative_patroni_url()
262+
for attempt in Retrying(stop=stop_after_attempt(attempts)):
261263
with attempt:
262-
url = self._get_alternative_patroni_url(attempt)
263264
cluster_status = requests.get(
264-
f"{url}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
265+
f"{next(urls)}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
265266
verify=self.verify,
266267
timeout=API_REQUEST_TIMEOUT,
267268
auth=self._patroni_auth,
@@ -284,11 +285,15 @@ def get_primary(
284285
primary pod or unit name.
285286
"""
286287
# Request info from cluster endpoint (which returns all members of the cluster).
287-
for attempt in Retrying(stop=stop_after_attempt(2 * len(self.peers_ips) + 1)):
288+
if alternative_endpoints:
289+
attempts = 2 * len(alternative_endpoints) + 2
290+
else:
291+
attempts = 2 * len(self.peers_ips) + 2
292+
urls = self._get_alternative_patroni_url(alternative_endpoints)
293+
for attempt in Retrying(stop=stop_after_attempt(attempts)):
288294
with attempt:
289-
url = self._get_alternative_patroni_url(attempt, alternative_endpoints)
290295
cluster_status = requests.get(
291-
f"{url}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
296+
f"{next(urls)}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
292297
verify=self.verify,
293298
timeout=API_REQUEST_TIMEOUT,
294299
auth=self._patroni_auth,
@@ -314,11 +319,12 @@ def get_standby_leader(
314319
standby leader pod or unit name.
315320
"""
316321
# Request info from cluster endpoint (which returns all members of the cluster).
317-
for attempt in Retrying(stop=stop_after_attempt(2 * len(self.peers_ips) + 1)):
322+
attempts = 2 * len(self.peers_ips) + 2
323+
urls = self._get_alternative_patroni_url()
324+
for attempt in Retrying(stop=stop_after_attempt(attempts)):
318325
with attempt:
319-
url = self._get_alternative_patroni_url(attempt)
320326
cluster_status = requests.get(
321-
f"{url}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
327+
f"{next(urls)}/{PATRONI_CLUSTER_STATUS_ENDPOINT}",
322328
verify=self.verify,
323329
timeout=API_REQUEST_TIMEOUT,
324330
auth=self._patroni_auth,
@@ -338,9 +344,13 @@ def get_sync_standby_names(self) -> list[str]:
338344
"""Get the list of sync standby unit names."""
339345
sync_standbys = []
340346
# Request info from cluster endpoint (which returns all members of the cluster).
341-
for attempt in Retrying(stop=stop_after_attempt(2 * len(self.peers_ips) + 1)):
347+
attempts = 2 * len(self.peers_ips) + 2
348+
for attempt, url in zip(
349+
Retrying(stop=stop_after_attempt(attempts)),
350+
self._get_alternative_patroni_url(),
351+
strict=True,
352+
):
342353
with attempt:
343-
url = self._get_alternative_patroni_url(attempt)
344354
r = requests.get(
345355
f"{url}/cluster",
346356
verify=self.verify,
@@ -352,33 +362,26 @@ def get_sync_standby_names(self) -> list[str]:
352362
sync_standbys.append("/".join(member["name"].rsplit("-", 1)))
353363
return sync_standbys
354364

355-
def _get_alternative_patroni_url(
356-
self, attempt: AttemptManager, alternative_endpoints: list[str] | None = None
357-
) -> str:
365+
def _get_alternative_patroni_url(self, alternative_endpoints: list[str] | None = None) -> str:
358366
"""Get an alternative REST API URL from another member each time.
359367
360368
When the Patroni process is not running in the current unit it's needed
361369
to use a URL from another cluster member REST API to do some operations.
362370
"""
363-
if alternative_endpoints is not None:
364-
return self._patroni_url.replace(
365-
self.unit_ip, alternative_endpoints[attempt.retry_state.attempt_number - 1]
366-
)
367-
attempt_number = attempt.retry_state.attempt_number
368-
if attempt_number > 1:
369-
url = self._patroni_url
370-
# Build the URL using http and later using https for each peer.
371-
if (attempt_number - 1) <= len(self.peers_ips):
372-
url = url.replace("https://", "http://")
373-
unit_number = attempt_number - 2
374-
else:
375-
url = url.replace("http://", "https://")
376-
unit_number = attempt_number - 2 - len(self.peers_ips)
377-
other_unit_ip = list(self.peers_ips)[unit_number]
378-
url = url.replace(self.unit_ip, other_unit_ip)
371+
if not alternative_endpoints:
372+
alternative_endpoints = list(self.peers_ips)
373+
urls = [self._patroni_url]
374+
urls += [
375+
self._patroni_url.replace(self.unit_ip, endpoint) for endpoint in alternative_endpoints
376+
]
377+
if self.tls_enabled:
378+
default_schema = "https"
379+
new_schema = "http"
379380
else:
380-
url = self._patroni_url
381-
return url
381+
default_schema = "http"
382+
new_schema = "https"
383+
urls += [url.replace(default_schema, new_schema) for url in urls]
384+
yield from urls
382385

383386
def are_all_members_ready(self) -> bool:
384387
"""Check if all members are correctly running Patroni and PostgreSQL.

Diff for: tests/unit/test_charm.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,6 @@ def test_patroni_scrape_config_tls(harness):
177177

178178
def test_primary_endpoint(harness):
179179
with (
180-
patch("charm.stop_after_delay", new_callable=PropertyMock) as _stop_after_delay,
181-
patch("charm.wait_fixed", new_callable=PropertyMock) as _wait_fixed,
182180
patch(
183181
"charm.PostgresqlOperatorCharm._units_ips",
184182
new_callable=PropertyMock,
@@ -190,12 +188,8 @@ def test_primary_endpoint(harness):
190188
_patroni.return_value.get_primary.return_value = sentinel.primary
191189
assert harness.charm.primary_endpoint == "1.1.1.1"
192190

193-
# Check needed to ensure a fast charm deployment.
194-
_stop_after_delay.assert_called_once_with(5)
195-
_wait_fixed.assert_called_once_with(3)
196-
197191
_patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary)
198-
_patroni.return_value.get_primary.assert_called_once_with(alternative_endpoints=None)
192+
_patroni.return_value.get_primary.assert_called_once_with()
199193

200194

201195
def test_primary_endpoint_no_peers(harness):

Diff for: tests/unit/test_cluster.py

+44-29
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
from ops.testing import Harness
1212
from pysyncobj.utility import UtilityException
1313
from tenacity import (
14-
AttemptManager,
15-
RetryCallState,
1614
RetryError,
1715
Retrying,
1816
stop_after_delay,
@@ -95,49 +93,59 @@ def patroni(harness, peers_ips):
9593

9694

9795
def test_get_alternative_patroni_url(peers_ips, patroni):
98-
# Mock tenacity attempt.
99-
retry = Retrying()
100-
retry_state = RetryCallState(retry, None, None, None)
101-
attempt = AttemptManager(retry_state)
102-
10396
# Test the first URL that is returned (it should have the current unit IP).
104-
url = patroni._get_alternative_patroni_url(attempt)
97+
url = next(patroni._get_alternative_patroni_url())
10598
assert url == f"http://{patroni.unit_ip}:8008"
10699

107-
# Test returning the other servers URLs.
108-
for attempt_number in range(attempt.retry_state.attempt_number + 1, len(peers_ips) + 2):
109-
attempt.retry_state.attempt_number = attempt_number
110-
url = patroni._get_alternative_patroni_url(attempt)
111-
assert url.split("http://")[1].split(":8008")[0] in peers_ips
100+
other_urls = list(patroni._get_alternative_patroni_url())
101+
for ip in list(peers_ips):
102+
assert f"http://{ip}:8008" in other_urls
103+
assert f"https://{ip}:8008" in other_urls
104+
105+
# Test alternatives
106+
assert [
107+
f"http://{patroni.unit_ip}:8008",
108+
"http://a:8008",
109+
"http://b:8008",
110+
"http://c:8008",
111+
f"https://{patroni.unit_ip}:8008",
112+
"https://a:8008",
113+
"https://b:8008",
114+
"https://c:8008",
115+
] == list(patroni._get_alternative_patroni_url(["a", "b", "c"]))
112116

113117

114118
def test_get_member_ip(peers_ips, patroni):
115119
with (
116120
patch("requests.get", side_effect=mocked_requests_get) as _get,
117-
patch("charm.Patroni._get_alternative_patroni_url") as _get_alternative_patroni_url,
121+
patch("charm.Patroni._patroni_url", new_callable=PropertyMock) as _patroni_url,
118122
):
119123
# Test error on trying to get the member IP.
120-
_get_alternative_patroni_url.side_effect = "http://server2"
124+
_patroni_url.return_value = "http://server2"
125+
patroni.unit_ip = "server2"
126+
patroni.peers_ips = []
121127
with pytest.raises(RetryError):
122128
patroni.get_member_ip(patroni.member_name)
123129
assert False
124130

125131
# Test using an alternative Patroni URL.
126-
_get_alternative_patroni_url.side_effect = [
127-
"http://server3",
128-
"http://server2",
129-
"http://server1",
132+
_patroni_url.return_value = "http://server3"
133+
patroni.unit_ip = "server3"
134+
patroni.peers_ips = [
135+
"server2",
136+
"server1",
130137
]
131138
ip = patroni.get_member_ip(patroni.member_name)
132139
assert ip == "1.1.1.1"
133140

134141
# Test using the current Patroni URL.
135-
_get_alternative_patroni_url.side_effect = ["http://server1"]
142+
_patroni_url.return_value = "http://server1"
143+
patroni.unit_ip = "server1"
144+
patroni.peers_ips = []
136145
ip = patroni.get_member_ip(patroni.member_name)
137146
assert ip == "1.1.1.1"
138147

139148
# Test when not having that specific member in the cluster.
140-
_get_alternative_patroni_url.side_effect = ["http://server1"]
141149
ip = patroni.get_member_ip("other-member-name")
142150
assert ip is None
143151

@@ -184,30 +192,36 @@ def test_get_postgresql_version(peers_ips, patroni):
184192
def test_get_primary(peers_ips, patroni):
185193
with (
186194
patch("requests.get", side_effect=mocked_requests_get) as _get,
187-
patch("charm.Patroni._get_alternative_patroni_url") as _get_alternative_patroni_url,
195+
patch("charm.Patroni._patroni_url", new_callable=PropertyMock) as _patroni_url,
188196
):
189197
# Test error on trying to get the member IP.
190-
_get_alternative_patroni_url.side_effect = "http://server2"
198+
_patroni_url.return_value = "http://server2"
199+
patroni.unit_ip = "server2"
200+
patroni.peers_ips = []
191201
with pytest.raises(RetryError):
192202
patroni.get_primary(patroni.member_name)
193203
assert False
194204

195205
# Test using an alternative Patroni URL.
196-
_get_alternative_patroni_url.side_effect = [
197-
"http://server3",
198-
"http://server2",
199-
"http://server1",
206+
_patroni_url.return_value = "http://server3"
207+
patroni.unit_ip = "server3"
208+
patroni.peers_ips = [
209+
"server2",
210+
"server1",
200211
]
201212
primary = patroni.get_primary()
202213
assert primary == "postgresql-0"
203214

204215
# Test using the current Patroni URL.
205-
_get_alternative_patroni_url.side_effect = ["http://server1"]
216+
_patroni_url.return_value = "http://server1"
217+
patroni.unit_ip = "server1"
218+
patroni.peers_ips = []
206219
primary = patroni.get_primary()
207220
assert primary == "postgresql-0"
208221

209222
# Test requesting the primary in the unit name pattern.
210-
_get_alternative_patroni_url.side_effect = ["http://server1"]
223+
patroni.unit_ip = "server1"
224+
patroni.peers_ips = []
211225
primary = patroni.get_primary(unit_name_pattern=True)
212226
assert primary == "postgresql/0"
213227

@@ -235,6 +249,7 @@ def test_is_replication_healthy(peers_ips, patroni):
235249
with (
236250
patch("requests.get") as _get,
237251
patch("charm.Patroni.get_primary"),
252+
patch("charm.Patroni.get_member_ip"),
238253
patch("cluster.stop_after_delay", return_value=stop_after_delay(0)),
239254
):
240255
# Test when replication is healthy.

0 commit comments

Comments
 (0)