fix(garm): make teardown lifecycle safe - #333
Conversation
Use Juju planned units as the local teardown signal before the paas-charm state decorator runs. Keep inherited handlers inert and prevent update-status from refreshing ingress after teardown begins.
1510c3a to
4992b8d
Compare
| _on_config_changed = _route_reconcile | ||
| _on_secret_changed = _route_reconcile | ||
| _on_secret_storage_relation_changed = _route_reconcile | ||
| _on_secret_storage_relation_departed = _route_reconcile | ||
| _on_postgresql_database_relation_broken = _route_reconcile | ||
| _on_ingress_ready = _route_reconcile | ||
| _on_ingress_revoked = _route_reconcile | ||
| _on_pebble_ready = _route_reconcile |
There was a problem hiding this comment.
ref 1.
12-factor reference comment for override
| _on_pebble_ready = _route_reconcile | ||
|
|
||
| def _route_reconcile_with_migrations(self, event: ops.EventBase) -> None: | ||
| """Route an inherited database event through GARM's migration gate.""" |
There was a problem hiding this comment.
This is strictly for the database relations
Lifecycle funnel safety auditI audited the current funnel against the unpatched Safety criterion: when
GARM-owned hooks
Inherited
|
| Hook | Unpatched operation | Teardown risk before patch | Patched active behavior | Decision |
|---|---|---|---|---|
config-changed |
Build state, then restart(). |
State/config/relation reads occur before restart(). |
Routes through GARM normal reconcile. | Safe. |
secret-changed |
Build state, then restart(). |
Secret and relation reads occur before restart(). |
Routes through GARM normal reconcile. | Safe. |
secret-storage-relation-changed |
Build state, then restart(). |
Peer state is read before the restart gate. | Routes through GARM normal reconcile. | Safe. |
secret-storage-relation-departed |
Build state, then restart(). |
Peer relation may be departing before state construction. | Routes through GARM normal reconcile. | Safe. |
PostgreSQL database-created |
Build state, then restart(rerun_migrations=True). |
PostgreSQL state is read before the gate. | Uses a separate gated migration path. | Safe; migration flag preserved. |
PostgreSQL endpoints-changed |
Same as database-created. |
Same PostgreSQL state-read risk. | Uses the same gated migration path. | Safe; migration flag preserved. |
PostgreSQL relation-broken |
Build state, then restart(). |
The relation is no longer valid. The data-platform helper documents that fetch_relation_data() cannot be used in relation-broken; its legacy PostgreSQL fetch path assumes a relation exists (data_interfaces.py:1696-1712, 2649-2653). |
Gate runs before the decorator. | Necessary and safe. |
Ingress ready |
Build state, then restart(). |
Late ingress event can arrive after relation/state teardown starts. | Routes through GARM normal reconcile. | Safe for the base callback. |
Ingress revoked |
Build state, then restart(). |
Same late relation/state risk. | Routes through GARM normal reconcile. | Safe for the base callback. |
App pebble-ready |
Build state, then restart(). |
A queued event may arrive after teardown starts. | Routes through GARM normal reconcile. | Safe for the base callback. |
rotate-secret-key action |
Build state, reset the peer secret, report success, then restart. | It could mutate peer state during teardown. | Rejects before calling the base decorator; delegates to base while active. | Safe if teardown must reject rotation. |
update-status base observer |
Build state, retry failed migrations, then call _ingress._publish_auto_data(). |
Ingress refresh can read/write relation data even if restart() returns. |
Calls base only while active. | Necessary and safe. |
Evidence and retained operations
- Base observer registration:
paas_charm/charm.py:195-225. - Decorator behavior:
paas_charm/charm_utils.py:36-71. - Framework state reconstruction:
paas_charm/charm.py:748-784. - Config validation errors:
paas_charm/charm_state.py:126-132andpaas_charm/charm.py:466-491. - PostgreSQL relation conversion:
paas_charm/databases.py:42-76. - PostgreSQL
relation-brokenhas no known remote units:ops/charm.py:775-789. planned_units()is local goal state current at hook start:ops/model.py:459-479.- Current GARM router and overrides:
charms/garm/src/charm.py:155-257. - Current GARM restart sequence is unchanged while active; the only new behavior is the early teardown return:
charms/garm/src/charm.py:348-421. - Scenario tests patch
GarmCharm._create_charm_stateto fail and cover teardown events; the current suite passes 257 tests.
Explicit limits
This patch protects GARM-owned and paas-charm-owned callbacks. It does not intercept direct callbacks registered on helper objects:
- Traefik ingress:
charms/garm/lib/charms/traefik_k8s/v2/ingress.py:386-392. - Prometheus metrics:
charms/garm/lib/charms/prometheus_k8s/v0/prometheus_scrape.py:1594-1623. - Loki logging:
charms/garm/lib/charms/loki_k8s/v1/loki_push_api.py:2383-2400. - Grafana dashboards:
charms/garm/lib/charms/grafana_k8s/v0/grafana_dashboard.py:1187-1198. - Data-platform PostgreSQL raw relation callbacks:
charms/garm/lib/charms/data_platform_libs/v0/data_interfaces.py:1801-1826.
The patch also does not add a raw postgresql-relation-departed handler. Early runner cleanup and helper-library lifecycle changes remain separate follow-up work.
Summary
This draft contains only the central lifecycle gate:
planned_units()as the teardown predicate._reconcile()undecorated so the predicate is checked before normal state construction.paas-charminherited observers through GARM-owned guarded overrides.restart()during local teardown._create_charm_state()or normal GARM reconciliation.No cleanup, secret migration, or relation teardown behavior is included here.
Related to #332.
Validation
tox -c tox.toml -e unit— 257 passedtox -c tox.toml -e unit -- tests/unit/test_charm.py -k 'teardown_events_skip or teardown_update_status or skip_charm_state_creation'— 11 passedgit diff --check— passedDeliberately out of scope for this draft
Those can follow as separate, reviewable changes after the lifecycle gate is agreed.