Skip to content

draft: fix machine refresh - #447

Draft
paulomach wants to merge 3 commits into
8.4/edgefrom
fix/vm-refresh-snap
Draft

draft: fix machine refresh#447
paulomach wants to merge 3 commits into
8.4/edgefrom
fix/vm-refresh-snap

Conversation

@paulomach

Copy link
Copy Markdown
Contributor

test_upgrade.py fails consistently on 8.4/edge: the first unit to refresh
ends up in hook failed: "upgrade-charm" and retries forever, so the snap is
never refreshed and the test times out before resume-refresh can run.

charm_refresh.Machines() calls refresh_snap() from its own __init__,
which runs on the right-hand side of self._refresh = charm_refresh.Machines(...)
in MySQLOperatorCharm.__init__. refresh_snap() therefore executes before
self._refresh exists, and both of its statements raise AttributeError:

  • set_unit_status() reads self._refresh -> "'MySQLOperatorCharm' object has
    no attribute '_refresh'"
  • self._charm.install_and_configure_mysql_dependencies(...) -> that method is
    a MySQL staticmethod in mysql_vm_helpers, not a charm attribute

Take the refresh instance as an explicit keyword argument in
set_unit_status() (mirroring the PostgreSQL charm) so callers running during
construction can pass it, and fall back to getattr for any other early call.
Call the installer on MySQL.

Also reconcile the unit after the snap is installed. _on_update_status() and
_on_config_changed() both return early while a refresh is in progress, so
nothing else starts mysqld, rejoins the cluster, or clears the maintenance
status for the refreshing unit — and the refresh only completes once the unit
reports healthy. _post_snap_refresh() closes that loop.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

`test_upgrade.py` fails consistently on 8.4/edge: the first unit to refresh
ends up in `hook failed: "upgrade-charm"` and retries forever, so the snap is
never refreshed and the test times out before `resume-refresh` can run.

`charm_refresh.Machines()` calls `refresh_snap()` from its own `__init__`,
which runs on the right-hand side of `self._refresh = charm_refresh.Machines(...)`
in `MySQLOperatorCharm.__init__`. `refresh_snap()` therefore executes before
`self._refresh` exists, and both of its statements raise `AttributeError`:

- `set_unit_status()` reads `self._refresh` -> "'MySQLOperatorCharm' object has
  no attribute '_refresh'"
- `self._charm.install_and_configure_mysql_dependencies(...)` -> that method is
  a `MySQL` staticmethod in `mysql_vm_helpers`, not a charm attribute

Take the refresh instance as an explicit keyword argument in
`set_unit_status()` (mirroring the PostgreSQL charm) so callers running during
construction can pass it, and fall back to `getattr` for any other early call.
Call the installer on `MySQL`.

Also reconcile the unit after the snap is installed. `_on_update_status()` and
`_on_config_changed()` both return early while a refresh is in progress, so
nothing else starts mysqld, rejoins the cluster, or clears the maintenance
status for the refreshing unit — and the refresh only completes once the unit
reports healthy. `_post_snap_refresh()` closes that loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@paulomach paulomach added the bug Something isn't working as expected label Jul 31, 2026
paulomach and others added 2 commits July 31, 2026 16:38
`test_refresh_from_edge` never ran `force-refresh-start` or `resume-refresh`,
so every refresh stalled until the final 20-minute `all_active` wait failed.

`jubilant.all_blocked` requires the app *and every unit* to be blocked, but
charm_refresh only sets the app status while a refresh is in progress or
incompatible — the units stay active. The wait therefore always timed out, the
`except TimeoutError` branch logged the misleading "Refresh completed without
snap refresh (Python code only)", and both the incompatibility workaround and
`resume-refresh` were skipped. Since `pause-after-unit-refresh` defaults to
`first`, `resume-refresh` is mandatory, so the refresh could never complete.

Wait on the app status alone and drop the `try`/`except`, so a refresh that
does not start is reported as a failure of the wait it actually failed.

This is what the last run tripped over: a locally built charm is not tagged,
so its charm version is a dev version (`8.4/1.80.0.post2.dev0+a18ec2d0c`) and
`charm_refresh._is_charm_version_compatible` rejects any unreleased version.
`force-refresh-start` with `check-compatibility=false` is the intended escape
hatch and is now reachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CharmSpecificMachines.refresh_snap()` installed the new snap revision but
never called `charm_refresh.Machines.update_snap_revision()`. The library
requires that call before `next_unit_allowed_to_refresh` may be set, so the
unconditional assignment in `MySQLOperatorCharm.__init__` raised:

    Exception: Must call `update_snap_revision()` before setting
    `next_unit_allowed_to_refresh = True`

The refresh itself succeeded (unit rejoined the cluster as SECONDARY), but the
hook crashed at the end of it, failing the `force-refresh-start` action and
therefore the upgrade integration test.

While here, gate `next_unit_allowed_to_refresh` on the unit actually being
healthy again, matching the PostgreSQL charm: post-snap-refresh work (start
mysqld, rejoin the cluster, publish member state) moves out of `refresh_snap`
into `MySQLOperatorCharm._post_snap_refresh`, driven from `__init__` because
both `_on_update_status` and `_on_config_changed` bail out while a refresh is
in progress. The next unit is only unblocked on the success path, so a broken
refresh no longer rolls forward across the whole application.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant