Spec §6.3's environment rules: pooled-connection detection and the
advisory lock. classify_config/1 is pure (the config-based half of
mix phoenix_kit.doctor's existing PgBouncer heuristic,
doctor.ex:174-195, reused rather than re-derived); pooled?/1 and the
lock functions execute real queries and have no unit test.
Detection is two-layered, on purpose
Config alone (a non-5432 port, a hostname containing "pgbouncer") is a
fast, always-available hint — it is what mix phoenix_kit.doctor
already prints a warning from. It is not proof: a direct Postgres
connection can legitimately run on a non-standard port, and a
transaction-pooling proxy can sit behind a hostname that says nothing
about it. The authoritative signal is behavioral: pg_backend_pid()
sampled twice over the SAME checked-out connection, as two separate
(non-transactional) statements. A direct connection — or a
session-pooling proxy, which behaves like one for this purpose — answers
with the same pid both times, because both statements ride the one
physical server backend for as long as the connection is checked out.
Transaction-pooling PgBouncer can hand each statement a different backend
connection, because nothing binds them together outside an explicit
transaction. pooled?/1 therefore checks out one connection
(Ecto.Repo.checkout/2) before sampling — sampling via two plain
repo.query!/3 calls without checking out first would let Ecto's own
pool (nothing to do with PgBouncer) hand the two queries to different
workers and manufacture a false positive on a perfectly direct database.
Config classification and pid-sampling are combined with OR: either one
saying "pooled" is enough to require --unsafe-pooled — false positives
(an operator has to pass one extra flag) are the safe failure direction;
false negatives (skipping VALIDATE/the advisory lock against a pooled
connection that fooled both checks) are not.
Summary
Types
The config-only heuristic's verdict — a hint, never authoritative on its own.
Functions
The config-based half of the detection (doctor.ex's existing heuristic,
reused verbatim): a non-5432 port, or a hostname containing
"pgbouncer", is :maybe_pooled.
The advisory lock key repair uses — exposed for tests/diagnostics, never meant to be passed elsewhere.
The authoritative, behavioral check — see moduledoc. Checks out one
connection and compares pg_backend_pid() across two separate statements
on it. Fails toward true (pooled, the safer assumption) on any error —
an environment we cannot prove is direct is treated as pooled, never the
other way around.
Combines classify_config/1 and pooled?/1 with OR (see moduledoc).
config is the repo's Application.get_env(app, repo, []) keyword list.
Acquires lock_key/0 (session-level pg_advisory_lock/1, blocking) on
repo's checked-out connection, runs fun, then releases it — even if
fun raises. Must run on a direct connection (§6.3): PgBouncer in
transaction-pooling mode can hand the lock and unlock calls to different
backend connections, silently defeating it. --unsafe-pooled skips
calling this at all (PhoenixKit.Migrations.Repair never calls
with_lock/2 when the environment was classified pooled and the flag was
given).
Types
Functions
@spec classify_config(keyword()) :: config_verdict()
The config-based half of the detection (doctor.ex's existing heuristic,
reused verbatim): a non-5432 port, or a hostname containing
"pgbouncer", is :maybe_pooled.
iex> Environment.classify_config(port: 5432, hostname: "db.internal")
:direct
iex> Environment.classify_config(port: 6432, hostname: "db.internal")
:maybe_pooled
iex> Environment.classify_config(port: 5432, hostname: "pgbouncer.internal")
:maybe_pooled
iex> Environment.classify_config(url: "ecto://user:pass@pgbouncer:6432/db")
:maybe_pooled
@spec lock_key() :: non_neg_integer()
The advisory lock key repair uses — exposed for tests/diagnostics, never meant to be passed elsewhere.
@spec pooled?(Ecto.Repo.t()) :: boolean()
The authoritative, behavioral check — see moduledoc. Checks out one
connection and compares pg_backend_pid() across two separate statements
on it. Fails toward true (pooled, the safer assumption) on any error —
an environment we cannot prove is direct is treated as pooled, never the
other way around.
@spec pooled?( Ecto.Repo.t(), keyword() ) :: boolean()
Combines classify_config/1 and pooled?/1 with OR (see moduledoc).
config is the repo's Application.get_env(app, repo, []) keyword list.
@spec with_lock(Ecto.Repo.t(), (-> result)) :: result when result: term()
Acquires lock_key/0 (session-level pg_advisory_lock/1, blocking) on
repo's checked-out connection, runs fun, then releases it — even if
fun raises. Must run on a direct connection (§6.3): PgBouncer in
transaction-pooling mode can hand the lock and unlock calls to different
backend connections, silently defeating it. --unsafe-pooled skips
calling this at all (PhoenixKit.Migrations.Repair never calls
with_lock/2 when the environment was classified pooled and the flag was
given).