mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
fix(cache): key the applications SPOT by the groups its payload reads
A service flag may be written as `{{ '<role>' in group_names }}`; 114
meta/services.yml files do exactly that, and it is the only host-dependent
variable any of them uses. The rendered payload therefore differs per host
while the cache key did not: it was built from applications, users,
DOMAIN_PRIMARY and SYSTEM_EMAIL_DOMAIN alone, and in the failing job all
five host_vars files were byte-identical.
113113e553 moved the render out of every task on the premise that Ansible
forks a fresh worker per task, so the in-process cache is always cold. That
holds for workers and not for the strategy's main process, which renders a
task's name once for the first host of a batch. Anything cached there is
inherited by every worker forked afterwards, and applications.py consults
that process cache before the host's own carrier fact, so an inherited
entry outranks the correct answer.
Pretix#2 of run 35152776835 shows both sides in one task: `skipping:
[web-app-pretix-swarm-mgr-01]` against `included: dedicated.yml for
wrk-01, wrk-02`, eleven seconds before mgr-01 resolved its database to
`pretix_database`. The swarm was running the central `postgres_postgres`,
so the wait never converged and the job died after 600 attempts. Variants 0
and 1 pin the flag to a literal and both pass; only variant 2 inherits the
Jinja.
The groups join the key as a sorted comma-joined string rather than a
tuple, because carrier.py stringifies only one side of its key comparison.
A non-scalar component would make the carrier silently stop matching and
every lookup re-render.
domains derives from the applications view and inherits the same
dependency, so this closes a second instance that had not surfaced yet.
users reads group_names nowhere. No extra rendering results: group_names is
constant inside a worker, and the one process that sees several hosts is
the one the split is for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c693f4f619
commit
c4556fe72d
3 files changed
+36
-5
No files matched your search
+24
-2
@@ -111,13 +111,35 @@ class TestStableVariablesSignature(unittest.TestCase):
|
||||
def test_empty_variables_collapses_to_canonical_tuple(self):
|
||||
self.assertEqual(
|
||||
base._stable_variables_signature(None),
|
||||
("0", "0", "", ""),
|
||||
("0", "0", "", "", ""),
|
||||
)
|
||||
self.assertEqual(
|
||||
base._stable_variables_signature({}),
|
||||
("0", "0", "", ""),
|
||||
("0", "0", "", "", ""),
|
||||
)
|
||||
|
||||
def test_two_hosts_of_one_play_do_not_share_a_key(self):
|
||||
shared = {"applications": {"web-app-pretix": {}}, "DOMAIN_PRIMARY": "x.test"}
|
||||
member = base._stable_variables_signature(
|
||||
{**shared, "group_names": ["web-app-pretix", "svc-db-postgres"]}
|
||||
)
|
||||
outsider = base._stable_variables_signature(
|
||||
{**shared, "group_names": ["web-app-pretix"]}
|
||||
)
|
||||
|
||||
self.assertNotEqual(
|
||||
member,
|
||||
outsider,
|
||||
"a service flag may read group_names, so one host's payload must "
|
||||
"never answer for a host whose groups differ",
|
||||
)
|
||||
|
||||
def test_group_order_does_not_split_the_key(self):
|
||||
first = base._stable_variables_signature({"group_names": ["a", "b"]})
|
||||
second = base._stable_variables_signature({"group_names": ["b", "a"]})
|
||||
|
||||
self.assertEqual(first, second)
|
||||
|
||||
def test_includes_domain_primary_and_email_domain_strings(self):
|
||||
sig = base._stable_variables_signature(
|
||||
{
|
||||
|
||||
+2
-2
@@ -129,8 +129,8 @@ class TestStableVariablesSignature(unittest.TestCase):
|
||||
_reset_cache_for_tests()
|
||||
|
||||
def test_empty_variables_returns_sentinel(self):
|
||||
self.assertEqual(_stable_variables_signature(None), ("0", "0", "", ""))
|
||||
self.assertEqual(_stable_variables_signature({}), ("0", "0", "", ""))
|
||||
self.assertEqual(_stable_variables_signature(None), ("0", "0", "", "", ""))
|
||||
self.assertEqual(_stable_variables_signature({}), ("0", "0", "", "", ""))
|
||||
|
||||
def test_captures_key_subset(self):
|
||||
sig = _stable_variables_signature(
|
||||
|
||||
Vendored
+10
-1
@@ -104,15 +104,24 @@ def _stable_variables_signature(variables: Mapping[str, Any] | None) -> tuple:
|
||||
"""Build a content-based cache signature from the subset of `variables`
|
||||
that influences the merged applications/users payload.
|
||||
|
||||
`group_names` belongs in the key because a service flag may be written as
|
||||
`{{ '<role>' in group_names }}`, so two hosts of one play render different
|
||||
payloads from identical host_vars. Ansible forks its workers, so a payload
|
||||
rendered in the strategy's main process for one host is inherited by every
|
||||
later worker; without this component that payload answers for hosts whose
|
||||
groups say otherwise.
|
||||
|
||||
See `_fingerprint_mapping` for why id()-only keys don't work reliably.
|
||||
"""
|
||||
if not variables:
|
||||
return ("0", "0", "", "")
|
||||
return ("0", "0", "", "", "")
|
||||
groups = variables.get("group_names")
|
||||
return (
|
||||
_fingerprint_mapping(variables.get("applications")),
|
||||
_fingerprint_mapping(variables.get("users")),
|
||||
str(variables.get("DOMAIN_PRIMARY") or ""),
|
||||
str(variables.get("SYSTEM_EMAIL_DOMAIN") or ""),
|
||||
",".join(sorted(str(group) for group in groups)) if groups else "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user