This page explains the database layer of Cluster ERP: the Database class in core/database.py, the request session dependency in server/dependencies.py, the request context in core/request_context.py, and the session helper for EventBus handlers in core/handler_session.py. It also explains when a request's transaction commits. Tenant routing is documented at mechanism level in multi-tenancy.
In short¶
Databasewraps one async SQLAlchemy engine on asyncpg and one session factory withexpire_on_commit=False(src/cluster_erp/core/database.py:53-93).get_db_sessiongives each request oneAsyncSession, publishes it in context variables, and rolls it back on error (src/cluster_erp/server/dependencies.py:160-200).- The commit runs before the first response byte is sent, through a pre-response hook (
src/cluster_erp/server/dependencies.py:163-190,src/cluster_erp/server/middleware.py:25-57). - The acting user, the correlation id, and the deactivation reason travel in context variables, not in function arguments (
src/cluster_erp/core/request_context.py:16-61). - An EventBus handler joins the request's session when there is one, else it opens its own (
src/cluster_erp/core/handler_session.py:25-37).
How it works¶
The Database class¶
Database(settings, *, database_url=None, pool_size=None, max_overflow=None) creates the engine with these options (src/cluster_erp/core/database.py:69-80):
| Option | Value | Source |
|---|---|---|
| URL | database_url or settings.database_url (postgresql+asyncpg://...) |
src/cluster_erp/core/database.py:70, src/cluster_erp/config/settings.py:838-843 |
echo |
settings.sql_echo |
src/cluster_erp/core/database.py:71 |
pool_size and max_overflow |
The arguments, else db_pool_size and db_pool_max_overflow |
src/cluster_erp/core/database.py:72-75 |
pool_pre_ping |
True |
src/cluster_erp/core/database.py:76 |
pool_recycle |
3600 seconds | src/cluster_erp/core/database.py:77 |
pool_timeout |
db_pool_timeout_seconds |
src/cluster_erp/core/database.py:78 |
| Server setting | plan_cache_mode = force_custom_plan on every connection |
src/cluster_erp/core/database.py:50, src/cluster_erp/core/database.py:79 |
The tenancy layer builds one Database per tenant database with a smaller pool (src/cluster_erp/core/database.py:62-68). Database.session() is an async generator that commits on success and rolls back on error; start-up passes use it (src/cluster_erp/core/database.py:95-102).
The request session¶
get_db_session is the FastAPI dependency for every route that touches data. It does the following:
- Resolves the request's
Database: the process default in single-tenant mode, the tenant's database in multi-tenant mode (src/cluster_erp/server/dependencies.py:104-157). - Opens a session and publishes it, its task, and its database in three context variables (
src/cluster_erp/server/dependencies.py:182-184,src/cluster_erp/core/database.py:21-37). - Opens the connection and injects the audit context (
src/cluster_erp/server/dependencies.py:186-187). This sets five transaction-local PostgreSQL settings in one statement (app.actor_id,app.ip,app.user_agent,app.session_id,app.correlation_id), and sets the request context variables (src/cluster_erp/server/dependencies.py:34-72). - Registers
_commit_onceas a pre-response hook when the middleware has created the hook list (src/cluster_erp/server/dependencies.py:188-190). - Yields the session to the route, then calls
_commit_onceas a fallback for contexts without the middleware (src/cluster_erp/server/dependencies.py:191-192). - On any exception, marks the hook as done and rolls back (
src/cluster_erp/server/dependencies.py:193-196). - Resets the context variables (
src/cluster_erp/server/dependencies.py:197-200).
_commit_once commits and then emits the events queued for after the commit (src/cluster_erp/server/dependencies.py:171-176, src/cluster_erp/server/dependencies.py:203-222).
Commit timing¶
The sequence diagram shows when the commit happens.
sequenceDiagram participant C as Client participant M as CommitBeforeResponseMiddleware participant R as Route participant S as get_db_session participant DB as PostgreSQL C->>M: HTTP request M->>M: state.pre_response_hooks = [] M->>R: call application R->>S: Depends(get_db_session) S->>S: register _commit_once S-->>R: session R->>DB: INSERT or UPDATE (flush) R-->>M: http.response.start M->>S: run hook _commit_once S->>DB: COMMIT M-->>C: response
In prose: the middleware creates an empty hook list on the request state. The session dependency adds _commit_once to it. When the route's response starts, the middleware runs the hooks before it sends the first message (src/cluster_erp/server/middleware.py:41-57). The commit is therefore complete before the client receives the status code. A commit failure becomes an error response.
Request context variables¶
| Variable | Set by | Read by | Source |
|---|---|---|---|
| acting user id | set_request_context |
change history, audit columns | src/cluster_erp/core/request_context.py:16, src/cluster_erp/core/request_context.py:30-32 |
| correlation id | set_request_context (header X-Correlation-Id, else a new UUID) |
change history | src/cluster_erp/core/request_context.py:17, src/cluster_erp/server/dependencies.py:39-41 |
| deactivation reason and note | set_deactivation_context (headers X-Deact-Reason, X-Deact-Note, URL-decoded) |
change history deactivation stamp | src/cluster_erp/core/request_context.py:21-22, src/cluster_erp/server/dependencies.py:67-72 |
| tenant key | set_tenant_context |
storage keys of attachments | src/cluster_erp/core/request_context.py:27, src/cluster_erp/core/attachments.py:38-44 |
| request session, task, database | get_db_session |
EventBus handlers, ModuleBase.request_database() |
src/cluster_erp/core/database.py:21-37 |
Sessions in EventBus handlers¶
handler_session(database_for) yields the request session when current_request_session is set, flushes it, and does not commit, because the request commits. Outside a request it opens a session on database_for() and commits it when the block ends without an exception (src/cluster_erp/core/handler_session.py:25-37). A comment in core/database.py states that a session belongs to one task; code in another task must open its own session (src/cluster_erp/core/database.py:34-36).
Background work started by a request¶
get_session_factory returns the session factory of the request's tenant database, or the default one, for work a request starts in the background (src/cluster_erp/server/dependencies.py:233-244).
Why it is built this way¶
- The
CommitBeforeResponseMiddlewaredocstring states the reason for the commit timing: FastAPI finalises yield dependencies after the response is sent, so a client could read a record before its insert committed (src/cluster_erp/server/middleware.py:26-36). - The
request_contextdocstring states why the actor travels in context variables: flush events run deep in the write path where the user is not available, and the context avoids module-to-module calls (src/cluster_erp/core/request_context.py:1-9). - A comment states why
plan_cache_modeis forced: generic plans turned trigram searches into sequential scans on some connections (src/cluster_erp/core/database.py:40-49).
Rules and boundaries¶
- Routes get their session from
get_db_session, not fromDatabase.session_factorydirectly. Whether every route follows this is not determined from code; it was not audited for this page. - A failed request never commits from the hook: the exception path sets
done = Truebefore the rollback (src/cluster_erp/server/dependencies.py:193-196). - The project's written engineering rules name PostgreSQL with asyncpg and Alembic migrations. The engine URL uses
postgresql+asyncpg(src/cluster_erp/config/settings.py:838-843).
Interaction with other parts¶
- Change history reads the actor and deactivation context at flush time.
- Request pipeline and middleware places
CommitBeforeResponseMiddlewarein the stack. - Event bus handlers use
handler_sessionandrequest_database. - Migrations are documented in database migrations.
Limits and open points¶
get_languagereads only the first two letters ofAccept-Languageand defaults toen(src/cluster_erp/server/dependencies.py:247-249). Which routes use it was not inventoried for this page.
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| request session | get_db_session |
The one SQLAlchemy session a request uses, committed before the response. | istek oturumu | src/cluster_erp/server/dependencies.py:160 |
| request context | core/request_context.py |
Context variables that carry the actor, correlation id, deactivation reason, and tenant key. | istek bağlamı | src/cluster_erp/core/request_context.py:1-9 |
| pre-response hook | pre_response_hooks |
A callback run before the first response byte is sent. | yanıt öncesi kanca | src/cluster_erp/server/middleware.py:45-55 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.