From 2783948a973bd831dec382b77ce675b64fd2a7f4 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Thu, 1 Oct 2026 12:30:07 +0200 Subject: [PATCH 1/2] test(stack): split the Pooler integration test by runtime and pool mode One test ran a database plus six full pooler launches (native x2 with a restart each, docker x2) under the single 120s guard sibling catalog tests use for one service launch, so it intermittently exceeded the guard under Docker-daemon contention (measured: a reproduced run spent time proportionally across every phase, no step hung). Split it into one test per (runtime, poolMode) case, each exercising only its own pooler (including the restart phase for native), sharing the one owned database through a @effect/vitest layer() scope so the database is still started once, readied once, and stopped once after the last test. Each case keeps the standard 120s guard, which now covers roughly one pooler launch instead of six; measured durations stayed under 40s even under heavy concurrent-suite contention. --- .../src/services/Pooler.integration.test.ts | 213 +++++++++++------- 1 file changed, 131 insertions(+), 82 deletions(-) diff --git a/packages/stack/src/services/Pooler.integration.test.ts b/packages/stack/src/services/Pooler.integration.test.ts index d6e0866bc4..fed82a52f5 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 { makeService } from "../Service.ts"; import { ProxyError } from "../Proxy.ts"; @@ -22,87 +22,136 @@ const dockerOptions = (root: string) => ({ runtime: "docker" as const, }); -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* makeService(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", "docker"] 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* makeService(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* makeService(databaseRecipe.definition, { + id: "database", + config: databaseRecipe.creation, + }).pipe(Scope.provide(serviceScope)); + yield* database.start; + yield* database.ready; + yield* Effect.addFinalizer(() => database.stop.pipe(Effect.ignore)); + 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" | "docker"; + 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* makeService(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 docker runtime", + () => servesPoolerSql({ runtime: "docker", poolMode: "transaction" }), + { timeout: 120_000 }, + ); + + it.effect( + "serves Pooler SQL in session mode on the docker runtime", + () => servesPoolerSql({ runtime: "docker", poolMode: "session" }), { timeout: 120_000 }, ); }); From c21ec9da310755dd56f8f2b96009cd6f411bb46c Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Thu, 1 Oct 2026 14:34:25 +0200 Subject: [PATCH 2/2] test(stack): report shared Pooler database stop failures The shared database finalizer ignored typed stop failures, so a failed shutdown passed silently. Die on them instead. --- packages/stack/src/services/Pooler.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/src/services/Pooler.integration.test.ts b/packages/stack/src/services/Pooler.integration.test.ts index fed82a52f5..8c1c780ba7 100644 --- a/packages/stack/src/services/Pooler.integration.test.ts +++ b/packages/stack/src/services/Pooler.integration.test.ts @@ -61,7 +61,7 @@ const databaseLayer = Layer.effect( }).pipe(Scope.provide(serviceScope)); yield* database.start; yield* database.ready; - yield* Effect.addFinalizer(() => database.stop.pipe(Effect.ignore)); + 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" });