mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
fix(web-app-weblate): collect static files into a tmpfs per container
Every Weblate container (weblate, beat, worker) runs collectstatic into STATIC_ROOT=/app/cache/static on start. /app/cache is the `cache` volume the three services share; in swarm it lives on NFS and every replica of every service mounts the same directory. Concurrent collectstatic runs delete and rewrite each other's hashed files, so the tasks crash-loop: weblate.1, .2, .3 and worker.1 on both swarm nodes died with "FileNotFoundError: [Errno 2] No such file or directory: '/app/cache/static/admin/js/vendor/select2/i18n/az.270c257daf81.js'" (and logo-32.png, dark_mode.a364bd93cdcc.css, widgets.8f8bf7f533a4.js), which failed Weblate#1 in runs 40625679270 and 40243434327. meta/volumes.yml mounts a tmpfs at /app/cache/static into every service that shares the cache volume, so each container collects and serves its own copy, consistent with its own manifest; the rest of /app/cache stays shared. container_volumes renders it as a long-form tmpfs mount for compose and swarm alike, as it already does for the Matrix bridges. test_static_root_per_container asserts that every service mounting the shared cache also gets the tmpfs at the static root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e1ea3e9cc
commit
a0bcd59150
2 files changed
+74
No files matched your search
@@ -19,3 +19,12 @@ cache:
|
||||
target: *app_cache_target
|
||||
- service: worker
|
||||
target: *app_cache_target
|
||||
static:
|
||||
type: tmpfs
|
||||
mounts:
|
||||
- service: weblate
|
||||
target: &app_static_target /app/cache/static
|
||||
- service: beat
|
||||
target: *app_static_target
|
||||
- service: worker
|
||||
target: *app_static_target
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from plugins.lookup.container_volumes import container_volumes
|
||||
from utils.cache.files import PROJECT_ROOT, read_text
|
||||
from utils.cache.yaml import load_yaml_str
|
||||
from utils.roles.mapping import ROLE_FILE_META_VOLUMES
|
||||
|
||||
APPLICATION_ID = "web-app-weblate"
|
||||
VOLUMES = PROJECT_ROOT / "roles" / APPLICATION_ID / ROLE_FILE_META_VOLUMES
|
||||
CACHE_DIR = "/app/cache"
|
||||
STATIC_ROOT = f"{CACHE_DIR}/static"
|
||||
|
||||
|
||||
def cache_sharers() -> list[str]:
|
||||
"""Return every service ``meta/volumes.yml`` mounts the shared cache into.
|
||||
|
||||
Returns:
|
||||
The service keys of the mounts whose target is ``CACHE_DIR``, sorted.
|
||||
"""
|
||||
volumes = load_yaml_str(read_text(str(VOLUMES)))
|
||||
return sorted(
|
||||
{
|
||||
mount["service"]
|
||||
for entry in volumes.values()
|
||||
if entry.get("type", "volume") == "volume"
|
||||
for mount in entry.get("mounts") or []
|
||||
if mount.get("target") == CACHE_DIR
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def rendered_mounts(service: str) -> list[object]:
|
||||
"""Render the ``volumes:`` list ``compose.yml.j2`` receives for a service.
|
||||
|
||||
Args:
|
||||
service: the compose service key.
|
||||
|
||||
Returns:
|
||||
The mount entries ``container_volumes`` emits for it.
|
||||
"""
|
||||
rendered = container_volumes({APPLICATION_ID: {}}, APPLICATION_ID, service)
|
||||
return load_yaml_str(rendered)["volumes"]
|
||||
|
||||
|
||||
class TestStaticRootPerContainer(unittest.TestCase):
|
||||
def test_every_cache_sharer_collects_static_into_its_own_tmpfs(self) -> None:
|
||||
sharers = cache_sharers()
|
||||
self.assertEqual(sharers, ["beat", "weblate", "worker"])
|
||||
for service in sharers:
|
||||
with self.subTest(service=service):
|
||||
self.assertIn(
|
||||
{"type": "tmpfs", "target": STATIC_ROOT},
|
||||
rendered_mounts(service),
|
||||
f"every Weblate task runs collectstatic into {STATIC_ROOT} on "
|
||||
f"start; on the {CACHE_DIR} volume the tasks share, concurrent "
|
||||
f"runs delete each other's hashed files and the swarm tasks "
|
||||
f"crash-loop with FileNotFoundError, so '{service}' needs a "
|
||||
f"tmpfs of its own there",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user