sqlalchemy-foundation-kit 0.2.1__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/CHANGELOG.md +14 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/PKG-INFO +4 -4
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/README.md +3 -3
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/__init__.py +3 -1
- sqlalchemy_foundation_kit-0.4.0/sqlalchemy_foundation_kit/__version__.py +1 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/config/postgres.py +3 -3
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/metrics/postgres.py +60 -4
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/settings/postgres.py +13 -4
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/protocols/__init__.py +2 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/protocols/metrics.py +36 -4
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/session/builder.py +18 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/session/factories.py +12 -3
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/session/manager.py +129 -2
- sqlalchemy_foundation_kit-0.2.1/sqlalchemy_foundation_kit/__version__.py +0 -1
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/.gitignore +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/LICENSE +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/pyproject.toml +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/_typing.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/base/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/base/_optional.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/base/engine.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/base/metadata.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/base/models.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/base/serialization.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/base/types.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/config/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/_metrics_utils.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/di/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/di/_base.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/di/_deps.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/di/database.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/di/metrics.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/metrics/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/settings/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/contrib/telemetry/uow.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/py.typed +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/session/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/session/connection.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/session/locks.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/session/retry.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/uow/__init__.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/uow/enums.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/uow/protocols.py +0 -0
- {sqlalchemy_foundation_kit-0.2.1 → sqlalchemy_foundation_kit-0.4.0}/sqlalchemy_foundation_kit/uow/sqlalchemy.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.3.0...sqlalchemy-foundation-kit-v0.4.0) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* measure the wait for a pool connection, and count pool checkout timeouts ([#27](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/27)) ([78c9d7e](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/78c9d7e2c05052090276bce08219047849cd6dbb))
|
|
9
|
+
|
|
10
|
+
## [0.3.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.2.1...sqlalchemy-foundation-kit-v0.3.0) (2026-09-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* stop sending jit and search_path as startup parameters ([#24](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/24)) ([704c343](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/704c343b932b15f0c6ee434f2fc80ab4732b0f80))
|
|
16
|
+
|
|
3
17
|
## [0.2.1](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.2.0...sqlalchemy-foundation-kit-v0.2.1) (2026-09-06)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlalchemy-foundation-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Foundation layer for SQLAlchemy-based services with UoW, session management, and observability
|
|
5
5
|
Project-URL: Repository, https://github.com/bedrock-python/sqlalchemy-foundation-kit
|
|
6
6
|
Project-URL: Documentation, https://bedrock-python.github.io/sqlalchemy-foundation-kit/
|
|
@@ -295,7 +295,7 @@ Only `sqlalchemy[asyncio]`, `pydantic` and `asyncpg` are required by default —
|
|
|
295
295
|
✅ **Single dependency** — All foundation pieces in one place
|
|
296
296
|
✅ **Unit of Work pattern** — Transactional consistency with automatic commit/rollback
|
|
297
297
|
✅ **Connection pool management** — `AsyncSessionManager` with metrics and health checks
|
|
298
|
-
✅ **
|
|
298
|
+
✅ **PgBouncer compatible** — A startup packet PgBouncer accepts, `search_path` per transaction, unique statement names
|
|
299
299
|
✅ **Observability built-in** — Prometheus metrics + OpenTelemetry tracing
|
|
300
300
|
✅ **Type-safe configuration** — Pydantic settings with validation
|
|
301
301
|
✅ **Base ORM models** — Pre-configured `Base` with naming conventions and mixins
|
|
@@ -398,7 +398,7 @@ class PostgresConfig:
|
|
|
398
398
|
application_name: str = "my-service"
|
|
399
399
|
db_schema: str | None = None
|
|
400
400
|
use_orjson_serialization: bool = True
|
|
401
|
-
jit: str | None =
|
|
401
|
+
jit: str | None = None
|
|
402
402
|
|
|
403
403
|
def to_dsn(self) -> str:
|
|
404
404
|
return f"postgresql+asyncpg://{self.connection.user}@{self.connection.host}:{self.connection.port}/{self.connection.database}"
|
|
@@ -494,7 +494,7 @@ from sqlalchemy_foundation_kit.contrib.settings import (
|
|
|
494
494
|
from sqlalchemy_foundation_kit.contrib.metrics import PostgresMetrics
|
|
495
495
|
```
|
|
496
496
|
- `PostgresMetrics` — Prometheus metrics for connection pool
|
|
497
|
-
- Tracks: pool size, checked out connections, checkout duration, errors
|
|
497
|
+
- Tracks: pool size, checked out connections, checkout wait, held duration, timeouts, errors
|
|
498
498
|
|
|
499
499
|
#### `contrib.di` (requires `[dishka]`)
|
|
500
500
|
```python
|
|
@@ -44,7 +44,7 @@ Only `sqlalchemy[asyncio]`, `pydantic` and `asyncpg` are required by default —
|
|
|
44
44
|
✅ **Single dependency** — All foundation pieces in one place
|
|
45
45
|
✅ **Unit of Work pattern** — Transactional consistency with automatic commit/rollback
|
|
46
46
|
✅ **Connection pool management** — `AsyncSessionManager` with metrics and health checks
|
|
47
|
-
✅ **
|
|
47
|
+
✅ **PgBouncer compatible** — A startup packet PgBouncer accepts, `search_path` per transaction, unique statement names
|
|
48
48
|
✅ **Observability built-in** — Prometheus metrics + OpenTelemetry tracing
|
|
49
49
|
✅ **Type-safe configuration** — Pydantic settings with validation
|
|
50
50
|
✅ **Base ORM models** — Pre-configured `Base` with naming conventions and mixins
|
|
@@ -147,7 +147,7 @@ class PostgresConfig:
|
|
|
147
147
|
application_name: str = "my-service"
|
|
148
148
|
db_schema: str | None = None
|
|
149
149
|
use_orjson_serialization: bool = True
|
|
150
|
-
jit: str | None =
|
|
150
|
+
jit: str | None = None
|
|
151
151
|
|
|
152
152
|
def to_dsn(self) -> str:
|
|
153
153
|
return f"postgresql+asyncpg://{self.connection.user}@{self.connection.host}:{self.connection.port}/{self.connection.database}"
|
|
@@ -243,7 +243,7 @@ from sqlalchemy_foundation_kit.contrib.settings import (
|
|
|
243
243
|
from sqlalchemy_foundation_kit.contrib.metrics import PostgresMetrics
|
|
244
244
|
```
|
|
245
245
|
- `PostgresMetrics` — Prometheus metrics for connection pool
|
|
246
|
-
- Tracks: pool size, checked out connections, checkout duration, errors
|
|
246
|
+
- Tracks: pool size, checked out connections, checkout wait, held duration, timeouts, errors
|
|
247
247
|
|
|
248
248
|
#### `contrib.di` (requires `[dishka]`)
|
|
249
249
|
```python
|
|
@@ -31,6 +31,7 @@ from .config import (
|
|
|
31
31
|
# Protocols
|
|
32
32
|
from .protocols import (
|
|
33
33
|
CheckoutRecorder,
|
|
34
|
+
CheckoutWaitRecorder,
|
|
34
35
|
ErrorRecorder,
|
|
35
36
|
PoolStatsRecorder,
|
|
36
37
|
PostgresMetricsProtocol,
|
|
@@ -64,7 +65,7 @@ from .uow import (
|
|
|
64
65
|
try:
|
|
65
66
|
__version__ = version("sqlalchemy-foundation-kit")
|
|
66
67
|
except PackageNotFoundError: # pragma: no cover
|
|
67
|
-
__version__ = "0.
|
|
68
|
+
__version__ = "0.4.0"
|
|
68
69
|
|
|
69
70
|
__all__ = [ # noqa: RUF022
|
|
70
71
|
# Base ORM
|
|
@@ -89,6 +90,7 @@ __all__ = [ # noqa: RUF022
|
|
|
89
90
|
"QuerySettingsProtocol",
|
|
90
91
|
# Protocols
|
|
91
92
|
"CheckoutRecorder",
|
|
93
|
+
"CheckoutWaitRecorder",
|
|
92
94
|
"ErrorRecorder",
|
|
93
95
|
"PoolStatsRecorder",
|
|
94
96
|
"PostgresMetricsProtocol",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -131,9 +131,9 @@ class PostgresSettingsProtocol(Protocol):
|
|
|
131
131
|
pool: Connection pool settings.
|
|
132
132
|
query: Query execution and transaction settings.
|
|
133
133
|
application_name: Application identifier for connections.
|
|
134
|
-
db_schema: Optional PostgreSQL
|
|
134
|
+
db_schema: Optional PostgreSQL ``search_path``, applied to every transaction.
|
|
135
135
|
use_orjson_serialization: Enable orjson for JSON operations.
|
|
136
|
-
jit: JIT compilation setting
|
|
136
|
+
jit: JIT compilation setting, sent as a startup parameter only when not ``None``.
|
|
137
137
|
|
|
138
138
|
Examples:
|
|
139
139
|
Implementing the protocol:
|
|
@@ -144,7 +144,7 @@ class PostgresSettingsProtocol(Protocol):
|
|
|
144
144
|
... application_name: str = "my-app"
|
|
145
145
|
... db_schema: str | None = None
|
|
146
146
|
... use_orjson_serialization: bool = True
|
|
147
|
-
... jit: str | None =
|
|
147
|
+
... jit: str | None = None
|
|
148
148
|
...
|
|
149
149
|
... def to_dsn(self) -> str:
|
|
150
150
|
... return f"postgresql://{self.connection.user}@{self.connection.host}..."
|
|
@@ -11,7 +11,7 @@ try:
|
|
|
11
11
|
except ImportError:
|
|
12
12
|
HAS_PROMETHEUS = False
|
|
13
13
|
|
|
14
|
-
# Default buckets for connection
|
|
14
|
+
# Default buckets for how long a connection was held (seconds)
|
|
15
15
|
CONNECTION_CHECKOUT_BUCKETS: tuple[float, ...] = (
|
|
16
16
|
0.001,
|
|
17
17
|
0.005,
|
|
@@ -26,6 +26,25 @@ CONNECTION_CHECKOUT_BUCKETS: tuple[float, ...] = (
|
|
|
26
26
|
5.0,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
# Default buckets for the wait for a connection from the pool (seconds). The range runs to
|
|
30
|
+
# 30 s, the default ``pool_timeout``, so a saturated pool fills the top buckets instead of
|
|
31
|
+
# collapsing into +Inf just when the histogram is worth reading.
|
|
32
|
+
CONNECTION_WAIT_BUCKETS: tuple[float, ...] = (
|
|
33
|
+
0.001,
|
|
34
|
+
0.005,
|
|
35
|
+
0.01,
|
|
36
|
+
0.025,
|
|
37
|
+
0.05,
|
|
38
|
+
0.1,
|
|
39
|
+
0.25,
|
|
40
|
+
0.5,
|
|
41
|
+
1.0,
|
|
42
|
+
2.5,
|
|
43
|
+
5.0,
|
|
44
|
+
10.0,
|
|
45
|
+
30.0,
|
|
46
|
+
)
|
|
47
|
+
|
|
29
48
|
|
|
30
49
|
def _check_prometheus() -> None:
|
|
31
50
|
"""Check if prometheus-client is installed."""
|
|
@@ -43,12 +62,19 @@ class PostgresMetrics:
|
|
|
43
62
|
- postgres_db_pool_size: Current database connection pool size.
|
|
44
63
|
- postgres_db_pool_checked_out: Number of connections currently checked out.
|
|
45
64
|
- postgres_db_pool_overflow: Number of connections over pool_size (within max_overflow).
|
|
46
|
-
-
|
|
65
|
+
- postgres_db_connection_checkout_wait_seconds: Time a caller waited for a connection.
|
|
66
|
+
- postgres_db_connection_held_duration_seconds: Time a connection was held by its caller.
|
|
67
|
+
- postgres_db_connection_checkout_duration_seconds: Deprecated alias of the held duration,
|
|
68
|
+
published unchanged for one more minor release.
|
|
47
69
|
- postgres_db_connection_timeouts_total: Number of connection checkout timeouts.
|
|
48
70
|
- postgres_db_connection_errors_total: Number of connection errors.
|
|
49
71
|
|
|
50
72
|
Labels:
|
|
51
73
|
- error_type: Type of connection error (for errors_total).
|
|
74
|
+
|
|
75
|
+
The wait histogram and the timeout counter are fed by the instrumented pool class the
|
|
76
|
+
session manager builds, so they only move on an engine created by
|
|
77
|
+
``AsyncSessionManager`` (or one whose pool went through ``instrument_pool_class``).
|
|
52
78
|
"""
|
|
53
79
|
|
|
54
80
|
def __init__(self, prefix: str | None = None) -> None:
|
|
@@ -74,9 +100,19 @@ class PostgresMetrics:
|
|
|
74
100
|
_make_metric_name("postgres_db_pool_overflow", prefix),
|
|
75
101
|
"Number of connections over pool_size (within max_overflow)",
|
|
76
102
|
)
|
|
103
|
+
self.connection_checkout_wait = Histogram(
|
|
104
|
+
_make_metric_name("postgres_db_connection_checkout_wait_seconds", prefix),
|
|
105
|
+
"Time a caller waited to acquire a connection from the pool",
|
|
106
|
+
buckets=list(CONNECTION_WAIT_BUCKETS),
|
|
107
|
+
)
|
|
108
|
+
self.connection_held_duration = Histogram(
|
|
109
|
+
_make_metric_name("postgres_db_connection_held_duration_seconds", prefix),
|
|
110
|
+
"Time a connection was held by its caller, from checkout to checkin",
|
|
111
|
+
buckets=list(CONNECTION_CHECKOUT_BUCKETS),
|
|
112
|
+
)
|
|
77
113
|
self.connection_checkout_duration = Histogram(
|
|
78
114
|
_make_metric_name("postgres_db_connection_checkout_duration_seconds", prefix),
|
|
79
|
-
"
|
|
115
|
+
"Deprecated: held duration, published under its old name. Use postgres_db_connection_held_duration_seconds",
|
|
80
116
|
buckets=list(CONNECTION_CHECKOUT_BUCKETS),
|
|
81
117
|
)
|
|
82
118
|
self.connection_timeouts_total = Counter(
|
|
@@ -104,9 +140,29 @@ class PostgresMetrics:
|
|
|
104
140
|
self,
|
|
105
141
|
duration: float,
|
|
106
142
|
) -> None:
|
|
107
|
-
"""Record a
|
|
143
|
+
"""Record how long a connection was held, from checkout to checkin.
|
|
144
|
+
|
|
145
|
+
Observed into ``postgres_db_connection_held_duration_seconds`` and, until the
|
|
146
|
+
deprecated name is dropped, into ``postgres_db_connection_checkout_duration_seconds``
|
|
147
|
+
as well.
|
|
148
|
+
"""
|
|
149
|
+
self.connection_held_duration.observe(duration)
|
|
108
150
|
self.connection_checkout_duration.observe(duration)
|
|
109
151
|
|
|
152
|
+
def record_checkout_wait(
|
|
153
|
+
self,
|
|
154
|
+
duration: float,
|
|
155
|
+
timed_out: bool = False,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Record the wait for a connection from the pool, and count a pool timeout.
|
|
158
|
+
|
|
159
|
+
A pool checkout timeout is raised before any DBAPI call, so it never reaches the
|
|
160
|
+
engine's ``handle_error`` listener; this is the only place it is counted.
|
|
161
|
+
"""
|
|
162
|
+
self.connection_checkout_wait.observe(duration)
|
|
163
|
+
if timed_out:
|
|
164
|
+
self.connection_timeouts_total.inc()
|
|
165
|
+
|
|
110
166
|
def record_error(
|
|
111
167
|
self,
|
|
112
168
|
error_type: str,
|
|
@@ -98,9 +98,12 @@ class BasePostgresConfig(BaseSettings):
|
|
|
98
98
|
pool: Connection pool configuration (size, overflow, timeouts).
|
|
99
99
|
query: Query execution settings (echo, caching, isolation level).
|
|
100
100
|
application_name: Application name for connection identification.
|
|
101
|
-
db_schema: Optional PostgreSQL schema
|
|
101
|
+
db_schema: Optional PostgreSQL ``search_path`` — a schema, or a comma-separated
|
|
102
|
+
list — applied to every transaction with ``SET LOCAL`` semantics.
|
|
102
103
|
use_orjson_serialization: Use orjson for JSON serialization (requires orjson).
|
|
103
|
-
jit: JIT compilation setting (off/on)
|
|
104
|
+
jit: JIT compilation setting (off/on), sent as a startup parameter only when set.
|
|
105
|
+
``None`` (the default) sends nothing and leaves the server's own setting; a
|
|
106
|
+
transaction-mode PgBouncer rejects the parameter unless it tracks it.
|
|
104
107
|
metrics_enabled: Enable connection pool metrics collection.
|
|
105
108
|
|
|
106
109
|
Examples:
|
|
@@ -125,12 +128,18 @@ class BasePostgresConfig(BaseSettings):
|
|
|
125
128
|
|
|
126
129
|
# Top-level settings
|
|
127
130
|
application_name: str = Field(description="Application name for PostgreSQL")
|
|
128
|
-
db_schema: str | None = Field(
|
|
131
|
+
db_schema: str | None = Field(
|
|
132
|
+
default=None,
|
|
133
|
+
description="PostgreSQL search_path (a schema or a comma-separated list), applied to every transaction",
|
|
134
|
+
)
|
|
129
135
|
use_orjson_serialization: bool = Field(
|
|
130
136
|
default=True,
|
|
131
137
|
description="Use orjson for JSON serialization (requires orjson installed)",
|
|
132
138
|
)
|
|
133
|
-
jit: PostgresJit | None = Field(
|
|
139
|
+
jit: PostgresJit | None = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="JIT setting (off/on), sent as a startup parameter only when set",
|
|
142
|
+
)
|
|
134
143
|
metrics_enabled: bool = Field(default=False, description="Enable PostgreSQL metrics")
|
|
135
144
|
|
|
136
145
|
def __repr__(self) -> str:
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from .metrics import (
|
|
10
10
|
CheckoutRecorder,
|
|
11
|
+
CheckoutWaitRecorder,
|
|
11
12
|
ErrorRecorder,
|
|
12
13
|
PoolStatsRecorder,
|
|
13
14
|
PostgresMetricsProtocol,
|
|
@@ -15,6 +16,7 @@ from .metrics import (
|
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
"CheckoutRecorder",
|
|
19
|
+
"CheckoutWaitRecorder",
|
|
18
20
|
"ErrorRecorder",
|
|
19
21
|
"PoolStatsRecorder",
|
|
20
22
|
"PostgresMetricsProtocol",
|
|
@@ -7,7 +7,7 @@ the narrow protocols selectively (e.g., only record errors), and
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from typing import Protocol
|
|
10
|
+
from typing import Protocol, runtime_checkable
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class PoolStatsRecorder(Protocol):
|
|
@@ -30,13 +30,39 @@ class PoolStatsRecorder(Protocol):
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class CheckoutRecorder(Protocol):
|
|
33
|
-
"""Capability protocol for recording connection
|
|
33
|
+
"""Capability protocol for recording how long a connection was held."""
|
|
34
34
|
|
|
35
35
|
def record_checkout(self, duration: float) -> None:
|
|
36
|
-
"""Record a database connection checkout
|
|
36
|
+
"""Record a completed database connection checkout.
|
|
37
37
|
|
|
38
38
|
Args:
|
|
39
|
-
duration: Time
|
|
39
|
+
duration: Time between the connection leaving the pool and coming back to it,
|
|
40
|
+
in seconds — how long the caller *held* it, which is the query time seen
|
|
41
|
+
from the pool. The time a caller spent *waiting* for it is
|
|
42
|
+
:class:`CheckoutWaitRecorder`.
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class CheckoutWaitRecorder(Protocol):
|
|
49
|
+
"""Capability protocol for recording the wait for a connection from the pool.
|
|
50
|
+
|
|
51
|
+
Deliberately not part of :class:`PostgresMetricsProtocol`: a metrics object written
|
|
52
|
+
against the three older capabilities stays valid and simply publishes no wait series.
|
|
53
|
+
The session manager tests for this protocol with ``isinstance`` and instruments the
|
|
54
|
+
pool class only when it is satisfied, which is what ``runtime_checkable`` is for here.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def record_checkout_wait(self, duration: float, timed_out: bool = False) -> None:
|
|
58
|
+
"""Record the time a caller spent acquiring a connection from the pool.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
duration: Seconds spent inside ``pool.connect()`` — the queue wait, and the
|
|
62
|
+
pre-ping round trip and connect handshake when the pool had to grow.
|
|
63
|
+
timed_out: True if the wait ended in ``sqlalchemy.exc.TimeoutError`` rather
|
|
64
|
+
than in a connection. This is the pool checkout timeout; it is raised
|
|
65
|
+
before any DBAPI call and so never reaches the engine's ``handle_error``.
|
|
40
66
|
"""
|
|
41
67
|
...
|
|
42
68
|
|
|
@@ -60,10 +86,15 @@ class PostgresMetricsProtocol(PoolStatsRecorder, CheckoutRecorder, ErrorRecorder
|
|
|
60
86
|
Aggregates the narrow capability protocols for convenience. Implementations
|
|
61
87
|
that only need a subset can implement the individual protocols directly.
|
|
62
88
|
|
|
89
|
+
:class:`CheckoutWaitRecorder` is intentionally left out so that implementations
|
|
90
|
+
written before it keep satisfying this protocol; add ``record_checkout_wait`` to get
|
|
91
|
+
the checkout wait histogram and the pool timeout counter as well.
|
|
92
|
+
|
|
63
93
|
Examples:
|
|
64
94
|
>>> class MyMetrics:
|
|
65
95
|
... def record_pool_stats(self, pool_size: int, pool_checked_out: int, pool_overflow: int) -> None: ...
|
|
66
96
|
... def record_checkout(self, duration: float) -> None: ...
|
|
97
|
+
... def record_checkout_wait(self, duration: float, timed_out: bool = False) -> None: ...
|
|
67
98
|
... def record_error(self, error_type: str, is_timeout: bool = False) -> None: ...
|
|
68
99
|
>>> metrics: PostgresMetricsProtocol = MyMetrics()
|
|
69
100
|
"""
|
|
@@ -71,6 +102,7 @@ class PostgresMetricsProtocol(PoolStatsRecorder, CheckoutRecorder, ErrorRecorder
|
|
|
71
102
|
|
|
72
103
|
__all__ = [
|
|
73
104
|
"CheckoutRecorder",
|
|
105
|
+
"CheckoutWaitRecorder",
|
|
74
106
|
"ErrorRecorder",
|
|
75
107
|
"PoolStatsRecorder",
|
|
76
108
|
"PostgresMetricsProtocol",
|
|
@@ -77,6 +77,7 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
|
|
|
77
77
|
self._metrics: PostgresMetricsProtocol | None = None
|
|
78
78
|
self._on_engine_created: Callable[[AsyncEngine], None] | None = None
|
|
79
79
|
self._dispose_timeout: float | None = None
|
|
80
|
+
self._search_path: str | None = None
|
|
80
81
|
self._extra_kwargs: dict[str, object] = {}
|
|
81
82
|
|
|
82
83
|
def with_echo(self, echo: bool = True) -> AsyncSessionManagerBuilder[SessionT]:
|
|
@@ -258,6 +259,22 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
|
|
|
258
259
|
self._dispose_timeout = timeout
|
|
259
260
|
return self
|
|
260
261
|
|
|
262
|
+
def with_search_path(self, search_path: str) -> AsyncSessionManagerBuilder[SessionT]:
|
|
263
|
+
"""Apply a PostgreSQL ``search_path`` to every transaction.
|
|
264
|
+
|
|
265
|
+
Issued with ``SET LOCAL`` semantics at the start of each transaction — the one
|
|
266
|
+
scope a transaction-mode pooler such as PgBouncer honours. See
|
|
267
|
+
:func:`~sqlalchemy_foundation_kit.session.manager.attach_search_path`.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
search_path: A schema, or a comma-separated list such as ``"tenant_7, public"``.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Self for method chaining.
|
|
274
|
+
"""
|
|
275
|
+
self._search_path = search_path
|
|
276
|
+
return self
|
|
277
|
+
|
|
261
278
|
def build(self) -> AsyncSessionManager[SessionT]:
|
|
262
279
|
"""Build AsyncSessionManager instance with configured parameters.
|
|
263
280
|
|
|
@@ -284,6 +301,7 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
|
|
|
284
301
|
"use_orjson": self._use_orjson,
|
|
285
302
|
"metrics": self._metrics,
|
|
286
303
|
"on_engine_created": self._on_engine_created,
|
|
304
|
+
"search_path": self._search_path,
|
|
287
305
|
}
|
|
288
306
|
if self._dispose_timeout is not None:
|
|
289
307
|
kwargs["dispose_timeout"] = self._dispose_timeout
|
|
@@ -31,6 +31,13 @@ def create_async_session_manager(
|
|
|
31
31
|
) -> AsyncSessionManager[AsyncSession]:
|
|
32
32
|
"""Create async session manager with PostgreSQL-specific configuration.
|
|
33
33
|
|
|
34
|
+
The startup packet carries ``application_name`` and, only when the config sets it,
|
|
35
|
+
``jit``; nothing else, because a transaction-mode pooler such as PgBouncer rejects
|
|
36
|
+
startup parameters it does not track (``unsupported startup parameter: jit``) or,
|
|
37
|
+
with ``ignore_startup_parameters``, silently drops them. ``db_schema`` therefore
|
|
38
|
+
travels as the manager's ``search_path`` and is applied with ``SET LOCAL`` semantics
|
|
39
|
+
at the start of every transaction, which is the one scope such a pooler honours.
|
|
40
|
+
|
|
34
41
|
Args:
|
|
35
42
|
postgres_config: PostgreSQL configuration implementing PostgresSettingsProtocol.
|
|
36
43
|
application_name: Optional custom application name. If None, uses postgres_config.application_name.
|
|
@@ -41,7 +48,8 @@ def create_async_session_manager(
|
|
|
41
48
|
which provides pgbouncer transaction-mode compatibility.
|
|
42
49
|
extra_server_settings: Additional PostgreSQL ``server_settings`` to merge with defaults
|
|
43
50
|
(e.g., ``{"statement_timeout": "30000", "timezone": "UTC"}``). User-provided keys
|
|
44
|
-
override library defaults.
|
|
51
|
+
override library defaults. These are startup parameters: through PgBouncer only
|
|
52
|
+
the ones it tracks arrive (``track_extra_parameters``).
|
|
45
53
|
extra_connect_args: Additional asyncpg ``connect_args`` to merge with defaults
|
|
46
54
|
(e.g., ``{"command_timeout": 60}``). User-provided keys override library defaults.
|
|
47
55
|
**kwargs: Additional keyword arguments passed to AsyncSessionManager.
|
|
@@ -73,11 +81,11 @@ def create_async_session_manager(
|
|
|
73
81
|
"""
|
|
74
82
|
app_name = application_name or postgres_config.application_name
|
|
75
83
|
|
|
76
|
-
# Build server settings with optional overrides
|
|
84
|
+
# Build server settings with optional overrides. These are startup parameters, so
|
|
85
|
+
# search_path is deliberately not among them -- see the docstring.
|
|
77
86
|
server_settings: dict[str, str] = {
|
|
78
87
|
"application_name": app_name,
|
|
79
88
|
**({"jit": postgres_config.jit} if postgres_config.jit is not None else {}),
|
|
80
|
-
**({"search_path": postgres_config.db_schema} if postgres_config.db_schema is not None else {}),
|
|
81
89
|
**(extra_server_settings or {}),
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -100,5 +108,6 @@ def create_async_session_manager(
|
|
|
100
108
|
use_orjson=postgres_config.use_orjson_serialization,
|
|
101
109
|
metrics=metrics,
|
|
102
110
|
on_engine_created=on_engine_created,
|
|
111
|
+
search_path=postgres_config.db_schema,
|
|
103
112
|
**kwargs,
|
|
104
113
|
)
|
|
@@ -10,7 +10,7 @@ from contextlib import asynccontextmanager
|
|
|
10
10
|
from types import TracebackType
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Generic, cast
|
|
12
12
|
|
|
13
|
-
from sqlalchemy import event
|
|
13
|
+
from sqlalchemy import event, text
|
|
14
14
|
from sqlalchemy.exc import TimeoutError as SATimeoutError
|
|
15
15
|
from sqlalchemy.ext.asyncio import (
|
|
16
16
|
AsyncEngine,
|
|
@@ -21,8 +21,11 @@ from sqlalchemy.ext.asyncio import (
|
|
|
21
21
|
|
|
22
22
|
from .._typing import SessionT
|
|
23
23
|
from ..base import build_engine_kwargs, resolve_pool_class
|
|
24
|
+
from ..protocols import CheckoutWaitRecorder
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
27
|
+
from sqlalchemy.engine import Connection
|
|
28
|
+
|
|
26
29
|
from ..config import PoolSettingsProtocol
|
|
27
30
|
from ..protocols import PostgresMetricsProtocol
|
|
28
31
|
|
|
@@ -30,6 +33,10 @@ logger = logging.getLogger(__name__)
|
|
|
30
33
|
|
|
31
34
|
DEFAULT_DISPOSE_TIMEOUT_SECONDS: float = 30.0
|
|
32
35
|
|
|
36
|
+
# Set on the pool subclass instrument_pool_class() builds, so attach_metrics() can tell an
|
|
37
|
+
# engine whose pool records the checkout wait from one that silently will not.
|
|
38
|
+
_WAIT_RECORDER_ATTRIBUTE = "_checkout_wait_recorder"
|
|
39
|
+
|
|
33
40
|
|
|
34
41
|
def _safe_metric_call(func: Callable[[], None], error_msg: str) -> None:
|
|
35
42
|
"""Call a metric-recording function and swallow exceptions.
|
|
@@ -47,18 +54,94 @@ def _safe_metric_call(func: Callable[[], None], error_msg: str) -> None:
|
|
|
47
54
|
logger.exception(error_msg)
|
|
48
55
|
|
|
49
56
|
|
|
57
|
+
def instrument_pool_class(pool_class: type, metrics: CheckoutWaitRecorder) -> type:
|
|
58
|
+
"""Build a pool class that times the wait for a connection and counts pool timeouts.
|
|
59
|
+
|
|
60
|
+
SQLAlchemy has no event for "a checkout was requested", only for one that succeeded, so
|
|
61
|
+
the time a caller spends queued for a connection cannot be observed from a listener.
|
|
62
|
+
The returned subclass wraps :meth:`sqlalchemy.pool.Pool.connect`, which runs exactly
|
|
63
|
+
once per checkout — for a sync and an async engine alike, since
|
|
64
|
+
``Engine.raw_connection()`` goes through it — and through which the pool's
|
|
65
|
+
``TimeoutError`` passes exactly once. Under an async pool the queue wait is an
|
|
66
|
+
``await_only`` inside that call, so the timer spans the whole suspension and measures
|
|
67
|
+
the time the caller really waited.
|
|
68
|
+
|
|
69
|
+
``_do_get`` is the wrong hook: ``QueuePool`` recurses into it when the non-blocking get
|
|
70
|
+
comes back empty and the race for an overflow slot is lost, which is exactly the
|
|
71
|
+
contended case worth measuring, and the wait would be observed twice.
|
|
72
|
+
|
|
73
|
+
The recorder lives on the class, so a pool produced by ``recreate()`` after
|
|
74
|
+
``dispose()`` keeps recording.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
pool_class: Pool class to instrument — anything :func:`resolve_pool_class` returns,
|
|
78
|
+
including a class registered with :func:`register_pool_class`.
|
|
79
|
+
metrics: Recorder to call once per checkout. A raising recorder is logged and
|
|
80
|
+
swallowed; the checkout itself is never affected.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A subclass of ``pool_class`` suitable for ``create_async_engine(poolclass=...)``.
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
>>> poolclass = instrument_pool_class(resolve_pool_class("async_adapted_queue"), metrics)
|
|
87
|
+
>>> engine = create_async_engine(url, poolclass=poolclass)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
class InstrumentedPool(pool_class): # type: ignore[misc, valid-type]
|
|
91
|
+
"""Pool that reports how long each checkout waited."""
|
|
92
|
+
|
|
93
|
+
def connect(self) -> Any:
|
|
94
|
+
started = time.perf_counter()
|
|
95
|
+
try:
|
|
96
|
+
connection = super().connect()
|
|
97
|
+
except SATimeoutError:
|
|
98
|
+
_safe_metric_call(
|
|
99
|
+
lambda: metrics.record_checkout_wait(
|
|
100
|
+
duration=time.perf_counter() - started,
|
|
101
|
+
timed_out=True,
|
|
102
|
+
),
|
|
103
|
+
"Failed to record database checkout timeout",
|
|
104
|
+
)
|
|
105
|
+
raise
|
|
106
|
+
_safe_metric_call(
|
|
107
|
+
lambda: metrics.record_checkout_wait(duration=time.perf_counter() - started),
|
|
108
|
+
"Failed to record database checkout wait",
|
|
109
|
+
)
|
|
110
|
+
return connection
|
|
111
|
+
|
|
112
|
+
setattr(InstrumentedPool, _WAIT_RECORDER_ATTRIBUTE, metrics)
|
|
113
|
+
InstrumentedPool.__name__ = f"Instrumented{pool_class.__name__}"
|
|
114
|
+
InstrumentedPool.__qualname__ = InstrumentedPool.__name__
|
|
115
|
+
return InstrumentedPool
|
|
116
|
+
|
|
117
|
+
|
|
50
118
|
def attach_metrics(engine: AsyncEngine, metrics: PostgresMetricsProtocol) -> None:
|
|
51
119
|
"""Attach metrics event listeners to a SQLAlchemy engine.
|
|
52
120
|
|
|
53
121
|
Registers event handlers for connection checkout, checkin, and error events
|
|
54
122
|
to collect pool statistics and connection metrics.
|
|
55
123
|
|
|
124
|
+
The checkout wait and the pool checkout timeout are **not** listeners — no pool event
|
|
125
|
+
fires for either — and come from the pool class :func:`instrument_pool_class` builds.
|
|
126
|
+
If ``metrics`` records the wait but the engine's pool was built without it, that is
|
|
127
|
+
logged as a warning rather than left as a series that never moves.
|
|
128
|
+
|
|
56
129
|
Args:
|
|
57
130
|
engine: SQLAlchemy ``AsyncEngine`` to attach listeners to.
|
|
58
131
|
metrics: Metrics collector implementing ``PostgresMetricsProtocol``.
|
|
59
132
|
"""
|
|
60
133
|
pool = engine.pool
|
|
61
134
|
|
|
135
|
+
if isinstance(metrics, CheckoutWaitRecorder) and getattr(type(pool), _WAIT_RECORDER_ATTRIBUTE, None) is None:
|
|
136
|
+
logger.warning(
|
|
137
|
+
"%s records the connection checkout wait, but the engine's pool (%s) was not built by "
|
|
138
|
+
"instrument_pool_class(), so no wait and no pool checkout timeout will ever be recorded. "
|
|
139
|
+
"Build the engine through AsyncSessionManager, or pass "
|
|
140
|
+
"instrument_pool_class(pool_class, metrics) as poolclass to create_async_engine().",
|
|
141
|
+
type(metrics).__name__,
|
|
142
|
+
type(pool).__name__,
|
|
143
|
+
)
|
|
144
|
+
|
|
62
145
|
def record_pool_stats() -> None:
|
|
63
146
|
_safe_metric_call(
|
|
64
147
|
lambda: metrics.record_pool_stats(
|
|
@@ -100,6 +183,35 @@ def attach_metrics(engine: AsyncEngine, metrics: PostgresMetricsProtocol) -> Non
|
|
|
100
183
|
event.listen(engine.sync_engine, "handle_error", on_error)
|
|
101
184
|
|
|
102
185
|
|
|
186
|
+
def attach_search_path(engine: AsyncEngine, search_path: str) -> None:
|
|
187
|
+
"""Apply ``search_path`` to every transaction the engine begins.
|
|
188
|
+
|
|
189
|
+
Registers a ``begin`` listener that issues ``set_config('search_path', …, true)`` —
|
|
190
|
+
the function form of ``SET LOCAL``, with the value as a bind parameter — as the first
|
|
191
|
+
statement of each transaction, so the setting lives exactly as long as the transaction
|
|
192
|
+
does. That is the one scope a transaction-mode pooler such as PgBouncer honours: a
|
|
193
|
+
startup parameter is rejected or dropped before it reaches PostgreSQL, and a plain
|
|
194
|
+
``SET`` on a server connection leaks to whichever client is handed it next.
|
|
195
|
+
|
|
196
|
+
Under the asyncpg adapter ``BEGIN`` is sent lazily with the first statement, so the
|
|
197
|
+
``set_config`` call lands inside the transaction rather than ahead of it. Every
|
|
198
|
+
transaction is covered — a session's autobegin, ``session.begin()``, a raw
|
|
199
|
+
``engine.connect()``, the one that follows a ``commit()`` — but a statement run with
|
|
200
|
+
``isolation_level="AUTOCOMMIT"`` begins none and runs with the server's default.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
engine: SQLAlchemy ``AsyncEngine`` to attach the listener to.
|
|
204
|
+
search_path: Value for ``search_path`` — a schema, or a comma-separated list such
|
|
205
|
+
as ``"tenant_7, public"``.
|
|
206
|
+
"""
|
|
207
|
+
statement = text("SELECT set_config('search_path', :search_path, true)")
|
|
208
|
+
|
|
209
|
+
def on_begin(conn: Connection) -> None:
|
|
210
|
+
conn.execute(statement, {"search_path": search_path})
|
|
211
|
+
|
|
212
|
+
event.listen(engine.sync_engine, "begin", on_begin)
|
|
213
|
+
|
|
214
|
+
|
|
103
215
|
class AsyncSessionManager(Generic[SessionT]):
|
|
104
216
|
"""Manages async database sessions with configurable connection pooling.
|
|
105
217
|
|
|
@@ -124,6 +236,7 @@ class AsyncSessionManager(Generic[SessionT]):
|
|
|
124
236
|
metrics: PostgresMetricsProtocol | None = None,
|
|
125
237
|
on_engine_created: Callable[[AsyncEngine], None] | None = None,
|
|
126
238
|
dispose_timeout: float = DEFAULT_DISPOSE_TIMEOUT_SECONDS,
|
|
239
|
+
search_path: str | None = None,
|
|
127
240
|
**kwargs: object,
|
|
128
241
|
) -> None:
|
|
129
242
|
"""Initialize session manager with direct configuration.
|
|
@@ -139,18 +252,29 @@ class AsyncSessionManager(Generic[SessionT]):
|
|
|
139
252
|
isolation_level: Default transaction isolation level (default: None).
|
|
140
253
|
pool_settings: Pool configuration settings (default: None).
|
|
141
254
|
use_orjson: If True, use orjson for JSON serialization (default: False).
|
|
142
|
-
metrics: Optional metrics collector (default: None).
|
|
255
|
+
metrics: Optional metrics collector (default: None). One that also implements
|
|
256
|
+
``CheckoutWaitRecorder`` gets the pool class wrapped by
|
|
257
|
+
:func:`instrument_pool_class`, which is what feeds the checkout wait and
|
|
258
|
+
the pool checkout timeout — neither is reachable from a pool event.
|
|
143
259
|
on_engine_created: Optional callback invoked with ``AsyncEngine`` after creation.
|
|
144
260
|
Use for OpenTelemetry instrumentation, custom event listeners, etc.
|
|
145
261
|
dispose_timeout: Maximum seconds to wait for engine disposal in :meth:`aclose`
|
|
146
262
|
(default: 30.0). Lower this in tests or short-lived environments; raise it
|
|
147
263
|
if you have long-running transactions that need more time to settle.
|
|
264
|
+
search_path: PostgreSQL ``search_path`` applied to every transaction with
|
|
265
|
+
``SET LOCAL`` semantics, which is what survives a transaction-mode pooler
|
|
266
|
+
(default: None — nothing is sent, the server default applies). See
|
|
267
|
+
:func:`attach_search_path`.
|
|
148
268
|
**kwargs: Additional keyword arguments for ``create_async_engine``.
|
|
149
269
|
"""
|
|
150
270
|
self._closed = False
|
|
151
271
|
self._close_lock = asyncio.Lock()
|
|
152
272
|
self._dispose_timeout = dispose_timeout
|
|
153
273
|
resolved_poolclass = resolve_pool_class(poolclass)
|
|
274
|
+
if metrics is not None and isinstance(metrics, CheckoutWaitRecorder):
|
|
275
|
+
# The wait for a connection is not observable from a pool event, so it has to
|
|
276
|
+
# be timed inside the pool -- which means deciding the class before the engine.
|
|
277
|
+
resolved_poolclass = instrument_pool_class(resolved_poolclass, metrics)
|
|
154
278
|
engine_kwargs = build_engine_kwargs(
|
|
155
279
|
echo=echo,
|
|
156
280
|
poolclass=resolved_poolclass,
|
|
@@ -174,6 +298,9 @@ class AsyncSessionManager(Generic[SessionT]):
|
|
|
174
298
|
if metrics:
|
|
175
299
|
attach_metrics(self._engine, metrics)
|
|
176
300
|
|
|
301
|
+
if search_path is not None:
|
|
302
|
+
attach_search_path(self._engine, search_path)
|
|
303
|
+
|
|
177
304
|
if on_engine_created is not None:
|
|
178
305
|
on_engine_created(self._engine)
|
|
179
306
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|