Waiting, Cancelling, Disconnecting
Real clients wait, give up, vanish and pile up: keep every one of them from hurting the others
Log every parked statement on the server's stdout, keep a waiting PostgreSQL client silent until its statement resumes and serve waiters in order, give a vanished client's locks back, refuse connections over --max-connections with 53300, close sessions idle past --idle-timeout-ms with 57P05, cancel a parked statement from a CancelRequest with the right key and ignore one with a wrong key, and shut down by telling every client 57P01.
This part needs a network, which the platform's test sandbox does not have, so its tests run on your computer. Download the test kit (it needs only Python 3) and your project, build your program, and run:
python3 run.py --stage pg-connection-lifecycle --bin ./c-dbSign in to download the code you wrote here.
- Waiting is just silence on the wire: the client sent a Query and no answer has come yet
- A positive signal on the server's own log is how anyone outside can tell waiting from slow
- Cancelling arrives on a second connection, because the first one is busy waiting
- The secret key: only the client that was given it can cancel its statements
- Limits protect the server: a maximum number of sessions, and a timeout for sessions doing nothing
- A shutdown that says goodbye: every client learns why its connection ended
Why this stage exists
A text client of the first stage saw the [LOCK] line when its statement waited. A PostgreSQL client sees nothing at all. It sent a Query, and no answer has come. This is correct: PostgreSQL itself says nothing while a statement waits for a lock. A client cannot tell "waiting" from "slow", and neither can a test, unless the server says so somewhere else. So the server says it on its own stdout:
$droid --server --port 5433 --db shop.db[SERVER] listening on 127.0.0.1:5433[SERVER] session 1 connected[SERVER] session 2 connected[LOCK] session 2 waits for session 1 on stock:7
This stage is about everything that happens around a statement while clients come and go. One waits, another gives up waiting, one vanishes with a transaction open, too many arrive at once, one is idle for an hour, and finally the whole server stops. Each of these is a way for one client to harm the others, and the server's job is to keep that harm in one place.
Cancelling from the side
A client whose statement waits can't send anything on its connection, because the protocol says it must wait for ReadyForQuery. So cancelling uses a second connection. The client opens a fresh one, and its first and only message is a CancelRequest with the two numbers the greeting gave it in BackendKeyData:
The secret key is why BackendKeyData exists. Session numbers are easy to guess, so without the key any client could cancel any other's statements. With a wrong key, the server does nothing and says nothing: telling the sender the key was wrong would help it guess.
A cancelled statement is undone like any failed statement. If its client had opened a transaction with BEGIN, that transaction stays open but failed (E, section 96), and the client decides what to do. Since droid runs one statement at a time to its end, only a statement that is parked can be cancelled. In PostgreSQL a cancel can also stop a long-running query halfway.
Limits
Every session costs memory, and a server without limits can be knocked over by one buggy client opening connections in a loop. --max-connections 2 caps the sessions. A connection over the cap gets 53300 ("too many connections") as soon as it has sent its StartupMessage, and is closed, without ever becoming a session or using a number.
A session that does nothing still holds its socket and its memory. --idle-timeout-ms 1000 closes sessions idle for a second, with 57P05. Idle is strict: no transaction open, nothing parked, nothing half received, nothing left to send. A session with a transaction open is never closed for being idle, because closing it would roll back work the client meant to commit.
The timeout is the first thing the poll loop has to wake up for without any socket being ready. Pass the time until the earliest idle session runs out as the poll's timeout, and check the clock when it returns.
The algorithm in words
A statement that waits
Sessions and Concurrency already parks it. Only two things are new.
- 1.When a statement parks, print its [LOCK] line on stdout too, and flush it.
- 2.Send nothing to its connection, and read no further message from it, until it resumes. Then send its result and ReadyForQuery as for any statement.
Cancelling
A CancelRequest is the first message of a connection of its own, and the last.
- 1.The first message has length 16 and the code 80877102: read the process id and the secret key after it, then close this connection without a reply. It took a session number when it was accepted, like any connection, but it runs nothing.
- 2.Find the session with that number. If there is none, its key is different, or it has nothing parked, stop: a cancel is a request, not a promise.
- 3.Otherwise drop its parked statement as if its client had gone, except that a transaction the client began with BEGIN stays open, now failed. Send that session's connection ErrorResponse 57014 and ReadyForQuery: E inside a transaction, I outside one.
Limits and the end
Both limits are checked by the loop, which already knows every connection.
- 1.Before a new connection becomes a session, count the sessions. At the limit, give it no number, but still read its first message as usual: N for an encryption request, 01905 for a protocol you do not speak, a CancelRequest honoured as always. Answer its StartupMessage with FATAL 53300 and close it.
- 2.Remember when each session last became idle: no transaction, nothing parked, nothing half received, nothing left to send. Make the loop's wait end when the earliest of them runs out, then send that one FATAL 57P05 and close it.
- 3.On shutdown, send every PostgreSQL connection FATAL 57P01 before closing it, then do what section 93 says.
Output contract for this stage
These are the exact lines and messages the tests check. The full definition is on the output contract page, section 97.
| Situation | Server's stdout | Client receives |
|---|---|---|
| a statement parks | [LOCK] session <n> waits for session <m> on <resource> | nothing, until it resumes |
| CancelRequest, right key, statement parked | the waiting client: ErrorResponse 57014, ReadyForQuery E or I; the cancelling connection: closed, no reply | |
| CancelRequest, wrong key | nothing anywhere; the cancelling connection closed | |
a connection over --max-connections | ErrorResponse FATAL 53300, then closed | |
a session idle past --idle-timeout-ms | [SERVER] session <n> closed | ErrorResponse FATAL 57P05, then closed |
Query shutdown | [SERVER] session <n> closed for each, then [SERVER] shutdown; exit status 0 | every client: ErrorResponse FATAL 57P01, then closed |
What you should see when done
In psql, a statement that waits, cancelled with Ctrl+C. psql sends the CancelRequest for you:
$psql -h 127.0.0.1 -p 5433 -U droid droiddroid=> begin;BEGINdroid=*> update stock set n = 9 where id = 7;^CCancel request sentERROR: canceling statement due to user requestDETAIL: droid error 01908droid=!> rollback;ROLLBACK
Common bugs in this stage
- Logging the
[LOCK]line only to the connection. A PostgreSQL client never shows it; the test reads it from your stdout. - Running the cancelling connection's message as a query. Its one message is the CancelRequest; close it after reading the 16 bytes, whatever they say.
- Rolling back a cancelled statement's whole transaction. Only the statement goes; the transaction the client began stays, failed.
- Answering a wrong key. Silence, always.
- Closing a session in a transaction for being idle. That would roll back work the client meant to keep.
- A poll with no timeout. Then an idle session is only closed when some other socket happens to wake the loop.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C:
clock_gettime(CLOCK_MONOTONIC, ...)for "now", never the wall clock, which can jump.poll's last argument is the timeout in milliseconds,-1for none. - C++:
std::chrono::steady_clock::now(), and astd::chrono::millisecondsuntil the earliest deadline. - Rust:
std::time::InstantandDuration.mio::Poll::polltakes anOption<Duration>. - Zig:
std.time.Instant.now()andsince;std.posix.polltakes the timeout in milliseconds.
Checklist
One line per thing the tests check. The tags name the test cases.
- A waiting statement is logged on the server's stdout and its client gets nothing until the holder commitslock-wait-across-sockets
- Waiters are served in the order they camefifo-three-waiters
- A client that sends Terminate mid-transaction gives its locks backterminate-releases-locks
- A client that vanishes mid-transaction gives its locks back tooabrupt-close-releases-locks
- A CancelRequest with the right key stops a waiting statement with 57014; a wrong key changes nothingcancel-request
- A connection over --max-connections gets FATAL 53300 and takes no session numbermax-connections
- A session idle past --idle-timeout-ms gets FATAL 57P05; one in a transaction is never closed for itidle-timeout
- shutdown sends every client FATAL 57P01, logs [SERVER] shutdown and exits 0graceful-shutdown