Files
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

80 lines
3.7 KiB
Python

"""PgBouncer beside an installed PostgreSQL (plan 86 §D1). Hermetic half; the
real round trip is test_real_pooler_docker.py."""
import json
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.service_connection_service import SHARED_NETWORK, ServiceConnectionService
from app.services.template_service import TemplateService
from tests.factories import make_application
@pytest.fixture
def installed(app, tmp_path, monkeypatch):
records = {}
monkeypatch.setattr(TemplateService, 'get_config', classmethod(
lambda cls: {'repos': [], 'installed': records}))
def make(name, template_id, variables):
root = tmp_path / name
root.mkdir()
(root / 'docker-compose.yml').write_text(yaml.safe_dump({'services': {'app': {'image': 'x'}}}))
(root / '.serverkit-template.json').write_text(json.dumps(
{'template_id': template_id, 'variables': variables}))
row = make_application(db, name=name, root_path=str(root))
records[str(row.id)] = {'template_id': template_id}
return row
return make
def test_pooling_adds_a_pgbouncer_service_and_a_pooled_url(installed):
pg = installed('pg', 'postgresql', {'DB_PASSWORD': 'pa$$', 'DB_NAME': 'shop'})
assert 'pooledUrl' not in ServiceConnectionService.spec(pg)['properties']
assert 'pgbouncer' not in (ComposeEnvService.render_override(pg.root_path)['content'] or '')
pg.pooler_enabled = True
db.session.commit()
override = yaml.safe_load(ComposeEnvService.render_override(pg.root_path)['content'])
bouncer = override['services']['pgbouncer']
assert bouncer['image'] == pooler_service.POOLER_IMAGE
assert bouncer['container_name'] == 'pg-pooler'
assert bouncer['environment']['DB_HOST'] == 'app'
assert bouncer['environment']['POOL_MODE'] == 'transaction'
assert bouncer['environment']['DB_PASSWORD'] == 'pa$$$$', 'compose would interpolate $'
assert 'ports' not in bouncer
assert bouncer['networks'] == {'default': {}, SHARED_NETWORK: {}}
props = ServiceConnectionService.spec(pg)['properties']
assert props['pooledUrl'] == 'postgresql://postgres:pa%24%24@pg-pooler:5432/shop'
assert props['connectionString'].startswith('postgresql://postgres:pa%24%24@pg:5432/')
def test_only_postgres_engines_get_a_pooler(installed):
cache = installed('cache', 'redis', {'DB_PASSWORD': 'x'})
cache.pooler_enabled = True
db.session.commit()
assert not pooler_service.is_postgres_engine(cache)
assert 'pgbouncer' not in (ComposeEnvService.render_override(cache.root_path)['content'] or '')
def test_api(installed, client, auth_headers, monkeypatch):
pg = installed('pg', 'postgresql', {'DB_PASSWORD': 'x'})
cache = installed('cache', 'redis', {'DB_PASSWORD': 'x'})
monkeypatch.setattr('app.services.docker_service.DockerService.compose_up',
classmethod(lambda cls, path, **kw: {'success': True}))
got = client.get(f'/api/v1/apps/{pg.id}/pooler', headers=auth_headers).get_json()
assert got['available'] is True and got['enabled'] is False and 'LISTEN' in got['tradeoff']
assert client.get(f'/api/v1/apps/{cache.id}/pooler',
headers=auth_headers).get_json()['available'] is False
put = client.put(f'/api/v1/apps/{pg.id}/pooler', json={'enabled': True}, headers=auth_headers)
assert put.status_code == 200 and put.get_json()['applied'] is True
assert client.put(f'/api/v1/apps/{pg.id}/pooler', json={'enabled': 'yes'},
headers=auth_headers).status_code == 400
assert client.put(f'/api/v1/apps/{cache.id}/pooler', json={'enabled': True},
headers=auth_headers).status_code == 400