mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Since778c18fb23the carrier key holds the tokens file's mtime and size. sys-token-store changed that stat on every inclusion (00_core.yml touched the file) and on every persisted token, and nothing re-parked a carrier afterwards. Every later task's fresh worker then rendered the applications SPOT cold, 30 to 80 s each. On mainfea6f31d9f(run 38812964308) seven heavy jobs logged 194 to 287 such tasks and ran out of the 20400 s budget; the same jobs before778c18fb23logged 65 to 83, all of them real waits. - 00_core.yml touches the store with modification_time and access_time preserved, so it only creates a missing file. In swarm the controller sees the store over NFS with attribute caching, and the touch alone made the second pass go cold without any write. - write.yml re-parks the carrier when the persist or the controller mirror changed the store. Both run once, so every host that ran the include gets the same verdict. - tasks/utils/setup/role.yml re-parks after every role on every host, so hosts that did not run the writing role recover too. Where the key still matches this serves the carried payload; measured 0.1 s per role in a local compose deploy. - The tokens file enters the key as the SHA-256 of its content, memoised per inode, mtime and size, so a touch no longer changes the key. A stat that still matches serves the memo (1.67 us against 1.41 us for the old stat signature), and a stale stat keeps key and YAML cache on the same old content instead of pairing new content with an old key. - applications_carrier opens the tokens file before it computes the key, so on the NFS-mounted controller the parked key reflects the revalidated attributes rather than a cached stat that changes a minute later. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
96 lines
3.0 KiB
Python
96 lines
3.0 KiB
Python
"""Play-scoped carrier of the rendered applications payload.
|
|
|
|
Ansible forks a fresh worker process per task, so the in-process
|
|
`_MERGED_APPLICATIONS_CACHE` starts empty in every worker. The constructor
|
|
stage renders once and parks the payload plus its cache key in the host fact
|
|
named by `APPLICATIONS_RENDERED_FACT`; every later worker inherits that fact
|
|
at fork time and `get_merged_applications` serves it instead of rendering.
|
|
Ansible-free at import time like the rest of the package.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from .base import (
|
|
_cache_key,
|
|
_resolve_roles_dir,
|
|
_revalidate_tokens_file,
|
|
_stable_variables_signature,
|
|
_tokens_file_signature,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
import os
|
|
|
|
APPLICATIONS_RENDERED_FACT = "_INFINITO_APPLICATIONS_RENDERED"
|
|
|
|
|
|
def merged_applications_cache_key(
|
|
variables: Mapping[str, Any] | None,
|
|
*,
|
|
roles_dir: str | os.PathLike[str] | None = None,
|
|
) -> tuple[str, tuple]:
|
|
"""Key under which one rendered applications payload stays valid.
|
|
|
|
Args:
|
|
variables: The play's variable scope. Only the subset that
|
|
`_stable_variables_signature` reads, and the tokens file it
|
|
resolves to, influence the key.
|
|
roles_dir: Roles tree the payload is built from; defaults to the
|
|
repository's `roles/`.
|
|
|
|
Returns:
|
|
``(resolved roles dir, stable variables signature extended by the
|
|
tokens file's path and content digest)``.
|
|
"""
|
|
from .users import _resolve_tokens_file
|
|
|
|
tokens = _tokens_file_signature(_resolve_tokens_file(variables))
|
|
return (
|
|
_cache_key(_resolve_roles_dir(roles_dir=roles_dir)),
|
|
(*_stable_variables_signature(variables), *(str(part) for part in tokens)),
|
|
)
|
|
|
|
|
|
def revalidate_tokens_file(variables: Mapping[str, Any] | None) -> None:
|
|
"""Refresh an NFS client's attributes of the tokens file before a carrier is keyed.
|
|
|
|
Args:
|
|
variables: The play's variable scope the tokens file resolves from.
|
|
"""
|
|
from .users import _resolve_tokens_file
|
|
|
|
_revalidate_tokens_file(_resolve_tokens_file(variables))
|
|
|
|
|
|
def _carried_applications(
|
|
variables: Mapping[str, Any], cache_key: tuple[str, tuple]
|
|
) -> dict[str, Any] | None:
|
|
"""Return the payload carried in `APPLICATIONS_RENDERED_FACT` when its
|
|
key equals *cache_key*, else ``None``.
|
|
|
|
Args:
|
|
variables: The play's variable scope, read for the carrier fact.
|
|
cache_key: The key the caller would render under.
|
|
"""
|
|
carrier = variables.get(APPLICATIONS_RENDERED_FACT)
|
|
if not isinstance(carrier, Mapping):
|
|
return None
|
|
key = carrier.get("key")
|
|
payload = carrier.get("applications")
|
|
if (
|
|
not isinstance(payload, Mapping)
|
|
or not isinstance(key, (list, tuple))
|
|
or len(key) != 2
|
|
or not isinstance(key[1], (list, tuple))
|
|
):
|
|
return None
|
|
if [str(key[0]), [str(part) for part in key[1]]] != [
|
|
cache_key[0],
|
|
list(cache_key[1]),
|
|
]:
|
|
return None
|
|
return payload
|