fix(swarm-test): ask the swarm for the DB service

03_wait_converge built the DB service name from the engine type:
"${DB_DEP}_${DB_DEP}" -> mariadb_mariadb. That name is only correct when
the database is central. force_shared_db deliberately leaves a
manager-pinned role carrying a literal `shared: false` alone, so
web-app-magento keeps its embedded engine, and sys-svc-rdbms deploys it as
magento_database inside magento's own stack.

The name came from the engine, which is global, and was looked up with
`docker service ls` across the whole swarm instead of the role's stack, so
the `shared` flag never entered the check at any step. Both polarities were
wrong:

  variant #1 - no consumer pulls the central provider in, so
  mariadb_mariadb does not exist. Run 32630684501 job 97175522113 polled it
  ninety times and failed the drill, while the rescue artifact shows
  magento_database at 1/1 Running and the play at failed=0.

  variant #0 - matomo and mailu declare a Jinja `shared`, get force-shared
  and drag svc-db-mariadb in. Job 95662862942 then reported
  `db(mariadb): 1/1 Running 2 hours ago` beside `magento: Running 21
  minutes ago` - green on a database magento never touches.

resolve_db_service asks the deployed swarm which service carries this
role's database: <entity>_database first, else <dep>_<dep>. The two cannot
collide, because sys-svc-rdbms renders the _database sidecar only under
`lookup('database', application_id, 'local')`. Exact-line matching, not
docker's substring name filter, per 246558a13.

Only web-app-magento changes behaviour: it is the one role that is both
pinned by force_shared_db and seen by the DB_DEP grep. matrix and
bigbluebutton never reach the DB half - their entity key carries no image,
so skip_if_no_swarm_service exits first - and discourse resolves
postgres_postgres exactly as before, in every variant.

A role that declares a DB dep no service serves now says so and gates the
app half alone, instead of polling a name nobody deployed.

diagnostics.sh carried the same guess in its container filter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-23 16:38:53 +02:00
1 parent 73ae405854
commit 8230c187a2
4 files changed
+119 -10

No files matched your search

@@ -0,0 +1,92 @@
"""Which swarm service the converge gate accepts as a role's database.
``resolve_db_service`` in ``scripts/tests/deploy/swarm/utils/_context.sh`` picks
between two naming schemes that ``plugins/lookup/database.py`` keeps apart: a
role pinning its own engine runs it as ``<entity>_database`` inside its own
stack, a central provider as ``<dep>_<dep>`` in its own. Guessing one of them
cost run 32630684501 a job - ``mariadb_mariadb`` never existed for Magento - and
the same guess passed vacuously in the all-bonds variant, where an unrelated
role had dragged the central provider in. Pinned here: the local engine wins
when both are deployed, the central name is still used when only it exists, and
a name that is deployed for nobody yields nothing instead of a false match.
"""
from __future__ import annotations
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
from utils.cache.files import PROJECT_ROOT
CONTEXT = (
PROJECT_ROOT / "scripts" / "tests" / "deploy" / "swarm" / "utils" / "_context.sh"
)
PINNED_ENGINE_APP = "web-app-magento"
NO_DB_APP = "web-svc-html"
LOCAL_SERVICE = "magento_database"
CENTRAL_SERVICE = "mariadb_mariadb"
class TestResolveDbService(unittest.TestCase):
def _resolve(self, app_id: str, deployed: tuple[str, ...]) -> str:
with tempfile.TemporaryDirectory() as tmp:
stub_bin = Path(tmp) / "bin"
stub_bin.mkdir()
docker = stub_bin / "docker"
docker.write_text(
"#!/usr/bin/env bash\nprintf '%s\\n' {}\n".format(
" ".join(f"'{name}'" for name in deployed) or '""'
)
)
docker.chmod(0o755)
env = dict(os.environ)
env.update(
APP_ID=app_id,
MGR="stub-manager",
PATH=f"{stub_bin}:{env['PATH']}",
BASH_ENV="",
)
proc = subprocess.run(
["bash", "-c", f'source "{CONTEXT}"; resolve_db_service'],
cwd=PROJECT_ROOT,
env=env,
capture_output=True,
text=True,
check=True,
)
return proc.stdout.strip()
def test_the_local_engine_wins_when_the_central_one_is_also_deployed(self) -> None:
self.assertEqual(
self._resolve(PINNED_ENGINE_APP, (LOCAL_SERVICE, CENTRAL_SERVICE)),
LOCAL_SERVICE,
)
def test_the_local_engine_is_found_when_it_is_the_only_one(self) -> None:
self.assertEqual(
self._resolve(PINNED_ENGINE_APP, (LOCAL_SERVICE,)), LOCAL_SERVICE
)
def test_the_central_provider_is_used_when_no_local_engine_exists(self) -> None:
self.assertEqual(
self._resolve(PINNED_ENGINE_APP, (CENTRAL_SERVICE,)), CENTRAL_SERVICE
)
def test_a_substring_of_a_deployed_name_is_not_a_match(self) -> None:
self.assertEqual(
self._resolve(PINNED_ENGINE_APP, ("other_mariadb_mariadb_replica",)), ""
)
def test_nothing_is_resolved_when_neither_scheme_is_deployed(self) -> None:
self.assertEqual(self._resolve(PINNED_ENGINE_APP, ("magento_nginx",)), "")
def test_a_role_without_a_database_dep_resolves_nothing(self) -> None:
self.assertEqual(self._resolve(NO_DB_APP, (CENTRAL_SERVICE,)), "")
if __name__ == "__main__":
unittest.main()