Opened 63 minutes ago
#37075 new Bug
`OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks `**pool_options` on top, raising `TypeError`.
| Reported by: | Raoni Timo de Castro Cambiaghi | Owned by: | |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 5.2 |
| Severity: | Normal | Keywords: | postgresql psycopg pool |
| Cc: | Raoni Timo de Castro Cambiaghi | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Reproduction
django/db/backends/postgresql/base.py (5.2.13) constructs the pool like this — see base.py L209-L215:
enable_checks = self.settings_dict["CONN_HEALTH_CHECKS"] pool = ConnectionPool( kwargs=connect_kwargs, open=False, configure=self._configure_connection, check=ConnectionPool.check_connection if enable_checks else None, **pool_options, )
If a user puts a "check" key in OPTIONS["pool"] to inject a custom validation callable — which is the documented way to extend psycopg_pool.ConnectionPool's liveness probe — the call collides at first cursor open:
TypeError: psycopg_pool.pool.ConnectionPool() got multiple values for keyword argument 'check'
CONN_HEALTH_CHECKS does not gate this. With True, Django passes check=ConnectionPool.check_connection; with False, Django passes check=None. Either way the keyword is set, so any check in pool_options collides.
Minimal settings.py:
def my_check(conn): conn.execute("SELECT 1").fetchone() DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "...", "USER": "...", "PASSWORD": "...", "HOST": "...", "PORT": "5432", "CONN_HEALTH_CHECKS": False, # also fails with True "OPTIONS": { "pool": { "min_size": 4, "max_size": 20, "check": my_check, }, }, } }
Expected behaviour
A "check" callable provided in OPTIONS["pool"] should win over Django's default. The documented contract for OPTIONS["pool"] is that the dict is forwarded to psycopg_pool.ConnectionPool, and check is a public, documented ConnectionPool.__init__ parameter — see psycopg pool docs.
Use case / motivation
The driving use case is AWS Aurora !PostgreSQL writer-flip handling. After a planned or unplanned writer failover, existing TCP connections survive but are now bound to a host that has become read-only. The default liveness probe (SELECT 1) does not detect this — the connection is "alive" but any subsequent INSERT/UPDATE fails with cannot execute X in a read-only transaction. AWS's Fast failover with Aurora PostgreSQL guide recommends chaining a pg_is_in_recovery() check into pool validation — exactly what psycopg_pool's check callable parameter exists for. Django 5.x makes this impossible without subclassing DatabaseWrapper and overriding pool.
Proposed fix
Use setdefault so OPTIONS["pool"]["check"], when present, takes precedence:
enable_checks = self.settings_dict["CONN_HEALTH_CHECKS"] pool_options.setdefault( "check", ConnectionPool.check_connection if enable_checks else None ) pool = ConnectionPool( kwargs=connect_kwargs, open=False, configure=self._configure_connection, **pool_options, )
This is fully backwards-compatible: users not setting OPTIONS["pool"]["check"] get exactly today's behaviour. Users who do set it get their callable. No new public surface — OPTIONS["pool"] is already documented as forwarding to ConnectionPool.
The same shape would make future custom callables (configure, reset) overridable too, though that's out of scope here — configure in particular needs more thought because Django chains its own connection configuration.
Versions affected
- Django 5.0 onward (where pool support landed)
- Confirmed on Django 5.2.13 with psycopg 3.3.3 / psycopg-pool 3.3.0
Happy to put up the patch + a regression test (assert that OPTIONS["pool"]["check"] is the actual callable used, with both CONN_HEALTH_CHECKS values) if the maintainers agree this is the right shape.