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? Because getaddrinfo blocks, 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.conf and /etc/hosts, so names that need nsswitch (LDAP, mDNS) do not resolve.

Try it. nix develop -c buck2 test //tests:keepalive shows 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

PathWhat
lib.rsThe crate's safe surface: what it exports, and Error.
executor.rsThe 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.rsNon-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.rsjev-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.rshickory's RuntimeProvider on the executor, against jev.dns_servers or /etc/resolv.conf.
ratelimit.rsThe 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.rsdefine_checked_string and StringCheck: a string setting validated when it is set, with the hook's unsafe kept here.
row.rsRowJson: 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.rsas_owner_of: run as a relation's owner, as a SECURITY DEFINER function would, which is how a granted role reaches jev_cache.
stats.rsThe 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/ →