Skip to content

cli: make test Postgres connection limits configurable#2410

Draft
ZeshanA wants to merge 2 commits into
encoredev:mainfrom
ZeshanA:zeshan/configurable-test-db-connections
Draft

cli: make test Postgres connection limits configurable#2410
ZeshanA wants to merge 2 commits into
encoredev:mainfrom
ZeshanA:zeshan/configurable-test-db-connections

Conversation

@ZeshanA
Copy link
Copy Markdown

@ZeshanA ZeshanA commented Apr 23, 2026

Summary

Adds two env vars, read at cluster creation time by the Encore daemon, so CI runners aren't capped by hardcoded defaults when running many parallel test packages:

  • ENCORE_SQLDB_MAX_CONNECTIONS — passed to the Postgres container as -c max_connections=N on creation. Lifts the server-side cap above Postgres's default of 100.
  • ENCORE_SQLDB_POOL_BUDGET — total pgx connection budget divided evenly across the local-cluster's databases. Replaces the effective runtime default (pgx's internal 30 per pool) with a configurable total-budget-divided-by-N.

Both default to current behaviour when unset — no migration needed.

Motivation

On CI runners with significant CPU/memory headroom, encore test -p=N ./... trips SQLSTATE 53300 (too many connections) long before the hardware is saturated. The Postgres container launches with stock max_connections=100, and the live runtime-config path (RuntimeConfigGenerator.initializeSQLDatabaseConfig) returns MaxConnections=0, so the Go runtime falls back to pgx's default of 30 per pool — each service eating a 30-conn slice of a 100-conn server.

