Files
ServerKit/backend/tests/test_real_pooler_docker.py
Juan Denis 45f7100d93 Offer an opt-in PgBouncer beside PostgreSQL
An installed PostgreSQL can switch on a transaction-mode PgBouncer
sidecar through its managed override, with no host port, on the
shared network. fromService gains pooledUrl; apps keep the direct
connectionString unless they switch, because transaction pooling
breaks prepared statements in some drivers, LISTEN and advisory
locks, and the card says so.
2026-09-27 22:27:32 -04:00

115 lines
4.9 KiB
Python

"""PgBouncer beside a REAL PostgreSQL (plan 86 §D1).
The bundled PostgreSQL template is rendered with the panel's rules; turning
pooling on adds the sidecar through the managed override. A separate project
that only has ``pooledUrl`` from fromService queries through the pooler (its
host is the pooler's name, so an answer proves the path), and turning pooling
off removes the sidecar while PostgreSQL keeps running.
SERVERKIT_DOCKER_BUILDS=1 pytest tests -m docker_builds
"""
import json
import os
import shutil
import subprocess
import time
import uuid
import pytest
import yaml
from app import db
from app.services import pooler_service
from app.services.compose_env_service import ComposeEnvService
from app.services.env_service import EnvService
from app.services.template_service import TemplateService
from tests.factories import make_application
def _docker_ready() -> bool:
if shutil.which('docker') is None:
return False
try:
return subprocess.run(['docker', 'info'], capture_output=True,
timeout=20).returncode == 0
except (OSError, subprocess.TimeoutExpired):
return False
pytestmark = [
pytest.mark.docker_builds,
pytest.mark.skipif(os.environ.get('SERVERKIT_DOCKER_BUILDS') != '1',
reason='docker-builds leg; opt in with SERVERKIT_DOCKER_BUILDS=1'),
pytest.mark.skipif(not _docker_ready(), reason='docker daemon not reachable'),
]
SECRET = 'p@ss:w/rd#1%'
def _running(name):
out = subprocess.run(['docker', 'ps', '--filter', f'name=^{name}$', '--format', '{{.Names}}'],
capture_output=True, text=True).stdout
return name in out.split()
def test_an_app_queries_postgres_through_the_pooler(app, tmp_path, monkeypatch):
suffix = uuid.uuid4().hex[:8]
pg_name, client_name = f'skc-pg-{suffix}', f'skc-api-{suffix}'
installed = {}
monkeypatch.setattr(TemplateService, 'get_config', classmethod(
lambda cls: {'repos': [], 'installed': installed}))
template = TemplateService.get_template('postgresql')['template']
variables = {'APP_NAME': pg_name, 'IMAGE_TAG': '16', 'BIND_ADDRESS': '127.0.0.1',
'PORT': '0', 'DB_PASSWORD': SECRET, 'DB_NAME': 'shop'}
root = tmp_path / pg_name
root.mkdir()
compose = yaml.safe_load(TemplateService.generate_compose(template, variables))
for svc in compose['services'].values():
svc.pop('ports', None)
(root / 'docker-compose.yml').write_text(yaml.safe_dump(compose))
(root / '.serverkit-template.json').write_text(json.dumps(
{'template_id': 'postgresql', 'variables': variables}))
pg = make_application(db, name=pg_name, root_path=str(root),
compose_file='docker-compose.yml')
installed[str(pg.id)] = {'template_id': 'postgresql'}
client_root = tmp_path / client_name
client_root.mkdir()
(client_root / 'docker-compose.yml').write_text(yaml.safe_dump({'services': {'api': {
'image': 'postgres:16-alpine',
'command': ['sh', '-c', 'for i in $$(seq 30); do '
'psql "$$DATABASE_URL" -Atc "select 40 + 2" && exit 0; sleep 2; done; exit 1'],
}}}))
client = make_application(db, name=client_name, root_path=str(client_root))
_v, _c, error = EnvService.set_env_reference(
client.id, 'DATABASE_URL', {'kind': 'service', 'service': pg_name, 'property': 'pooledUrl'})
assert error is None
pg_files = ['-f', str(root / 'docker-compose.yml'),
'-f', str(root / ComposeEnvService.OVERRIDE_NAME)]
client_files = ['-f', str(client_root / 'docker-compose.yml'),
'-f', str(client_root / ComposeEnvService.OVERRIDE_NAME)]
try:
result = pooler_service.set_enabled(pg, True)
assert result['applied'] is True, result
assert _running(pooler_service.container_name(pg))
assert EnvService.get_effective_env(client.id)['DATABASE_URL'].startswith(
f'postgresql://postgres:')
ComposeEnvService.refresh_for_project(str(client_root))
ran = subprocess.run(['docker', 'compose', '-p', client_name, *client_files,
'run', '--rm', 'api'], capture_output=True, text=True, timeout=300)
assert ran.stdout.strip().splitlines()[-1:] == ['42'], ran.stdout + ran.stderr
off = pooler_service.set_enabled(pg, False)
assert off['applied'] is True
assert not _running(pooler_service.container_name(pg))
assert _running(pg_name), 'turning pooling off must not stop PostgreSQL'
finally:
subprocess.run(['docker', 'compose', '-p', client_name, *client_files, 'down', '-v'],
capture_output=True, timeout=180)
subprocess.run(['docker', 'rm', '-f', pooler_service.container_name(pg)],
capture_output=True, timeout=60)
subprocess.run(['docker', 'compose', '-p', pg_name, *pg_files, 'down', '-v'],
capture_output=True, timeout=180)