mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
868 lines
38 KiB
Python
868 lines
38 KiB
Python
import base64
|
|
import configparser
|
|
import json
|
|
import logging
|
|
import os
|
|
import signal
|
|
import socket
|
|
import stat
|
|
import sys
|
|
import tempfile
|
|
import textwrap
|
|
import time
|
|
import urllib.parse
|
|
from collections import deque
|
|
from collections.abc import Callable
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from typing import Final, cast
|
|
|
|
import pytest
|
|
|
|
from litellm._logging import verbose_proxy_logger
|
|
from litellm.proxy.db.pgbouncer import (
|
|
PGBOUNCER_POOLED_ENV_VAR,
|
|
PgBouncerError,
|
|
PgBouncerPlan,
|
|
PgBouncerProcess,
|
|
PgBouncerSettings,
|
|
PgBouncerTokenRefresher,
|
|
PgBouncerTokenSource,
|
|
database_url_is_pooled,
|
|
export_pooled_database_url,
|
|
install_pgbouncer_token,
|
|
pgbouncer_version,
|
|
plan_pgbouncer,
|
|
start_in_container_pgbouncer,
|
|
unix_socket_path,
|
|
write_pgbouncer_ini,
|
|
write_userlist,
|
|
)
|
|
from litellm.proxy.db.token_auth import AzureEntraTokenAuth, IAMEndpoint
|
|
|
|
UPSTREAM: Final = (
|
|
"postgresql://app:p%40ss%27w@db.internal:5433/litellm"
|
|
"?schema=public&connection_limit=10&pool_timeout=20"
|
|
"&sslmode=require&sslaccept=strict&sslcert=/certs/ca.pem"
|
|
"&options=-c%20statement_timeout%3D7000%20-c%20lock_timeout%3D3000"
|
|
)
|
|
SETTINGS: Final = PgBouncerSettings(enabled=True, port=6543, max_db_connections=8, max_client_conn=400)
|
|
|
|
|
|
def _plan(url: str = UPSTREAM, run_as_user: str | None = None) -> PgBouncerPlan:
|
|
plan: Final = plan_pgbouncer(url, SETTINGS, Path("/run/pgb"), run_as_user)
|
|
assert isinstance(plan, PgBouncerPlan), plan
|
|
return plan
|
|
|
|
|
|
def _ini(plan: PgBouncerPlan) -> configparser.ConfigParser:
|
|
parser: Final = configparser.ConfigParser(interpolation=None)
|
|
parser.read_string(plan.ini)
|
|
return parser
|
|
|
|
|
|
def _query(url: str) -> dict[str, str]:
|
|
return dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(url).query, keep_blank_values=True))
|
|
|
|
|
|
class TestPlanPgBouncer:
|
|
def test_upstream_route_and_timeouts_move_into_the_pgbouncer_config_without_the_password(self):
|
|
ini: Final = _ini(_plan())
|
|
assert ini["databases"]["litellm"] == (
|
|
"host='db.internal' port=5433 dbname='litellm' user='app' "
|
|
"connect_query='SET statement_timeout TO ''7000''; SET lock_timeout TO ''3000'''"
|
|
)
|
|
|
|
def test_the_auth_file_holds_the_upstream_password_and_the_pool_users_own(self):
|
|
plan: Final = _plan()
|
|
assert plan.upstream_password == "p@ss'w"
|
|
assert plan.userlist("p@ss'w") == f'"app" "p@ss\'w"\n"litellm_pgbouncer" "{plan.pool_password}"\n'
|
|
|
|
def test_a_token_with_quotes_is_escaped_the_way_pgbouncer_reads_it(self):
|
|
assert _plan().userlist('to"ken').startswith('"app" "to""ken"\n')
|
|
|
|
def test_an_upstream_without_a_port_is_reached_on_the_postgres_default(self):
|
|
ini: Final = _ini(_plan("postgresql://app:pw@db/litellm"))
|
|
assert ini["databases"]["litellm"] == "host='db' port=5432 dbname='litellm' user='app'"
|
|
|
|
def test_pool_is_sized_from_settings_in_transaction_mode(self):
|
|
pgb: Final = _ini(_plan())["pgbouncer"]
|
|
assert pgb["pool_mode"] == "transaction"
|
|
assert pgb["max_db_connections"] == "8"
|
|
assert pgb["default_pool_size"] == "8"
|
|
assert pgb["max_client_conn"] == "400"
|
|
assert pgb["auth_type"] == "scram-sha-256"
|
|
assert pgb["listen_addr"] == "127.0.0.1"
|
|
assert pgb["listen_port"] == "6543"
|
|
assert pgb["auth_file"] == "/run/pgb/userlist.txt"
|
|
assert pgb["unix_socket_dir"] == "/run/pgb"
|
|
|
|
def test_the_pool_user_can_read_the_pgbouncer_console(self):
|
|
assert _ini(_plan())["pgbouncer"]["stats_users"] == "litellm_pgbouncer"
|
|
|
|
def test_a_database_user_named_like_the_pool_user_is_refused(self):
|
|
outcome: Final = plan_pgbouncer(
|
|
"postgresql://litellm_pgbouncer:pw@db/litellm", SETTINGS, Path("/run/pgb"), None
|
|
)
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "litellm_pgbouncer" in outcome.reason
|
|
|
|
def test_pooled_url_points_prisma_at_loopback_as_the_pool_user_without_prepared_statements(self):
|
|
plan: Final = _plan()
|
|
pooled: Final = urllib.parse.urlsplit(plan.pooled_url)
|
|
assert (pooled.hostname, pooled.port, pooled.path) == ("127.0.0.1", 6543, "/litellm")
|
|
assert (pooled.username, pooled.password) == ("litellm_pgbouncer", plan.pool_password)
|
|
assert len(plan.pool_password) >= 32
|
|
assert "p%40ss" not in plan.pooled_url
|
|
assert _query(plan.pooled_url) == {
|
|
"schema": "public",
|
|
"connection_limit": "10",
|
|
"pool_timeout": "20",
|
|
"pgbouncer": "true",
|
|
}
|
|
|
|
@pytest.mark.parametrize("hop_param", ["channel_binding=require", "gssencmode=require"])
|
|
def test_transport_params_for_the_postgres_hop_stay_off_the_plain_tcp_loopback_url(self, hop_param: str):
|
|
pooled: Final = _plan(f"postgresql://app:pw@db/litellm?connection_limit=5&{hop_param}").pooled_url
|
|
assert _query(pooled) == {"connection_limit": "5", "pgbouncer": "true"}
|
|
|
|
def test_verified_tls_becomes_server_side_verify_full_with_a_ca_copy_in_the_runtime_dir(self):
|
|
plan: Final = _plan()
|
|
pgb: Final = _ini(plan)["pgbouncer"]
|
|
assert pgb["server_tls_sslmode"] == "verify-full"
|
|
assert pgb["server_tls_ca_file"] == "/run/pgb/server-ca.pem"
|
|
assert plan.ca_source == "/certs/ca.pem"
|
|
|
|
def test_unverified_require_stays_require_without_a_ca_file(self):
|
|
plan: Final = _plan("postgresql://app:pw@db/litellm?sslmode=require")
|
|
pgb: Final = _ini(plan)["pgbouncer"]
|
|
assert pgb["server_tls_sslmode"] == "require"
|
|
assert "server_tls_ca_file" not in pgb
|
|
assert plan.ca_source is None
|
|
|
|
def test_every_plan_gets_its_own_pool_password(self):
|
|
assert _plan().pool_password != _plan().pool_password
|
|
|
|
def test_no_tls_params_default_to_prefer(self):
|
|
assert _ini(_plan("postgresql://app:pw@db/litellm"))["pgbouncer"]["server_tls_sslmode"] == "prefer"
|
|
|
|
def test_verification_without_a_ca_bundle_is_refused(self):
|
|
outcome: Final = plan_pgbouncer(
|
|
"postgresql://app:pw@db/litellm?sslmode=require&sslaccept=strict", SETTINGS, Path("/run/pgb"), None
|
|
)
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "sslcert" in outcome.reason
|
|
|
|
def test_client_certificates_are_refused(self):
|
|
outcome: Final = plan_pgbouncer(
|
|
"postgresql://app:pw@db/litellm?sslidentity=/certs/client.p12", SETTINGS, Path("/run/pgb"), None
|
|
)
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "sslidentity" in outcome.reason
|
|
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
[
|
|
"postgresql://app:pw@db",
|
|
"postgresql://:pw@db/litellm",
|
|
"postgresql://app:pw@/litellm",
|
|
],
|
|
)
|
|
def test_urls_missing_a_route_are_refused(self, url: str):
|
|
outcome: Final = plan_pgbouncer(url, SETTINGS, Path("/run/pgb"), None)
|
|
assert isinstance(outcome, PgBouncerError)
|
|
|
|
def test_a_url_without_a_password_plans_for_a_token_to_be_installed_later(self):
|
|
plan: Final = _plan("postgresql://app@db/litellm")
|
|
assert plan.upstream_password is None
|
|
assert plan.userlist("minted").startswith('"app" "minted"\n')
|
|
|
|
def test_every_options_spelling_becomes_a_set_statement(self):
|
|
options: Final = urllib.parse.quote("-c a=1 -cb=2 --c=3")
|
|
ini: Final = _ini(_plan(f"postgresql://app:pw@db/litellm?options={options}"))
|
|
assert ini["databases"]["litellm"].endswith("connect_query='SET a TO ''1''; SET b TO ''2''; SET c TO ''3'''")
|
|
|
|
def test_options_that_are_not_settings_are_refused(self):
|
|
outcome: Final = plan_pgbouncer(
|
|
"postgresql://app:pw@db/litellm?options=-c%20search_path", SETTINGS, Path("/run/pgb"), None
|
|
)
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "options" in outcome.reason
|
|
|
|
def test_run_as_user_is_only_written_when_given(self):
|
|
assert _ini(_plan(run_as_user="nobody"))["pgbouncer"]["user"] == "nobody"
|
|
assert "user" not in _ini(_plan())["pgbouncer"]
|
|
|
|
|
|
class TestWritePgBouncerFiles:
|
|
def test_files_hold_the_plan_and_are_private_to_the_owner(self, tmp_path: Path):
|
|
plan: Final = _plan("postgresql://app:pw@db/litellm")
|
|
ini_path: Final = write_pgbouncer_ini(plan, tmp_path, None)
|
|
assert isinstance(ini_path, Path), ini_path
|
|
userlist_path: Final = write_userlist(plan.userlist("pw"), tmp_path, None)
|
|
assert ini_path == tmp_path / "pgbouncer.ini"
|
|
assert userlist_path == tmp_path / "userlist.txt"
|
|
assert ini_path.read_text() == plan.ini
|
|
assert userlist_path.read_text() == plan.userlist("pw")
|
|
for path in (ini_path, userlist_path):
|
|
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
|
assert not (tmp_path / "server-ca.pem").exists()
|
|
|
|
def test_the_ca_bundle_is_copied_next_to_the_ini_pgbouncer_reads(self, tmp_path: Path):
|
|
bundle: Final = tmp_path / "rds-root.pem"
|
|
bundle.write_text("-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n")
|
|
runtime_dir: Final = tmp_path / "run"
|
|
runtime_dir.mkdir()
|
|
plan: Final = plan_pgbouncer(
|
|
f"postgresql://app:pw@db/litellm?sslmode=verify-full&sslcert={bundle}", SETTINGS, runtime_dir, None
|
|
)
|
|
assert isinstance(plan, PgBouncerPlan), plan
|
|
ini_path: Final = write_pgbouncer_ini(plan, runtime_dir, None)
|
|
assert isinstance(ini_path, Path), ini_path
|
|
ca_file: Final = Path(_ini(plan)["pgbouncer"]["server_tls_ca_file"])
|
|
assert ca_file.parent == runtime_dir
|
|
assert ca_file.read_text() == bundle.read_text()
|
|
|
|
def test_an_unreadable_ca_bundle_is_reported(self, tmp_path: Path):
|
|
plan: Final = plan_pgbouncer(
|
|
f"postgresql://app:pw@db/litellm?sslmode=verify-full&sslcert={tmp_path / 'missing.pem'}",
|
|
SETTINGS,
|
|
tmp_path,
|
|
None,
|
|
)
|
|
assert isinstance(plan, PgBouncerPlan), plan
|
|
outcome: Final = write_pgbouncer_ini(plan, tmp_path, None)
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "missing.pem" in outcome.reason
|
|
assert not (tmp_path / "pgbouncer.ini").exists()
|
|
|
|
def test_rewriting_the_userlist_replaces_it_whole_and_leaves_nothing_else_behind(self, tmp_path: Path):
|
|
write_userlist('"app" "first"\n', tmp_path, None)
|
|
with open(tmp_path / "userlist.txt", encoding="utf-8") as before_rewrite:
|
|
write_userlist('"app" "second"\n', tmp_path, None)
|
|
assert before_rewrite.read() == '"app" "first"\n'
|
|
assert (tmp_path / "userlist.txt").read_text() == '"app" "second"\n'
|
|
assert stat.S_IMODE((tmp_path / "userlist.txt").stat().st_mode) == 0o600
|
|
assert sorted(path.name for path in tmp_path.iterdir()) == ["userlist.txt"]
|
|
|
|
|
|
class TestPooledUrlMarker:
|
|
def test_exporting_the_pooled_url_marks_it_for_the_workers(self, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv(PGBOUNCER_POOLED_ENV_VAR, "")
|
|
monkeypatch.delenv(PGBOUNCER_POOLED_ENV_VAR)
|
|
monkeypatch.setenv("DATABASE_URL", "postgresql://app:token@db/litellm")
|
|
assert not database_url_is_pooled()
|
|
export_pooled_database_url("postgresql://litellm_pgbouncer:pw@127.0.0.1:6432/litellm?pgbouncer=true")
|
|
assert os.environ["DATABASE_URL"] == "postgresql://litellm_pgbouncer:pw@127.0.0.1:6432/litellm?pgbouncer=true"
|
|
assert database_url_is_pooled()
|
|
assert PGBOUNCER_POOLED_ENV_VAR == "LITELLM_PGBOUNCER_POOLED_DATABASE_URL"
|
|
|
|
|
|
def _bound_port(sock: socket.socket) -> int:
|
|
return cast(tuple[str, int], sock.getsockname())[1]
|
|
|
|
|
|
def _free_port() -> int:
|
|
with socket.socket() as probe:
|
|
probe.bind(("127.0.0.1", 0))
|
|
return _bound_port(probe)
|
|
|
|
|
|
def _fake_pooler(
|
|
tmp_path: Path,
|
|
port: int,
|
|
exit_immediately: bool = False,
|
|
port_file: Path | None = None,
|
|
bind_delay_seconds: float = 0.0,
|
|
version_banner: str = "PgBouncer 1.25.2\nlibevent 2.1.13-stable",
|
|
auth_log: Path | None = None,
|
|
) -> Path:
|
|
"""An executable that listens like PgBouncer: on the TCP port first, then on ``.s.PGSQL.<port>`` in the socket dir.
|
|
|
|
Port and socket dir come from the ini it is given, else from ``port`` and
|
|
``tmp_path``. With ``port_file`` each start reads the port from that file
|
|
instead. ``bind_delay_seconds`` holds the bind back, like a slow start.
|
|
``--version`` prints ``version_banner``. With ``auth_log`` it appends the
|
|
``auth_file`` it reads at startup and on every SIGHUP, one line per read,
|
|
like PgBouncer loading its credentials.
|
|
"""
|
|
script: Final = tmp_path / "fake-pgbouncer"
|
|
script.write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
#!{sys.executable}
|
|
import configparser, os, pathlib, select, signal, socket, sys, time
|
|
if sys.argv[1:] == ["--version"]:
|
|
print({version_banner!r})
|
|
sys.exit(0)
|
|
if {exit_immediately!r}:
|
|
sys.exit(3)
|
|
ini = configparser.ConfigParser()
|
|
ini.read(sys.argv[1:2])
|
|
if not {auth_log is None!r}:
|
|
def load_auth_file(*_):
|
|
with open({str(auth_log)!r}, "a") as log:
|
|
log.write(repr(pathlib.Path(ini.get("pgbouncer", "auth_file")).read_text()) + "\\n")
|
|
load_auth_file()
|
|
signal.signal(signal.SIGHUP, load_auth_file)
|
|
port = ini.getint("pgbouncer", "listen_port", fallback={port})
|
|
if not {port_file is None!r}:
|
|
port = int(pathlib.Path({str(port_file)!r}).read_text())
|
|
socket_dir = ini.get("pgbouncer", "unix_socket_dir", fallback={str(tmp_path)!r})
|
|
socket_path = f"{{socket_dir}}/.s.PGSQL.{{port}}"
|
|
time.sleep({bind_delay_seconds!r})
|
|
listener = socket.socket()
|
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
listener.bind(("127.0.0.1", port))
|
|
listener.listen()
|
|
if os.path.exists(socket_path):
|
|
os.unlink(socket_path)
|
|
unix_listener = socket.socket(socket.AF_UNIX)
|
|
unix_listener.bind(socket_path)
|
|
unix_listener.listen()
|
|
while True:
|
|
for ready in select.select([listener, unix_listener], [], [])[0]:
|
|
conn, _ = ready.accept()
|
|
conn.close()
|
|
"""
|
|
)
|
|
)
|
|
script.chmod(0o700)
|
|
return script
|
|
|
|
|
|
def _listening(port: int) -> bool:
|
|
try:
|
|
with socket.create_connection(("127.0.0.1", port), timeout=0.5):
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _wait_until(condition: Callable[[], bool], timeout_seconds: float = 5.0) -> bool:
|
|
deadline: Final = time.monotonic() + timeout_seconds
|
|
while time.monotonic() < deadline:
|
|
if condition():
|
|
return True
|
|
time.sleep(0.05)
|
|
return False
|
|
|
|
|
|
class TestPgBouncerProcess:
|
|
def test_start_waits_for_the_listener_and_stop_ends_it(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port)),), port=port, socket_path=unix_socket_path(tmp_path, port)
|
|
)
|
|
assert pooler.start() is None
|
|
assert _listening(port)
|
|
pid: Final = pooler.pid
|
|
assert pid is not None
|
|
pooler.stop()
|
|
assert _wait_until(lambda: not _listening(port))
|
|
with pytest.raises(ProcessLookupError):
|
|
os.kill(pid, 0)
|
|
|
|
def test_a_crashed_pooler_is_restarted_with_a_new_pid(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port)),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
restart_delay_seconds=0.1,
|
|
)
|
|
assert pooler.start() is None
|
|
first_pid: Final = pooler.pid
|
|
assert first_pid is not None
|
|
os.kill(first_pid, signal.SIGKILL)
|
|
assert _wait_until(lambda: pooler.pid not in (None, first_pid) and _listening(port))
|
|
pooler.stop()
|
|
assert _wait_until(lambda: not _listening(port))
|
|
|
|
def test_a_failed_restart_is_retried_until_the_pooler_is_back(
|
|
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
):
|
|
port: Final = _free_port()
|
|
script: Final = _fake_pooler(tmp_path, port)
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(script),), port=port, socket_path=unix_socket_path(tmp_path, port), restart_delay_seconds=0.1
|
|
)
|
|
assert pooler.start() is None
|
|
first_pid: Final = pooler.pid
|
|
assert first_pid is not None
|
|
hidden: Final = script.rename(tmp_path / "hidden")
|
|
with caplog.at_level(logging.ERROR, logger=verbose_proxy_logger.name):
|
|
os.kill(first_pid, signal.SIGKILL)
|
|
assert _wait_until(lambda: any("could not be restarted" in record.message for record in caplog.records))
|
|
assert not _listening(port)
|
|
hidden.rename(script)
|
|
assert _wait_until(lambda: pooler.pid not in (None, first_pid) and _listening(port))
|
|
pooler.stop()
|
|
assert _wait_until(lambda: not _listening(port))
|
|
|
|
def test_a_replacement_that_never_listens_is_replaced_again(self, tmp_path: Path, caplog: pytest.LogCaptureFixture):
|
|
port: Final = _free_port()
|
|
port_file: Final = tmp_path / "port"
|
|
port_file.write_text(str(port))
|
|
script: Final = _fake_pooler(tmp_path, port, port_file=port_file)
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(script),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
restart_delay_seconds=0.1,
|
|
ready_timeout_seconds=2.0,
|
|
)
|
|
assert pooler.start() is None
|
|
first_pid: Final = pooler.pid
|
|
assert first_pid is not None
|
|
wrong_port: Final = _free_port()
|
|
port_file.write_text(str(wrong_port))
|
|
with caplog.at_level(logging.ERROR, logger=verbose_proxy_logger.name):
|
|
os.kill(first_pid, signal.SIGKILL)
|
|
assert _wait_until(lambda: _listening(wrong_port))
|
|
port_file.write_text(str(port))
|
|
assert _wait_until(lambda: any("did not start listening" in record.message for record in caplog.records))
|
|
assert _wait_until(lambda: _listening(port))
|
|
assert _wait_until(lambda: not _listening(wrong_port))
|
|
pooler.stop()
|
|
assert _wait_until(lambda: not _listening(port))
|
|
|
|
def test_stopping_during_the_restart_delay_leaves_no_pooler_behind(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port)),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
restart_delay_seconds=0.3,
|
|
)
|
|
assert pooler.start() is None
|
|
first_pid: Final = pooler.pid
|
|
assert first_pid is not None
|
|
os.kill(first_pid, signal.SIGKILL)
|
|
assert _wait_until(lambda: not _listening(port))
|
|
pooler.stop()
|
|
time.sleep(1.0)
|
|
assert not _listening(port)
|
|
assert pooler.pid == first_pid
|
|
|
|
def test_a_stopped_pooler_is_not_restarted(self, tmp_path: Path, caplog: pytest.LogCaptureFixture):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port)),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
restart_delay_seconds=0.1,
|
|
)
|
|
assert pooler.start() is None
|
|
with caplog.at_level(logging.ERROR, logger=verbose_proxy_logger.name):
|
|
pooler.stop()
|
|
time.sleep(0.5)
|
|
assert not _listening(port)
|
|
assert caplog.records == []
|
|
|
|
def test_a_pooler_that_exits_during_startup_is_reported(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port, exit_immediately=True)),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
)
|
|
outcome: Final = pooler.start()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "status 3" in outcome.reason
|
|
|
|
def test_a_missing_binary_is_reported(self, tmp_path: Path):
|
|
outcome: Final = PgBouncerProcess(
|
|
argv=("/nonexistent/pgbouncer",), port=_free_port(), socket_path=tmp_path / "sock"
|
|
).start()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "/nonexistent/pgbouncer" in outcome.reason
|
|
|
|
def test_a_port_owned_by_someone_else_is_refused_before_spawning(self, tmp_path: Path):
|
|
with socket.socket() as squatter:
|
|
squatter.bind(("127.0.0.1", 0))
|
|
squatter.listen()
|
|
port: Final = _bound_port(squatter)
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port)),), port=port, socket_path=unix_socket_path(tmp_path, port)
|
|
)
|
|
outcome: Final = pooler.start()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert f"127.0.0.1:{port} is already in use" in outcome.reason
|
|
assert pooler.pid is None
|
|
|
|
def test_a_replacement_waits_until_a_squatter_leaves_the_port(
|
|
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port)),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
restart_delay_seconds=0.5,
|
|
)
|
|
assert pooler.start() is None
|
|
first_pid: Final = pooler.pid
|
|
assert first_pid is not None
|
|
os.kill(first_pid, signal.SIGKILL)
|
|
assert _wait_until(lambda: not _listening(port))
|
|
with socket.socket() as squatter, caplog.at_level(logging.ERROR, logger=verbose_proxy_logger.name):
|
|
squatter.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
squatter.bind(("127.0.0.1", port))
|
|
squatter.listen()
|
|
assert _wait_until(lambda: any("already in use" in record.message for record in caplog.records))
|
|
assert pooler.pid == first_pid
|
|
assert _wait_until(lambda: pooler.pid not in (None, first_pid) and _listening(port))
|
|
pooler.stop()
|
|
assert _wait_until(lambda: not _listening(port))
|
|
|
|
def test_a_listener_that_grabs_the_port_after_the_spawn_is_not_taken_for_the_pooler(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port, bind_delay_seconds=0.5)),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
ready_timeout_seconds=3.0,
|
|
)
|
|
with socket.socket() as squatter, ThreadPoolExecutor(max_workers=1) as starter:
|
|
squatter.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
starting: Final = starter.submit(pooler.start)
|
|
assert _wait_until(lambda: pooler.pid is not None)
|
|
squatter.bind(("127.0.0.1", port))
|
|
squatter.listen()
|
|
outcome: Final = starting.result()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "exited with status 1" in outcome.reason
|
|
|
|
def test_a_port_served_by_a_stranger_while_the_pooler_is_still_starting_is_reported(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port, bind_delay_seconds=30.0)),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
ready_timeout_seconds=0.5,
|
|
)
|
|
with socket.socket() as squatter, ThreadPoolExecutor(max_workers=1) as starter:
|
|
squatter.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
starting: Final = starter.submit(pooler.start)
|
|
assert _wait_until(lambda: pooler.pid is not None)
|
|
squatter.bind(("127.0.0.1", port))
|
|
squatter.listen()
|
|
outcome: Final = starting.result()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert f"127.0.0.1:{port} is served by another process" in outcome.reason
|
|
pid: Final = pooler.pid
|
|
assert pid is not None
|
|
with pytest.raises(ProcessLookupError):
|
|
os.kill(pid, 0)
|
|
|
|
def test_a_pooler_that_never_listens_times_out(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, _free_port())),),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
ready_timeout_seconds=0.5,
|
|
)
|
|
outcome: Final = pooler.start()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "did not start listening" in outcome.reason
|
|
pid: Final = pooler.pid
|
|
assert pid is not None
|
|
with pytest.raises(ProcessLookupError):
|
|
os.kill(pid, 0)
|
|
|
|
|
|
NOW: Final = datetime(2026, 9, 10, 12, 0, tzinfo=timezone.utc)
|
|
ENDPOINT: Final = IAMEndpoint(host="db", port="5432", user="app", name="litellm")
|
|
|
|
|
|
def _entra_jwt(expires_at: datetime) -> str:
|
|
payload: Final = base64.urlsafe_b64encode(json.dumps({"exp": int(expires_at.timestamp())}).encode())
|
|
return f"aGVhZGVy.{payload.rstrip(b'=').decode()}.c2ln"
|
|
|
|
|
|
def _token_source(*tokens: str | Exception) -> PgBouncerTokenSource:
|
|
"""A token source handing out ``tokens`` in order, raising the exceptions among them, then repeating the last."""
|
|
pending: Final = deque(tokens)
|
|
|
|
def provide() -> str:
|
|
outcome: Final = pending.popleft() if len(pending) > 1 else pending[0]
|
|
if isinstance(outcome, Exception):
|
|
raise outcome
|
|
return outcome
|
|
|
|
return PgBouncerTokenSource(auth=AzureEntraTokenAuth(token_provider=provide), endpoint=ENDPOINT)
|
|
|
|
|
|
class TestPgBouncerTokenRefresher:
|
|
def _refresher(
|
|
self,
|
|
source: PgBouncerTokenSource,
|
|
installed: list[str],
|
|
install: Callable[[str], None] | None = None,
|
|
**timing: float,
|
|
) -> PgBouncerTokenRefresher:
|
|
return PgBouncerTokenRefresher(
|
|
source,
|
|
install if install is not None else installed.append,
|
|
now=lambda: NOW.replace(tzinfo=None),
|
|
**timing,
|
|
)
|
|
|
|
def test_the_next_refresh_is_due_a_buffer_before_the_token_expires(self):
|
|
installed: Final[list[str]] = []
|
|
token: Final = _entra_jwt(NOW + timedelta(hours=1))
|
|
refresher: Final = self._refresher(_token_source(token), installed, buffer_seconds=180)
|
|
assert refresher.refresh() == 3600 - 180
|
|
assert installed == [token]
|
|
|
|
def test_a_token_whose_expiry_cannot_be_read_is_refreshed_on_the_fallback_interval(self):
|
|
installed: Final[list[str]] = []
|
|
refresher: Final = self._refresher(_token_source("opaque token"), installed, fallback_seconds=600)
|
|
assert refresher.refresh() == 600
|
|
assert installed == ["opaque token"]
|
|
|
|
def test_a_token_already_inside_the_buffer_is_refreshed_after_the_retry_delay(self):
|
|
token: Final = _entra_jwt(NOW + timedelta(seconds=100))
|
|
refresher: Final = self._refresher(_token_source(token), [], buffer_seconds=180, retry_seconds=30)
|
|
assert refresher.refresh() == 30
|
|
|
|
def test_the_token_reaches_the_auth_file_in_wire_form_not_url_encoded(self):
|
|
installed: Final[list[str]] = []
|
|
self._refresher(_token_source("to ken/with+odd=chars"), installed).refresh()
|
|
assert installed == ["to ken/with+odd=chars"]
|
|
|
|
def test_a_failed_mint_is_reported_and_installs_nothing(self):
|
|
installed: Final[list[str]] = []
|
|
outcome: Final = self._refresher(_token_source(RuntimeError("no credential")), installed).refresh()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "Azure Entra token" in outcome.reason
|
|
assert "no credential" in outcome.reason
|
|
assert installed == []
|
|
|
|
def test_a_token_pgbouncer_cannot_hold_is_refused(self):
|
|
installed: Final[list[str]] = []
|
|
outcome: Final = self._refresher(_token_source("x" * 2048), installed).refresh()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "2047" in outcome.reason
|
|
assert installed == []
|
|
|
|
def test_an_auth_file_that_cannot_be_written_is_reported_not_raised(self):
|
|
def refuse(_: str) -> None:
|
|
raise PermissionError("read-only runtime dir")
|
|
|
|
outcome: Final = self._refresher(_token_source("token"), [], install=refuse).refresh()
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "read-only runtime dir" in outcome.reason
|
|
|
|
def test_start_fails_when_the_first_token_cannot_be_minted_and_schedules_nothing(self):
|
|
installed: Final[list[str]] = []
|
|
refresher: Final = self._refresher(
|
|
_token_source(RuntimeError("no credential"), "later"), installed, fallback_seconds=0.05
|
|
)
|
|
assert isinstance(refresher.start(), PgBouncerError)
|
|
time.sleep(0.3)
|
|
assert installed == []
|
|
|
|
def test_a_failed_renewal_keeps_the_previous_token_until_the_retry_succeeds(self, caplog: pytest.LogCaptureFixture):
|
|
installed: Final[list[str]] = []
|
|
refresher: Final = self._refresher(
|
|
_token_source("first", RuntimeError("blip"), "third"),
|
|
installed,
|
|
fallback_seconds=0.05,
|
|
retry_seconds=0.05,
|
|
)
|
|
with caplog.at_level(logging.ERROR, logger=verbose_proxy_logger.name):
|
|
assert refresher.start() is None
|
|
assert installed == ["first"]
|
|
assert _wait_until(lambda: "third" in installed)
|
|
assert installed[:2] == ["first", "third"]
|
|
assert any("keeps its current Azure Entra token" in record.message for record in caplog.records)
|
|
refresher.stop()
|
|
settled: Final = len(installed)
|
|
time.sleep(0.3)
|
|
assert len(installed) == settled
|
|
|
|
|
|
def _runtime_dir_listening_on(port: int) -> Path:
|
|
matches: Final = tuple(
|
|
ini.parent
|
|
for ini in Path(tempfile.gettempdir()).glob("litellm-pgbouncer-*/pgbouncer.ini")
|
|
if f"listen_port = {port}\n" in ini.read_text()
|
|
)
|
|
assert len(matches) == 1, matches
|
|
return matches[0]
|
|
|
|
|
|
class TestStartInContainerPgBouncer:
|
|
def test_returns_the_loopback_url_once_the_pooler_listens(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
auth_log: Final = tmp_path / "auth.log"
|
|
binary: Final = _fake_pooler(tmp_path, port, auth_log=auth_log)
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(binary))
|
|
pooled: Final = start_in_container_pgbouncer(settings, "postgresql://app:pw@db/litellm?connection_limit=5")
|
|
assert isinstance(pooled, str), pooled
|
|
parsed: Final = urllib.parse.urlsplit(pooled)
|
|
assert (parsed.username, parsed.hostname, parsed.port, parsed.path) == (
|
|
"litellm_pgbouncer",
|
|
"127.0.0.1",
|
|
port,
|
|
"/litellm",
|
|
)
|
|
assert _query(pooled) == {"connection_limit": "5", "pgbouncer": "true"}
|
|
assert _listening(port)
|
|
assert auth_log.read_text() == repr(f'"app" "pw"\n"litellm_pgbouncer" "{parsed.password}"\n') + "\n"
|
|
|
|
@pytest.mark.filterwarnings("ignore:This process .* is multi-threaded:DeprecationWarning")
|
|
def test_a_forked_worker_exiting_leaves_the_pooler_and_its_files_to_the_parent(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(_fake_pooler(tmp_path, port)))
|
|
exit_hooks: Final[list[Callable[[], None]]] = []
|
|
pooled: Final = start_in_container_pgbouncer(
|
|
settings, "postgresql://app:pw@db/litellm", register_exit_hook=exit_hooks.append
|
|
)
|
|
assert isinstance(pooled, str), pooled
|
|
runtime_dir: Final = _runtime_dir_listening_on(port)
|
|
|
|
worker: Final = os.fork()
|
|
if worker == 0:
|
|
try:
|
|
for hook in exit_hooks:
|
|
hook()
|
|
finally:
|
|
os._exit(0)
|
|
if not _wait_until(lambda: os.waitpid(worker, os.WNOHANG) != (0, 0)):
|
|
os.kill(worker, signal.SIGKILL)
|
|
pytest.fail("the forked worker did not exit: an exit hook blocked on state inherited from the parent")
|
|
assert _listening(port)
|
|
assert (runtime_dir / "pgbouncer.ini").exists()
|
|
|
|
for hook in exit_hooks:
|
|
hook()
|
|
assert _wait_until(lambda: not _listening(port))
|
|
assert not runtime_dir.exists()
|
|
|
|
def test_a_bad_upstream_url_is_reported_without_starting_anything(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(_fake_pooler(tmp_path, port)))
|
|
outcome: Final = start_in_container_pgbouncer(settings, "postgresql://app:pw@db")
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert not _listening(port)
|
|
|
|
def test_a_passwordless_url_without_token_auth_is_refused_without_starting_anything(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(_fake_pooler(tmp_path, port)))
|
|
outcome: Final = start_in_container_pgbouncer(settings, "postgresql://app@db/litellm")
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "IAM_TOKEN_DB_AUTH" in outcome.reason
|
|
assert not _listening(port)
|
|
|
|
def test_token_auth_mints_the_first_token_into_the_auth_file_before_the_pooler_starts(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
auth_log: Final = tmp_path / "auth.log"
|
|
binary: Final = _fake_pooler(tmp_path, port, auth_log=auth_log)
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(binary))
|
|
token: Final = _entra_jwt(datetime.now(tz=timezone.utc) + timedelta(hours=1))
|
|
pooled: Final = start_in_container_pgbouncer(
|
|
settings,
|
|
"postgresql://app:stale-token@db/litellm",
|
|
token_auth=AzureEntraTokenAuth(token_provider=lambda: token),
|
|
)
|
|
assert isinstance(pooled, str), pooled
|
|
parsed: Final = urllib.parse.urlsplit(pooled)
|
|
assert parsed.username == "litellm_pgbouncer"
|
|
assert token not in pooled
|
|
assert _listening(port)
|
|
assert auth_log.read_text() == repr(f'"app" "{token}"\n"litellm_pgbouncer" "{parsed.password}"\n') + "\n"
|
|
|
|
def test_a_first_token_that_cannot_be_minted_is_reported_without_starting_anything(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(_fake_pooler(tmp_path, port)))
|
|
|
|
def fail() -> str:
|
|
raise RuntimeError("no Azure credential")
|
|
|
|
outcome: Final = start_in_container_pgbouncer(
|
|
settings, "postgresql://app@db/litellm", token_auth=AzureEntraTokenAuth(token_provider=fail)
|
|
)
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "no Azure credential" in outcome.reason
|
|
assert not _listening(port)
|
|
|
|
def test_a_renewed_token_is_written_and_picked_up_by_the_running_and_by_a_restarted_pooler(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
auth_log: Final = tmp_path / "auth.log"
|
|
plan: Final = plan_pgbouncer(
|
|
"postgresql://app@db/litellm", PgBouncerSettings(enabled=True, port=port), tmp_path, None
|
|
)
|
|
assert isinstance(plan, PgBouncerPlan), plan
|
|
ini_path: Final = write_pgbouncer_ini(plan, tmp_path, None)
|
|
write_userlist(plan.userlist("first"), tmp_path, None)
|
|
pooler: Final = PgBouncerProcess(
|
|
argv=(str(_fake_pooler(tmp_path, port, auth_log=auth_log)), str(ini_path)),
|
|
port=port,
|
|
socket_path=unix_socket_path(tmp_path, port),
|
|
restart_delay_seconds=0.1,
|
|
)
|
|
assert pooler.start() is None
|
|
first_pid: Final = pooler.pid
|
|
assert first_pid is not None
|
|
install_pgbouncer_token(plan, tmp_path, None, pooler, "second")
|
|
assert _wait_until(lambda: auth_log.read_text().count("\n") == 2)
|
|
os.kill(first_pid, signal.SIGKILL)
|
|
assert _wait_until(lambda: pooler.pid not in (None, first_pid) and _listening(port))
|
|
pooler.stop()
|
|
assert auth_log.read_text().splitlines() == [
|
|
repr(plan.userlist("first")),
|
|
repr(plan.userlist("second")),
|
|
repr(plan.userlist("second")),
|
|
]
|
|
|
|
def test_a_pgbouncer_that_survives_a_failed_tcp_bind_is_refused_without_starting(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
binary: Final = _fake_pooler(tmp_path, port, version_banner="PgBouncer 1.18.1\nlibevent 2.1.12-stable")
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(binary))
|
|
outcome: Final = start_in_container_pgbouncer(settings, "postgresql://app:pw@db/litellm")
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "PgBouncer 1.18" in outcome.reason
|
|
assert "1.19" in outcome.reason
|
|
assert not _listening(port)
|
|
|
|
def test_the_first_version_that_dies_on_a_failed_tcp_bind_is_accepted(self, tmp_path: Path):
|
|
port: Final = _free_port()
|
|
binary: Final = _fake_pooler(tmp_path, port, version_banner="PgBouncer 1.19.0")
|
|
settings: Final = PgBouncerSettings(enabled=True, port=port, binary=str(binary))
|
|
pooled: Final = start_in_container_pgbouncer(settings, "postgresql://app:pw@db/litellm")
|
|
assert isinstance(pooled, str), pooled
|
|
assert urllib.parse.urlsplit(pooled).port == port
|
|
assert _listening(port)
|
|
|
|
|
|
class TestPgBouncerVersion:
|
|
def test_reads_major_and_minor_from_the_banner(self, tmp_path: Path):
|
|
assert pgbouncer_version(str(_fake_pooler(tmp_path, _free_port()))) == (1, 25)
|
|
|
|
def test_a_binary_that_cannot_run_is_reported(self, tmp_path: Path):
|
|
outcome: Final = pgbouncer_version(str(tmp_path / "missing-pgbouncer"))
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "missing-pgbouncer" in outcome.reason
|
|
|
|
def test_a_banner_without_a_version_is_reported(self, tmp_path: Path):
|
|
outcome: Final = pgbouncer_version(str(_fake_pooler(tmp_path, _free_port(), version_banner="something else")))
|
|
assert isinstance(outcome, PgBouncerError)
|
|
assert "something else" in outcome.reason
|
|
|
|
|
|
class TestPgBouncerSettings:
|
|
def test_reads_the_litellm_pgbouncer_env_vars(self, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("LITELLM_PGBOUNCER_ENABLED", "true")
|
|
monkeypatch.setenv("LITELLM_PGBOUNCER_PORT", "7000")
|
|
monkeypatch.setenv("LITELLM_PGBOUNCER_MAX_DB_CONNECTIONS", "12")
|
|
settings: Final = PgBouncerSettings()
|
|
assert (settings.enabled, settings.port, settings.max_db_connections) == (True, 7000, 12)
|
|
|
|
def test_defaults_are_off(self, monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.delenv("LITELLM_PGBOUNCER_ENABLED", raising=False)
|
|
assert PgBouncerSettings().enabled is False
|