mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 10:36:45 +09:00
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""Opt-in PgBouncer beside an installed PostgreSQL.
|
|
|
|
Turning pooling on adds a ``pgbouncer`` service to the PostgreSQL app's
|
|
managed compose override: same project, container ``<app>-pooler``, on the
|
|
shared service network, no host port. Apps then choose per connection:
|
|
``fromService: <pg>.connectionString`` stays direct, and ``pooledUrl`` /
|
|
``pooledConnectionString`` go through the pooler.
|
|
|
|
Transaction pooling is the mode: many app connections share few server
|
|
connections. It breaks session state: session-level prepared statements in
|
|
some drivers, ``LISTEN``/``NOTIFY``, advisory locks and ``SET`` without
|
|
``LOCAL``. So nothing switches to the pooled URL on its own, and the UI says
|
|
so where the choice is made.
|
|
"""
|
|
from typing import Dict, Optional
|
|
|
|
POOLER_IMAGE = 'edoburu/pgbouncer:v1.24.1-p1'
|
|
POOLER_SERVICE = 'pgbouncer'
|
|
POOL_MODE = 'transaction'
|
|
MAX_CLIENT_CONN = 500
|
|
DEFAULT_POOL_SIZE = 20
|
|
|
|
TRADEOFF = ('Transaction pooling breaks session features: prepared statements in some '
|
|
'drivers, LISTEN/NOTIFY, advisory locks and session SET. Apps keep the '
|
|
'direct connection unless they switch to the pooled URL.')
|
|
|
|
|
|
def container_name(app) -> str:
|
|
return f'{app.name}-pooler'
|
|
|
|
|
|
def is_postgres_engine(app) -> bool:
|
|
from app.services.service_connection_service import _template_for
|
|
from app.services.template_service import TemplateService
|
|
engine = TemplateService.engine_metadata(_template_for(app) or {}) or {}
|
|
return engine.get('protocol') == 'postgresql'
|
|
|
|
|
|
def sidecar(app, base_services: Dict) -> Optional[Dict]:
|
|
"""The compose service to add to ``app``'s override, or None."""
|
|
if not getattr(app, 'pooler_enabled', False) or not base_services:
|
|
return None
|
|
if not is_postgres_engine(app):
|
|
return None
|
|
from app.services.service_connection_service import _install_variables, _template_for
|
|
from app.services.template_service import TemplateService
|
|
engine = TemplateService.engine_metadata(_template_for(app) or {}) or {}
|
|
variables = _install_variables(app)
|
|
password = variables.get(engine.get('admin_password_var') or '', '')
|
|
if not password:
|
|
return None
|
|
primary = next(iter(base_services))
|
|
return {
|
|
'image': POOLER_IMAGE,
|
|
'container_name': container_name(app),
|
|
'restart': 'unless-stopped',
|
|
'depends_on': [primary],
|
|
'environment': {
|
|
'DB_HOST': primary,
|
|
'DB_PORT': '5432',
|
|
'DB_USER': engine.get('admin_user') or 'postgres',
|
|
'DB_PASSWORD': password,
|
|
'POOL_MODE': POOL_MODE,
|
|
'AUTH_TYPE': 'scram-sha-256',
|
|
'MAX_CLIENT_CONN': str(MAX_CLIENT_CONN),
|
|
'DEFAULT_POOL_SIZE': str(DEFAULT_POOL_SIZE),
|
|
'LISTEN_PORT': '5432',
|
|
},
|
|
}
|
|
|
|
|
|
def set_enabled(app, enabled: bool) -> Dict:
|
|
"""Save the flag and apply it: `compose up` adds or removes the sidecar
|
|
(the PostgreSQL container itself is unchanged, so it keeps running)."""
|
|
from app import db
|
|
from app.services.docker_service import DockerService
|
|
|
|
app.pooler_enabled = bool(enabled)
|
|
db.session.commit()
|
|
applied = None
|
|
if app.root_path and not app.server_id:
|
|
args = {'compose_file': app.compose_file} if app.compose_file else {}
|
|
up = DockerService.compose_up(app.root_path, detach=True, **args)
|
|
applied = bool(up.get('success'))
|
|
if applied and not enabled:
|
|
# `up` leaves a service dropped from the override running.
|
|
DockerService.run(['rm', '-f', container_name(app)], timeout=60)
|
|
return {'enabled': bool(app.pooler_enabled), 'applied': applied,
|
|
'host': container_name(app), 'tradeoff': TRADEOFF}
|