diff --git a/packages/stack/src/services/Pooler.integration.test.ts b/packages/stack/src/services/Pooler.integration.test.ts index 9cb5ebbc88..7ce42948b6 100644 --- a/packages/stack/src/services/Pooler.integration.test.ts +++ b/packages/stack/src/services/Pooler.integration.test.ts @@ -1,6 +1,6 @@ import { NodeHttpClient, NodeServices } from "@effect/platform-node"; -import { describe, expect, it } from "@effect/vitest"; -import { Context, Effect, Layer, Redacted } from "effect"; +import { expect, layer } from "@effect/vitest"; +import { Context, Effect, Layer, Redacted, Scope } from "effect"; import { PgClient } from "@effect/sql-pg"; import { makeStandaloneService } from "../../tests/standalone-service.ts"; import { ProxyError } from "../Proxy.ts"; @@ -25,87 +25,136 @@ const dockerOptions = (root: string) => ({ engineTarget, }); -describe("service catalog", () => { - it.live( - "serves Pooler SQL against the owned database", - () => - Effect.scoped( - Effect.gen(function* () { - const root = yield* makeDockerDatabaseRoot("catalog-pooler-"); - const secret = "catalog-pooler-secret-with-at-least-32-chars"; - const databaseRecipe = yield* makeServiceRecipe( - { - service: "database", - config: { - version: "17", - databasePassword: Redacted.make("postgres"), - jwtSecret: Redacted.make(secret), - jwtExpiry: 3600, - }, - }, - dockerOptions(root), - ); - const database = yield* makeStandaloneService(databaseRecipe.definition, { - id: "database", - config: databaseRecipe.creation, - }); - yield* database.start; - yield* database.ready; - const databaseEndpoint = yield* databaseRecipe.endpoint("sql"); - if (databaseEndpoint.kind !== "tcp" || databaseEndpoint.host === undefined) - return yield* new ProxyError({ message: "Docker database did not expose TCP" }); - const databaseRelay = yield* makeDockerTcpRelay(databaseRecipe.endpoint("sql")); - const dockerDatabaseUrl = `postgresql://supabase_admin:postgres@${databaseRelay.host}:${databaseRelay.port}/_supabase`; - const nativeDatabaseUrl = `postgresql://supabase_admin:postgres@${databaseEndpoint.host}:${databaseEndpoint.port}/_supabase`; - for (const runtime of ["native", testEngine] as const) { - for (const poolMode of ["transaction", "session"] as const) { - const tenant = `catalog-${runtime}-${poolMode}`; - const poolerRecipe = yield* makeServiceRecipe( - { - service: "pooler", - config: { - databaseUrl: runtime === "native" ? nativeDatabaseUrl : dockerDatabaseUrl, - jwtSecret: secret, - tenant, - poolMode, - defaultPoolSize: 7, - maxClientConnections: 42, - }, - }, - runtime === "native" ? options(root) : dockerOptions(root), - ); - const pooler = yield* makeStandaloneService(poolerRecipe.definition, { - id: `pooler-${runtime}-${poolMode}`, - config: poolerRecipe.creation, - }); - for (const phase of runtime === "native" ? ["start", "restart"] : ["start"]) { - if (phase === "restart") { - yield* pooler.stop; - } - yield* pooler.start; - yield* pooler.ready; - const sqlEndpoint = yield* poolerRecipe.endpoint("sql"); - if (sqlEndpoint.kind !== "tcp" || sqlEndpoint.host === undefined) - return yield* new ProxyError({ message: "Pooler did not expose TCP" }); - const poolerLayer = yield* Layer.build( - PgClient.layer({ - host: sqlEndpoint.host, - port: sqlEndpoint.port, - database: "postgres", - username: `supabase_admin.${tenant}`, - password: Redacted.make("postgres"), - }), - ); - const sql = Context.get(poolerLayer, PgClient.PgClient); - const rows = yield* sql.unsafe<{ readonly value: number }>("SELECT 1 AS value"); - expect(rows[0]?.value).toBe(1); - } - yield* pooler.stop; - } - } - yield* database.stop; +interface SharedDatabase { + readonly root: string; + readonly secret: string; + readonly nativeDatabaseUrl: string; + readonly dockerDatabaseUrl: string; +} + +/** The one owned database shared by every pooler case below; started once, stopped once. */ +class PoolerDatabase extends Context.Service()( + "Pooler.integration.PoolerDatabase", +) {} + +const databaseLayer = Layer.effect( + PoolerDatabase, + Effect.gen(function* () { + const root = yield* makeDockerDatabaseRoot("catalog-pooler-"); + const secret = "catalog-pooler-secret-with-at-least-32-chars"; + const databaseRecipe = yield* makeServiceRecipe( + { + service: "database", + config: { + version: "17", + databasePassword: Redacted.make("postgres"), + jwtSecret: Redacted.make(secret), + jwtExpiry: 3600, + }, + }, + dockerOptions(root), + ); + // Forked so the graceful `database.stop` below can still run normally once this layer's + // own scope starts closing: scope finalizers run in reverse (LIFO) order of registration, + // so the fork's auto-registered close finalizer runs after ours, not concurrently with it. + const serviceScope = yield* Scope.fork(yield* Effect.scope, "sequential"); + const database = yield* makeStandaloneService(databaseRecipe.definition, { + id: "database", + config: databaseRecipe.creation, + }).pipe(Scope.provide(serviceScope)); + yield* database.start; + yield* database.ready; + yield* Effect.addFinalizer(() => database.stop.pipe(Effect.orDie)); + const databaseEndpoint = yield* databaseRecipe.endpoint("sql"); + if (databaseEndpoint.kind !== "tcp" || databaseEndpoint.host === undefined) + return yield* new ProxyError({ message: "Docker database did not expose TCP" }); + const databaseRelay = yield* makeDockerTcpRelay(databaseRecipe.endpoint("sql")); + return { + root, + secret, + nativeDatabaseUrl: `postgresql://supabase_admin:postgres@${databaseEndpoint.host}:${databaseEndpoint.port}/_supabase`, + dockerDatabaseUrl: `postgresql://supabase_admin:postgres@${databaseRelay.host}:${databaseRelay.port}/_supabase`, + }; + }), +).pipe(Layer.provideMerge(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))); + +/** Exercises one (runtime, poolMode) pooler against the shared database, restarting native poolers. */ +const servesPoolerSql = (params: { + readonly runtime: "native" | typeof testEngine; + readonly poolMode: "transaction" | "session"; +}) => + Effect.gen(function* () { + const { runtime, poolMode } = params; + const database = yield* PoolerDatabase; + const tenant = `catalog-${runtime}-${poolMode}`; + const poolerRecipe = yield* makeServiceRecipe( + { + service: "pooler", + config: { + databaseUrl: + runtime === "native" ? database.nativeDatabaseUrl : database.dockerDatabaseUrl, + jwtSecret: database.secret, + tenant, + poolMode, + defaultPoolSize: 7, + maxClientConnections: 42, + }, + }, + runtime === "native" ? options(database.root) : dockerOptions(database.root), + ); + const pooler = yield* makeStandaloneService(poolerRecipe.definition, { + id: `pooler-${runtime}-${poolMode}`, + config: poolerRecipe.creation, + }); + for (const phase of runtime === "native" + ? (["start", "restart"] as const) + : (["start"] as const)) { + if (phase === "restart") { + yield* pooler.stop; + } + yield* pooler.start; + yield* pooler.ready; + const sqlEndpoint = yield* poolerRecipe.endpoint("sql"); + if (sqlEndpoint.kind !== "tcp" || sqlEndpoint.host === undefined) + return yield* new ProxyError({ message: "Pooler did not expose TCP" }); + const poolerLayer = yield* Layer.build( + PgClient.layer({ + host: sqlEndpoint.host, + port: sqlEndpoint.port, + database: "postgres", + username: `supabase_admin.${tenant}`, + password: Redacted.make("postgres"), }), - ).pipe(Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))), + ); + const sql = Context.get(poolerLayer, PgClient.PgClient); + const rows = yield* sql.unsafe<{ readonly value: number }>("SELECT 1 AS value"); + expect(rows[0]?.value).toBe(1); + } + yield* pooler.stop; + }); + +layer(databaseLayer, { excludeTestServices: true })("service catalog", (it) => { + it.effect( + "serves Pooler SQL in transaction mode on the native runtime, across a restart", + () => servesPoolerSql({ runtime: "native", poolMode: "transaction" }), + { timeout: 120_000 }, + ); + + it.effect( + "serves Pooler SQL in session mode on the native runtime, across a restart", + () => servesPoolerSql({ runtime: "native", poolMode: "session" }), + { timeout: 120_000 }, + ); + + it.effect( + "serves Pooler SQL in transaction mode on the container runtime", + () => servesPoolerSql({ runtime: testEngine, poolMode: "transaction" }), + { timeout: 120_000 }, + ); + + it.effect( + "serves Pooler SQL in session mode on the container runtime", + () => servesPoolerSql({ runtime: testEngine, poolMode: "session" }), { timeout: 120_000 }, ); });