Repository navigation
Add pgdevkit[fastapi]: streaming Postgres JSON response with query cancellation - #48
Merged
Merged
Conversation
Contributor
Author
|
Implementation plan (for review before coding) AttachmentsClaude Session: c9be081f-307e-54df-85ad-8cc47a4eda79 |
Contributor
Author
|
Plan review amendments (bms-plan-review: go with changes) AttachmentsClaude Session: c9be081f-307e-54df-85ad-8cc47a4eda79 |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… on client disconnect Optional extra pgdevkit[fastapi]. Port of CCMT2's PostgresJsonResponse, with a connection-source API usable from CCMT2, OneSales and MDMApp, true row streaming (cursor.stream), own http.disconnect detection (independent of ASGI spec version) and shielded cancel_safe() so Postgres stops the query. Bump to 0.13.0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… connection handling PostgresJsonResponse now takes (query, params, *, con=, pool=) like fetch_all, reuses its connection acquisition (set_default_pool), query rendering (sqlglot support) and types. Version 0.14.0 (main is at 0.13.0). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nterruptible cancel cleanup, FastAPI >= 0.118, docs - cursor uses tuple_row so a dict_row connection/pool can't corrupt the output - batch_size >= 1 and t-strings are rejected at construction - cancel cleanup runs as its own shielded task: a second (native) cancellation can no longer skip cancel+close - only send() failures count as a client disconnect; HTTPException headers kept - fastapi >= 0.118 (Depends with yield releases after the streaming response); extra = pgdevkit[db] + fastapi - docs: con= semantics, OpenAPI responses=, first-byte timing, expose_errors warning, slow-reader note Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Postgres enforces it (set_config(..., is_local => true)): transaction-scoped, restored on a borrowed connection, never leaks into a pool. fetch_all raises QueryCanceled; the response answers 504 before the first byte. It does not free a stalled reader (documented, verified). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the optional
pgdevkit[fastapi]extra withpgdevkit.fastapi.PostgresJsonResponse: a port of CCMT2'sPostgresJsonResponsethat CCMT2, OneSales and MDMApp can share. It streams a query as a JSON array (Postgres builds the JSON viarow_to_json; rows are streamed withcursor.stream) and cancels the query in Postgres when the client disconnects.fetch_all(con=/pool=/set_default_pool()), reusing its_acquire,_renderand types.http.disconnectwatcher (Starlette only listens on ASGI spec < 2.4), then a shieldedcancel_safe()+ close of a connection left mid-query. Only a still-running query is cancelled, so a late cancel can't hit the next statement on a pooled connection.expose_errors); after the first byte the array stays unterminated so clients can't mistake it for complete.statement_timeout=<seconds>on bothPostgresJsonResponseandfetch_all: Postgres aborts the query (SET LOCALsemantics: transaction-scoped, never leaks into the pool, previous value restored on a borrowedcon).fetch_allraisesQueryCanceled; the response sends a 504 before the first byte. It bounds query execution only: a client that stopped reading keeps its backend (ClientWrite) regardless, so a proxy write timeout is still needed.fastapi >= 0.118(aDependswithyieldonly releases its connection after the streaming response from 0.118).Findings worth knowing (all covered by tests)
pg_sleep(30)staysactive); psycopg only cancels on native asyncio cancellation, not when an anyio scope re-cancels its cleanup.StreamingResponse) makes Granian hang when the body raises mid-stream, so the disconnect watcher uses anasyncio.timeoutscope instead.Test plan
tests/test_pg_json_response.py: output shape (empty / exact batch multiples / multi-batch), params, composables,query_produces_json, headers, default pool,con/pool,dict_rowconnection, validation, errors before/after the first bytetests/test_pg_json_cancel.py: disconnect before first byte / while streaming, task cancellation, failingsend, anyio scope cancellation, a second cancel during cleanup; each verified inpg_stat_activitytests/test_pg_json_granian.py: real Granian server, aborted HTTP client, mid-stream errortests/db/test_fetch_statement_timeout.py,tests/test_pg_json_statement_timeout.py: timeout before/after the first byte, no leak into the pool, previous setting restored, autocommit, validation (mutation-checked)tests/db/test_fetch.py(from Add fetch_all with optional borrowed connection and Pydantic model overload #47) needs Docker and could not run in my sandbox: CINot in this PR
Migrating CCMT2 / OneSales (
backend/db/pg_response.py,notifications.py) / MDMApp call sites, after release. A write timeout for slow readers is documented (proxy + pool sizing), not implemented:statement_timeoutdoesn't cover it.🤖 Generated with Claude Code