diff --git a/roles/sys-dns-cloudflare-records/defaults/main.yml b/roles/sys-dns-cloudflare-records/defaults/main.yml index 4226f6f8ec..c0d187c1f2 100644 --- a/roles/sys-dns-cloudflare-records/defaults/main.yml +++ b/roles/sys-dns-cloudflare-records/defaults/main.yml @@ -7,7 +7,8 @@ cloudflare_request_timeout: 20 # Seconds one Cloudflare HTTP socket operation ma cloudflare_attempt_timeout: 60 # Seconds one record job may run before async_wrapper kills it as hung cloudflare_attempts: 3 # Attempts per record; only hangs, timeouts, connection errors, 429 and 5xx earn another cloudflare_retry_delay: 10 # Seconds between two attempts -cloudflare_collect_retries: "{{ ((lookup('timeout', cloudflare_attempt_timeout | int) | int) / 5) | round(0, 'ceil') | int + 2 }}" # Cap on 5 s polls per job; the attempt's collect deadline ends them sooner +cloudflare_collect_interval: 1 # Seconds between two polls of one record job while collecting +cloudflare_collect_retries: "{{ ((lookup('timeout', cloudflare_attempt_timeout | int) | int) / (cloudflare_collect_interval | int)) | round(0, 'ceil') | int + 2 }}" # Cap on polls per job; the attempt's collect deadline ends them sooner cloudflare_tolerate: # Substrings of an answer that mean the record already stands as wanted - "An identical record already exists" - "code: 81058" diff --git a/roles/sys-dns-cloudflare-records/tasks/judge.yml b/roles/sys-dns-cloudflare-records/tasks/judge.yml index b996b516a8..4602094153 100644 --- a/roles/sys-dns-cloudflare-records/tasks/judge.yml +++ b/roles/sys-dns-cloudflare-records/tasks/judge.yml @@ -8,7 +8,7 @@ register: _cf_reaped until: _cf_reaped.finished or (now(utc=true).timestamp() > (_cf_collect_deadline | float)) retries: "{{ cloudflare_collect_retries | int }}" - delay: 5 + delay: "{{ cloudflare_collect_interval | int }}" changed_when: false ignore_errors: true when: ASYNC_ENABLED | bool diff --git a/tests/unit/python/roles/sys-dns-cloudflare-records/__init__.py b/tests/unit/python/roles/sys-dns-cloudflare-records/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/python/roles/sys-dns-cloudflare-records/test_collect_polling.py b/tests/unit/python/roles/sys-dns-cloudflare-records/test_collect_polling.py new file mode 100644 index 0000000000..dc59c12755 --- /dev/null +++ b/tests/unit/python/roles/sys-dns-cloudflare-records/test_collect_polling.py @@ -0,0 +1,76 @@ +import unittest + +from jinja2 import Environment, StrictUndefined + +from plugins.lookup.timeout import scaled_timeout +from utils import PROJECT_ROOT +from utils.cache.yaml import load_yaml, load_yaml_any +from utils.roles.mapping import ROLE_FILE_DEFAULTS_MAIN + +ROLE = PROJECT_ROOT / "roles" / "sys-dns-cloudflare-records" + + +def _defaults() -> dict: + return load_yaml(str(ROLE / ROLE_FILE_DEFAULTS_MAIN)) + + +def _collect_task() -> dict: + return next( + task + for task in load_yaml_any(str(ROLE / "tasks" / "judge.yml")) + if "ansible.builtin.async_status" in task + ) + + +def _render(expression: str, variables: dict, factor: int): + """ + Args: + expression: one templated value of the role. + variables: the role defaults the expression may read. + factor: the ``TIMEOUT_FACTOR`` the ``timeout`` lookup scales by. + + Returns: + The rendered value as an integer. + """ + environment = Environment(undefined=StrictUndefined, autoescape=True) + environment.globals["lookup"] = lambda name, base: scaled_timeout(base, factor) + return int(environment.from_string(str(expression)).render(**variables)) + + +class TestCollectPolling(unittest.TestCase): + def test_collect_polls_at_the_role_interval_and_retries_by_it(self): + defaults = _defaults() + task = _collect_task() + + self.assertEqual(task["delay"], "{{ cloudflare_collect_interval | int }}") + self.assertEqual(task["retries"], "{{ cloudflare_collect_retries | int }}") + self.assertIn( + "cloudflare_collect_interval", defaults["cloudflare_collect_retries"] + ) + self.assertIn("_cf_collect_deadline", task["until"]) + + def test_the_polling_window_covers_the_attempt_timeout(self): + defaults = _defaults() + task = _collect_task() + for factor in (1, 2, 3): + for interval in (1, 2, 5, 7): + with self.subTest(factor=factor, interval=interval): + variables = dict(defaults, cloudflare_collect_interval=interval) + variables["cloudflare_collect_retries"] = _render( + defaults["cloudflare_collect_retries"], variables, factor + ) + retries = _render(task["retries"], variables, factor) + delay = _render(task["delay"], variables, factor) + attempt = scaled_timeout( + defaults["cloudflare_attempt_timeout"], factor + ) + + self.assertEqual(delay, interval) + self.assertGreaterEqual(retries * delay, attempt) + + def test_the_default_interval_is_one_second(self): + self.assertEqual(_defaults()["cloudflare_collect_interval"], 1) + + +if __name__ == "__main__": + unittest.main()