Skip to content

Commit e0f6bc7

Browse files
mr-callengau
andauthored
fix: retry 'snap watch' (#899)
Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com> Co-authored-by: Alex Lowe <alex.lowe@canonical.com>
1 parent c1abc9e commit e0f6bc7

File tree

6 files changed

+97
-3
lines changed

6 files changed

+97
-3
lines changed

craft_providers/base.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,12 +612,23 @@ def _disable_and_wait_for_snap_refresh(self, executor: Executor) -> None:
612612

613613
# a refresh may have started before the hold was set
614614
logger.debug("Waiting for pending snap refreshes to complete.")
615-
try:
615+
616+
def snap_watch(timeout: float) -> None:
616617
executor.execute_run(
617618
["snap", "watch", "--last=auto-refresh?"],
618619
capture_output=True,
619620
check=True,
620-
timeout=self._timeout_simple,
621+
timeout=timeout,
622+
)
623+
624+
try:
625+
# There is a small time window after restarting the snapd service where snapd
626+
# claims it's ready but actually isn't (SNAPDENG-36387). The workaround is to
627+
# retry.
628+
retry.retry_until_timeout(
629+
self._timeout_complex or TIMEOUT_COMPLEX,
630+
self._retry_wait,
631+
snap_watch,
621632
)
622633
except subprocess.CalledProcessError as error:
623634
raise BaseConfigurationError(

docs/reference/changelog.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Changelog
44
See the `Releases page`_ on GitHub for a complete list of commits that are
55
included in each version.
66

7-
3.3.0 (Unreleased)
7+
3.3.0 (2026-02-09)
88
------------------
99

1010
Breaking changes:
@@ -14,6 +14,7 @@ Breaking changes:
1414
Bug fixes:
1515

1616
- Do not fail if the build host is an unknown Ubuntu release.
17+
- Fix a bug where snapd would fail while waiting for snap refreshes to complete.
1718

1819
3.2.0 (2025-11-13)
1920
------------------

tests/unit/bases/test_almalinux.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,8 @@ def test_setup_snapd_failures(fake_process, fake_executor):
838838
@pytest.mark.parametrize("fail_index", list(range(0, 8)))
839839
def test_post_setup_snapd_failures(fake_process, fake_executor, fail_index):
840840
base_config = almalinux.AlmaLinuxBase(alias=almalinux.AlmaLinuxBaseAlias.NINE)
841+
base_config._retry_wait = 0.01
842+
base_config._timeout_complex = 0.01
841843

842844
return_codes = [0] * 8
843845
return_codes[fail_index] = 1
@@ -1607,6 +1609,8 @@ def test_disable_and_wait_for_snap_refresh_hold_error(fake_process, fake_executo
16071609
def test_disable_and_wait_for_snap_refresh_wait_error(fake_process, fake_executor):
16081610
"""Raise BaseConfigurationError when the `snap watch` command fails."""
16091611
base_config = almalinux.AlmaLinuxBase(alias=almalinux.AlmaLinuxBaseAlias.NINE)
1612+
base_config._retry_wait = 0.01
1613+
base_config._timeout_complex = 0.01
16101614
fake_process.register_subprocess([*DEFAULT_FAKE_CMD, "snap", "refresh", "--hold"])
16111615
fake_process.register_subprocess(
16121616
[*DEFAULT_FAKE_CMD, "snap", "watch", "--last=auto-refresh?"],

tests/unit/bases/test_centos_7.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,8 @@ def test_setup_snapd_failures(fake_process, fake_executor):
776776
@pytest.mark.parametrize("fail_index", list(range(0, 8)))
777777
def test_post_setup_snapd_failures(fake_process, fake_executor, fail_index):
778778
base_config = centos.CentOSBase(alias=centos.CentOSBaseAlias.SEVEN)
779+
base_config._retry_wait = 0.01
780+
base_config._timeout_complex = 0.01
779781
return_codes = [0] * 8
780782
return_codes[fail_index] = 1
781783
fake_process.register_subprocess(
@@ -1543,6 +1545,8 @@ def test_disable_and_wait_for_snap_refresh_hold_error(fake_process, fake_executo
15431545
def test_disable_and_wait_for_snap_refresh_wait_error(fake_process, fake_executor):
15441546
"""Raise BaseConfigurationError when the `snap watch` command fails."""
15451547
base_config = centos.CentOSBase(alias=centos.CentOSBaseAlias.SEVEN)
1548+
base_config._retry_wait = 0.01
1549+
base_config._timeout_complex = 0.01
15461550
fake_process.register_subprocess([*DEFAULT_FAKE_CMD, "snap", "refresh", "--hold"])
15471551
fake_process.register_subprocess(
15481552
[*DEFAULT_FAKE_CMD, "snap", "watch", "--last=auto-refresh?"],

tests/unit/bases/test_ubuntu_buildd.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,8 @@ def test_setup_snapd_failures(fake_process, fake_executor):
969969
@pytest.mark.parametrize("fail_index", list(range(0, 8)))
970970
def test_post_setup_snapd_failures(fake_process, fake_executor, fail_index):
971971
base_config = ubuntu.BuilddBase(alias=ubuntu.BuilddBaseAlias.JAMMY)
972+
base_config._retry_wait = 0.01
973+
base_config._timeout_complex = 0.01
972974
return_codes = [0] * 8
973975
return_codes[fail_index] = 1
974976

@@ -1721,6 +1723,8 @@ def test_disable_and_wait_for_snap_refresh_hold_error(fake_process, fake_executo
17211723
def test_disable_and_wait_for_snap_refresh_wait_error(fake_process, fake_executor):
17221724
"""Raise BaseConfigurationError when the `snap watch` command fails."""
17231725
base_config = ubuntu.BuilddBase(alias=ubuntu.BuilddBaseAlias.JAMMY)
1726+
base_config._retry_wait = 0.01
1727+
base_config._timeout_complex = 0.01
17241728
fake_process.register_subprocess([*DEFAULT_FAKE_CMD, "snap", "refresh", "--hold"])
17251729
fake_process.register_subprocess(
17261730
[*DEFAULT_FAKE_CMD, "snap", "watch", "--last=auto-refresh?"],

tests/unit/test_base.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,73 @@ def test_set_hostname(fake_base):
327327
bad_name = "bad_123-ABC%-"
328328
fake_base._set_hostname(bad_name)
329329
assert fake_base._hostname == "bad123-ABC"
330+
331+
332+
def test_snap_refresh(fake_process, fake_executor, fake_base):
333+
"""Disable and wait for snap refreshes."""
334+
fake_process.register_subprocess(
335+
[*DEFAULT_FAKE_CMD, "snap", "refresh", "--hold"],
336+
returncode=0,
337+
)
338+
fake_process.register_subprocess(
339+
[*DEFAULT_FAKE_CMD, "snap", "watch", "--last=auto-refresh?"],
340+
returncode=0,
341+
)
342+
343+
fake_base._disable_and_wait_for_snap_refresh(executor=fake_executor)
344+
345+
346+
def test_snap_refresh_hold_error(fake_process, fake_executor, fake_base):
347+
"""Error on failure to hold refreshes."""
348+
fake_process.register_subprocess(
349+
[*DEFAULT_FAKE_CMD, "snap", "refresh", "--hold"],
350+
stderr="test error",
351+
returncode=1,
352+
)
353+
fake_process.register_subprocess(
354+
[*DEFAULT_FAKE_CMD, "snap", "watch", "--last=auto-refresh?"],
355+
returncode=0,
356+
)
357+
358+
with pytest.raises(BaseConfigurationError) as err:
359+
fake_base._disable_and_wait_for_snap_refresh(executor=fake_executor)
360+
361+
assert err.value.brief == "Failed to hold snap refreshes."
362+
assert err.value.details
363+
assert "test error" in err.value.details
364+
365+
366+
def test_snap_refresh_watch_error(fake_process, fake_executor, fake_base):
367+
"""Error on failure of 'snap watch'."""
368+
stderr = "error: daemon is stopping to wait for socket activation"
369+
fake_base._timeout_complex = 0.01
370+
fake_process.register_subprocess(
371+
[*DEFAULT_FAKE_CMD, "snap", "refresh", "--hold"],
372+
returncode=0,
373+
)
374+
fake_process.register_subprocess(
375+
[*DEFAULT_FAKE_CMD, "snap", "watch", "--last=auto-refresh?"],
376+
stderr=stderr,
377+
returncode=1,
378+
)
379+
380+
with pytest.raises(BaseConfigurationError) as err:
381+
fake_base._disable_and_wait_for_snap_refresh(executor=fake_executor)
382+
383+
assert err.value.brief == "Failed to wait for snap refreshes to complete."
384+
assert err.value.details
385+
assert stderr in err.value.details
386+
387+
388+
def test_snap_refresh_watch_retry(fake_process, fake_executor, fake_base):
389+
"""Retry when 'snap watch' fails."""
390+
fake_process.register_subprocess(
391+
[*DEFAULT_FAKE_CMD, "snap", "refresh", "--hold"],
392+
returncode=0,
393+
)
394+
# fail twice then succeed
395+
snap_watch = [*DEFAULT_FAKE_CMD, "snap", "watch", "--last=auto-refresh?"]
396+
for returncode in (1, 1, 0):
397+
fake_process.register_subprocess(snap_watch, returncode=returncode)
398+
399+
fake_base._disable_and_wait_for_snap_refresh(executor=fake_executor)

0 commit comments

Comments
 (0)