mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
136 lines
5.7 KiB
Python
136 lines
5.7 KiB
Python
import subprocess
|
|
from collections.abc import Generator
|
|
from contextlib import ExitStack, contextmanager
|
|
from pathlib import Path
|
|
from typing import Final
|
|
from urllib.parse import urlsplit, urlunsplit
|
|
from uuid import uuid4
|
|
|
|
import psycopg
|
|
import pytest
|
|
from psycopg import sql
|
|
|
|
from .checks import COMPLETE, assert_completed
|
|
from .containers import Containers, docker, ready, until
|
|
from .database import Database, Databases, prisma_url, restricted_user
|
|
|
|
POOL_IMAGE: Final = (
|
|
"ghcr.io/cloudnative-pg/pgbouncer@sha256:e6ddfe22d845e603825e235dd8334b21ecd125abea2a2172478f556b8dee2bb8"
|
|
)
|
|
pytestmark: Final = [pytest.mark.e2e, pytest.mark.migration_startup]
|
|
|
|
|
|
@contextmanager
|
|
def application_user(database: Database) -> Generator[Database]:
|
|
with restricted_user(database) as application:
|
|
role: Final = sql.Identifier(str(urlsplit(application.url).username))
|
|
schema: Final = sql.Identifier(database.schema)
|
|
database.execute(sql.SQL("REVOKE CREATE ON SCHEMA {} FROM PUBLIC").format(schema))
|
|
for statement in (
|
|
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA {} TO {}",
|
|
"GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA {} TO {}",
|
|
"ALTER DEFAULT PRIVILEGES IN SCHEMA {} GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {}",
|
|
"ALTER DEFAULT PRIVILEGES IN SCHEMA {} GRANT USAGE, SELECT ON SEQUENCES TO {}",
|
|
):
|
|
database.execute(sql.SQL(statement).format(schema, role))
|
|
assert application.query("SELECT has_schema_privilege(current_user, %s, 'CREATE')", (database.schema,)) == (
|
|
(False,),
|
|
)
|
|
yield application
|
|
|
|
|
|
@contextmanager
|
|
def pool(database: Database, output: Path) -> Generator[str]:
|
|
name: Final = f"litellm-migration-pool-{uuid4().hex[:12]}"
|
|
url: Final = urlsplit(database.container_url)
|
|
directory: Final = output / name
|
|
directory.mkdir(parents=True)
|
|
(directory / "users.txt").write_text(f'"{url.username}" "{url.password}"\n')
|
|
(directory / "pgbouncer.ini").write_text(
|
|
f"[databases]\n* = host={url.hostname} port={url.port} user={url.username} password={url.password}\n"
|
|
"[pgbouncer]\nlisten_addr = 0.0.0.0\nlisten_port = 6432\nauth_type = trust\nauth_file = /pool/users.txt\n"
|
|
"pool_mode = transaction\ndefault_pool_size = 1\nreserve_pool_size = 0\nmax_client_conn = 100\n"
|
|
'max_prepared_statements = 100\nquery_wait_timeout = 8\nignore_startup_parameters = '
|
|
'extra_float_digits,options\n'
|
|
)
|
|
try:
|
|
docker(
|
|
"run",
|
|
"-d",
|
|
"--name",
|
|
name,
|
|
"--label",
|
|
"litellm-migration-test=true",
|
|
"--add-host",
|
|
"host.docker.internal:host-gateway",
|
|
"-p",
|
|
"0.0.0.0::6432",
|
|
"-v",
|
|
f"{directory}:/pool:ro",
|
|
"--entrypoint",
|
|
"/usr/bin/pgbouncer",
|
|
POOL_IMAGE,
|
|
"/pool/pgbouncer.ini",
|
|
)
|
|
port: Final = int(docker("port", name, "6432/tcp").splitlines()[0].rsplit(":", 1)[1])
|
|
local_url: Final = urlunsplit(url._replace(netloc=f"{url.username}:{url.password}@127.0.0.1:{port}"))
|
|
|
|
def connected() -> bool:
|
|
try:
|
|
with psycopg.connect(local_url, autocommit=True, connect_timeout=2) as connection:
|
|
return connection.execute("SELECT 1").fetchone() == (1,)
|
|
except psycopg.Error:
|
|
return False
|
|
|
|
until("PgBouncer ready", connected, 30)
|
|
yield local_url.replace("127.0.0.1", "host.docker.internal") + "?pgbouncer=true"
|
|
finally:
|
|
try:
|
|
logs: Final = subprocess.run(("docker", "logs", name), text=True, capture_output=True, timeout=30)
|
|
(directory / "pool.log").write_text(logs.stdout + logs.stderr)
|
|
finally:
|
|
subprocess.run(("docker", "rm", "-f", name), capture_output=True, text=True, timeout=30, check=True)
|
|
|
|
|
|
class TestMigrationPooling:
|
|
@pytest.mark.parametrize("scenario,replica_count", (("fresh", 3), ("upgrade", 3), ("legacy", 3), ("upgrade", 6)))
|
|
def test_direct_migrations_with_one_application_backend(
|
|
self,
|
|
containers: Containers,
|
|
databases: Databases,
|
|
migrated_template: Database,
|
|
scenario: str,
|
|
replica_count: int,
|
|
) -> None:
|
|
with databases.create(None if scenario == "fresh" else migrated_template) as database:
|
|
if scenario == "legacy":
|
|
database.execute("DROP TABLE _prisma_migrations")
|
|
with (
|
|
application_user(database) as application,
|
|
pool(application, containers.output) as pooled_url,
|
|
ExitStack() as stack,
|
|
):
|
|
replicas: Final = tuple(
|
|
stack.enter_context(
|
|
containers.start(
|
|
database,
|
|
(COMPLETE,) if scenario == "upgrade" else (),
|
|
environment={
|
|
"DATABASE_URL": prisma_url(pooled_url, database.schema),
|
|
"DIRECT_URL": database.container_url,
|
|
},
|
|
)
|
|
)
|
|
for _ in range(replica_count)
|
|
)
|
|
ready(replicas, database)
|
|
if scenario == "upgrade":
|
|
assert_completed(database)
|
|
if scenario == "legacy":
|
|
assert any(
|
|
"historical data backfills were not replayed or verified" in replica.logs()
|
|
for replica in replicas
|
|
)
|
|
assert database.query("SELECT count(*) FROM _prisma_migrations WHERE applied_steps_count <> 0") == (
|
|
(0,),
|
|
)
|