Files
computer-playbook/tests/unit/python/roles/sys-front-inj-matomo/test_site_cache.py
T
kevinveenbirkenbachandClaude Opus 5.5 5b7284c4a4 perf(sys-front-inj-matomo): cache a failed site list load so later vhosts skip the dead API
On a host whose Matomo was not deployed yet, every vhost injected before
web-app-matomo asked SitesManager.getSitesWithAtLeastViewAccess again and
waited out uri_retry (30 retries x 2 s): 6 vhosts, 84 to 98 s each, 8.7
minutes in galaxyserver full-2. matomo_site_cache only kept a loaded list.

inject.yml now caches every executed load: the list on success, none on a
failed request or an error mapping. A cache of none skips the cached-id step
and getSitesIdFromSiteUrl, so the existing skip task turns the injection off
for the vhost as before, without the retry wait. web-app-matomo resets the
cache of every play host (delegate_facts, swarm workers included) after
07_config and before 08_reinject_consumers, so the re-injection and every
later vhost load the list from the now running Matomo.

Evidence: the real inject.yml with the real uri_retry against a closed port
and a stub Matomo, 11 vhosts in 4 scenarios: the first probe took 68.6 s, the
next two 0.4 and 0.3 s with matomo off; after the reset a known site came
from the cache, an unknown one went getSitesIdFromSiteUrl -> addSite and was
appended, a later vhost of that domain hit the cache. HEAD inject.yml took
68 s per vhost with the same outcome. The swarm reset reached a second host.
ansible-lint passes; tests/unit/python/roles/sys-front-inj-matomo pins the
gates and the reset order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 12:26:05 +01:00

82 lines
2.5 KiB
Python

import unittest
from jinja2 import Environment
from utils.cache.yaml import load_yaml_any
from . import PROJECT_ROOT
INJECT = PROJECT_ROOT / "roles" / "sys-front-inj-matomo" / "tasks" / "inject.yml"
INSTALL_PATH = (
PROJECT_ROOT / "roles" / "web-app-matomo" / "tasks" / "03_install_path.yml"
)
SCOPE = "abc"
SITES = [{"idsite": "3", "main_url": "https://a.example"}]
def _tasks(path):
return load_yaml_any(str(path)) or []
def _guard(task):
when = task["when"]
return when if isinstance(when, list) else [when]
def _eval(expressions, context) -> bool:
env = Environment(autoescape=True)
return all(bool(env.compile_expression(expr)(**context)) for expr in expressions)
def _task(tasks, predicate):
return next(task for task in tasks if predicate(task))
class TestNegativeSiteCache(unittest.TestCase):
def setUp(self):
tasks = _tasks(INJECT)
self.load = _task(tasks, lambda t: t.get("register") == "matomo_site_list")
self.site_check = _task(tasks, lambda t: t.get("register") == "site_check")
self.cached_id = _task(
tasks,
lambda t: "matomo_cached_site_ids" in str(t.get("vars", {})),
)
def _state(self, sites, scope=SCOPE):
return {
"matomo_site_cache": {"scope": scope, "sites": sites},
"matomo_site_cache_scope": SCOPE,
"matomo_site_id": None,
}
def test_a_cached_absence_skips_both_api_calls(self):
state = self._state(None)
self.assertFalse(_eval(_guard(self.load), state))
self.assertFalse(_eval(_guard(self.site_check), state))
self.assertFalse(_eval(_guard(self.cached_id), state))
def test_a_cached_list_still_asks_for_an_unknown_site(self):
self.assertTrue(_eval(_guard(self.site_check), self._state(SITES)))
def test_a_cache_of_another_scope_reloads_the_list(self):
self.assertTrue(_eval(_guard(self.load), self._state(None, scope="")))
class TestMatomoDropsTheCacheBeforeReinjecting(unittest.TestCase):
def test_the_reset_precedes_the_consumer_reinjection(self):
tasks = _tasks(INSTALL_PATH)
names = [str(t.get("include_tasks", "")) for t in tasks]
reinject = names.index("08_reinject_consumers.yml")
reset = _task(
tasks[:reinject],
lambda t: "matomo_site_cache" in t.get("ansible.builtin.set_fact", {}),
)
self.assertEqual(
reset["ansible.builtin.set_fact"]["matomo_site_cache"]["scope"], ""
)
if __name__ == "__main__":
unittest.main()