(Note: ResourceManager.UpdateConfig in infra.go still contains a 96/N heuristic, but it's dead code — Manager.generateConfig has zero callers. Out of scope for this PR; leaving it for a follow-up cleanup.)

Design notes

  • Two independent knobs. The client-side pool budget is bounded above by the server's max_connections, so CI generally sets them together (e.g. ENCORE_SQLDB_MAX_CONNECTIONS=500 ENCORE_SQLDB_POOL_BUDGET=480). Keeping them as separate vars avoids hard-coding a server-vs-client ratio.
  • Env vars, not a CLI flag. cli/cmd/encore/test.go uses DisableFlagParsing and forwards everything to go test. Threading a new flag would mean touching the daemonpb.TestRequest proto + daemon + driver plumbing. Env vars read at the edges match the existing ENCORE_SQLDB_HOST/DATABASE/USER/PASSWORD family in cli/cmd/encore/daemon/daemon.go and are trivial to set in CI.
  • Override semantics. The budget is applied in runtime_config2.go only when SQLDatabaseConfig() returned MaxConnections=0, so a future explicit per-DB override still wins.
  • int32 clamp. ENCORE_SQLDB_POOL_BUDGET parses to a Go int but the proto field is int32; the helper clamps before returning to avoid silent wrap on pathological inputs.
  • Daemon-lifetime caveat. The daemon's environment is frozen at startup; changing the env var in a later shell requires restarting the daemon. ENCORE_SQLDB_MAX_CONNECTIONS additionally only takes effect on a fresh container — docker start reuses an existing container's args. Fine for ephemeral CI runners; flagged in code comments for local dev.

Code layout

  • cli/daemon/run/infra/infra.go: new helper SQLDatabaseMaxConnections(numLocalDBs int) int alongside SQLDatabaseConfig.
  • cli/daemon/run/runtime_config2.go: adds the helper to the infraManager interface; counts local (non-external) DBs once in initialize(); applies the per-DB cap at the SQLConnectionPool construction site.
  • cli/daemon/sqldb/docker/docker.go: appends -c max_connections=N to the docker run args when the env var is set.
  • cli/daemon/run/infra/infra_test.go: table-driven tests covering defaults, custom budgets, floor-at-1, zero/negative counts, invalid env values, and int32 overflow clamping.

Example usage

ENCORE_SQLDB_MAX_CONNECTIONS=500 \
ENCORE_SQLDB_POOL_BUDGET=480 \
encore test -p=8 ./...

Local verification

Confirmed end-to-end with a three-service sample app:

Scenario Docker args SHOW max_connections Per-DB pool
Both env vars set -c fsync=off -c max_connections=500 500 160 (= 480/3)
Both env vars unset -c fsync=off 100 32 (= 96/3)

Unit tests: 12 cases pass (go test ./cli/daemon/run/infra/...).

Test plan

  • go build ./cli/... passes
  • go test ./cli/daemon/run/... ./cli/daemon/sqldb/... passes
  • End-to-end verified via docker inspect and psql SHOW max_connections on a 3-service sample app
  • Defaults preserved when env vars unset (byte-identical emitted args)
  • rm.log used for warnings (carries app_id context, matching file conventions)

Notes for reviewers

  1. The existing 96/N heuristic in ResourceManager.UpdateConfig is dead code. Happy to remove it in a follow-up if desired.
  2. The external detection (g.DefinedSecrets["sqldb::"+db.Name]) is duplicated just to count local DBs. There's no existing helper in the run layer; cluster-side IsExternalDB uses the same sqldb:: secret-membership check. Can extract into a small helper if preferred.

ZeshanA added 2 commits April 23, 2026 17:10
Exposes two env vars, read when the daemon creates a test Postgres
cluster, so CI runners aren't capped by the hardcoded defaults:

- ENCORE_SQLDB_MAX_CONNECTIONS passes `-c max_connections=N` to the
  Postgres container on creation. Lifts the server-side cap above
  Postgres's default of 100.
- ENCORE_SQLDB_POOL_SIZE replaces the hardcoded 96 total budget that
  infra.go divides evenly across per-package test databases.

Both default to today's behaviour when unset. Raising one without the
other is rarely useful: the client-side pool budget is bounded above
by the server's max_connections, so CI should set them together.

The server-side flag only applies when a fresh container is created
(`docker start` reuses an existing container's args), which is fine
for ephemeral CI runners but flagged in a comment for local dev.
Second-opinion review surfaced two issues with the previous commit:

1. The `UpdateConfig` path on ResourceManager is dead code — its only
   caller `Manager.generateConfig` has zero callers in the repo. All
   live paths (encore run, encore test for both Go and TS) flow through
   `RuntimeConfigGenerator.initialize()` and call `SQLDatabaseConfig()`,
   which returns `MaxConnections=0`. The runtime then falls back to
   pgx's default of 30 per pool — that's why the 100-connection server
   cap is hit so quickly in CI. The previous commit's env-var read in
   `UpdateConfig` had no runtime effect.

2. ENCORE_SQLDB_POOL_SIZE was misnamed (it's a total budget, not a
   per-pool size), used the global logger instead of rm.log, and didn't
   clamp its int result before being cast to the proto's int32 field.

This commit:

- Reverts `UpdateConfig` to its main-branch shape (dead but harmless).
- Adds `ResourceManager.SQLDatabaseMaxConnections(numLocalDBs int) int`
  alongside `SQLDatabaseConfig`. Reads ENCORE_SQLDB_POOL_BUDGET,
  defaults to 96, logs via rm.log.Warn on invalid values, and clamps
  to int32 before returning.
- Plumbs the helper through the live path in `runtime_config2.go`:
  the generator counts local (non-external) DBs once, calls the
  helper, and sets MaxConnections on each emitted SQLConnectionPool
  only when SQLDatabaseConfig returned zero, so a future non-zero
  override still wins.
- Improves the docker.go comment to frame the daemon-lifetime caveat
  (the daemon's env is frozen at startup; the container reuse is one
  consequence of that).
- Adds table-driven unit tests for SQLDatabaseMaxConnections covering
  defaults, custom budgets, floor-at-1, zero/negative DB counts,
  invalid env values, and int32 overflow clamping.

Locally verified end-to-end with a three-service app:
- ENCORE_SQLDB_MAX_CONNECTIONS=500 ENCORE_SQLDB_POOL_BUDGET=480 →
  container launched with `-c max_connections=500`; `SHOW
  max_connections` returns 500; each DB's pool gets 160 (= 480/3).
- Both vars unset → container gets no `-c max_connections`; server
  reports 100; each DB's pool gets 32 (= 96/3).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant