Chapter 6: postjevsql-pg/src, an async runtime made of Postgres
Rust's async is a promise with no engine attached: a future does nothing
until something polls it, and something has to sleep until there is a
reason to poll again. That something is an executor, and usually you
borrow one (tokio, in most of the Rust world). Chapter 5 said why this crate
cannot. So executor.rs is a small executor of its own, about 650 lines,
whose one place to sleep is a Postgres WaitEventSet.
sequenceDiagram
participant S as The scan
participant E as executor.rs
participant W as WaitEventSet (built for this wait)
participant P as Postgres
S->>E: block_on: wait for the oldest row's answer
E->>W: the latch, postmaster death, every registered socket, the earliest timer
W-->>E: woken: a socket is readable, a timer is due, or the latch is set
E->>P: ResetLatch, CHECK_FOR_INTERRUPTS
alt Ctrl-C or statement_timeout
P-->>E: ERROR, unwinding: every future in flight is dropped, streams reset
else nothing to cancel
E->>E: poll the woken tasks
E-->>S: the answer, when it is there
end
The set is built afresh for each wait and freed after it, because Postgres
has no way to remove one socket from a set (no RemoveWaitEvent in any
supported major), and sockets come and go with reconnects and DNS. With the
usual one HTTP/2 socket this costs a few syscalls against requests of 70 to
500 ms, which is nothing.
On top of the executor sits an ordinary-looking HTTP stack, every layer
chosen because it needs no runtime of its own: non-blocking std sockets
(net.rs), rustls through futures-rustls for TLS, hyper for HTTP/2, and
hickory for DNS (dns.rs), all driven by the executor. There is one HTTP/2
connection per backend, reused across queries, and every row a scan judges
is a stream on it.
Aside. Why bother resolving names on the executor, when libc has
getaddrinfo? Becausegetaddrinfoblocks, and while it blocks nothing sees a cancel: glibc waits 5 seconds per attempt, per nameserver. DNS is the one step where plain libc would quietly make Ctrl-C not work. The price is that hickory reads only/etc/resolv.confand/etc/hosts, so names that need nsswitch (LDAP, mDNS) do not resolve.
Try it.
nix develop -c buck2 test //tests:keepaliveshows the connection's pings at the mock: sent while a scan waits, never between statements, and an unanswered one closing the connection so the request goes again on a new one.
For the people who maintain it
| Path | What |
|---|---|
| lib.rs | The crate's safe surface: what it exports, and Error. |
| executor.rs | The single-threaded executor: block_on, spawn_local, task Scopes that a scan's end or a reset callback drops, socket register/deregister, timers (sleep, sleep_until), and random_unit from Postgres's PRNG. |
| net.rs | Non-blocking TCP and UDP for the executor, HyperIo (futures-io to hyper), and PeerSettings, which reads the server's MAX_CONCURRENT_STREAMS off its frames since hyper keeps it private. |
| https.rs | jev-client's two ports: HttpsTransport (one reused HTTP/2 connection over rustls; it sends and classifies failures, and never retries) and Backend (time, sleep and jitter on the executor). Endpoint parses jev.endpoint, KeepAlive the ping settings, stream_limit the server's stream cap. |
| dns.rs | hickory's RuntimeProvider on the executor, against jev.dns_servers or /etc/resolv.conf. |
| ratelimit.rs | The cluster's rate limit: postjevsql-core's GCRA words in a named DSM segment every backend attaches to, the admission wait on the executor, and the feedback from 429s and answers. |
| guc.rs | define_checked_string and StringCheck: a string setting validated when it is set, with the hook's unsafe kept here. |
| row.rs | RowJson: a row as JSON that is the same whoever asks, with dates and times in RFC 3339 and every other value under fixed output settings. |
| owner.rs | as_owner_of: run as a relation's owner, as a SECURITY DEFINER function would, which is how a granted role reaches jev_cache. |
| stats.rs | The backend's counters behind jev_stats(), and Telemetry, jev-client's observer, which logs each retry, redial, connection and answer at DEBUG1. |
| scan/ | The batch scan. Chapter 7. |
← Previous: Chapter 5, postjevsql-pg/ · Up: postjevsql-pg · Next: Chapter 7, scan/ →