Chapter 3: postjevsql/src, the judge

Two files, and one of them is long. lib.rs is about two thousand lines, but it reads well from top to bottom once you know that it has exactly one job: it is the Judge the scan (chapter 7) calls. The scan hands it one row's calls at a time and gets back one answer per call. Everything between those two moments is here.

Follow one call, jev_prob(t, 'Urgent?') on one row:

flowchart TD
  C["a call, with the row as canonical JSON"] --> K["its cache key: the hash of the exact bytes it would send"]
  K --> S{"an equal call already asked in this statement?"}
  S -->|yes| W["wait on that one's answer (dedupe_hits)"]
  S -->|no| L{"in jev_cache?"}
  L -->|yes| H["the kept answer, for nothing"]
  L -->|no| B{"within jev.max_rows and jev.max_cost?"}
  B -->|no| R["refuse: 54000"]
  B -->|yes| Q["one request: the row as the state, each distinct question about it once"]
  Q --> J["jev-client: send, retry, classify"]
  J --> A["the answer, checked against the pinned model"]
  A --> St["settle: stored in jev_cache, on the backend"]

The last box is the subtle one. A judgment runs as a future on the backend's executor (chapter 6), polled from inside the executor's wait in the middle of the scan's own work, and the Judge contract says plainly that such a future must not call Postgres. So answers pile up as receipts, and the scan calls settle between waits, on the backend proper, which writes them to jev_cache with ordinary SQL.

Aside. The same trick, run backwards, is how a label-tree search works. It takes several rounds, and each round's Choices should be looked up in the cache before anything is sent. But the cache is SQL, and the search is a future. So the future hands a closure to the backend (scan::on_backend), the scan runs it between waits, and the future resumes with the result. A future asking a database for a favour, and waiting politely for it.

lib.rs, in the order it is written

  1. The settings. _PG_init registers every jev.* GUC, with who may set it and why, then registers the scan (scan::register::<JevScan>()). PinnedModel refuses an alias when jev.model is set.
  2. The SQL around the functions. Three extension_sql! blocks: the jev_cache table, the five result types (jev_noul_result, jev_score_result, jev_choice_result, jev_tree_result, jev_shortlist_result), and the REVOKE … FROM PUBLIC of every function.
  3. The functions. One #[pg_extern] each, all volatile, parallel_restricted and planned with jev_support, all with a body that refuses (chapter 2 says why). Then jev_support, the planner support function that prices them, and jev_stats.
  4. JevScan, the Judge. begin reads the settings once per statement (a prepared plan outlives a SET) and builds the client; afford refuses a statement whose estimate is over budget; explain is what EXPLAIN shows; window is jev.concurrency clamped to the server's stream limit; judge answers one row; settle stores what was answered.
  5. Sharing. Asking and Shared are the statement's registry of judgments in flight, so an equal call waits instead of sending. Owed unregisters a key whose request was dropped, so its waiters ask again themselves. Budget and Committed hold each request's worst case against the spend guards until its real usage replaces it.
  6. The searches. TreeCall drives postjevsql-core's label-tree search for jev_choice_tree, and ShortlistCall its chunk-and-shortlist search for jev_choice_shortlist, a round at a time, each round looked up through on_backend. tree_out turns a search's outcome into the SQL result.
  7. Asking. Lookup finds calls in the cache; Judged is a judgment's answer and how each function reads it; Sender sends the misses within the budget, serves their waiters and queues their receipts.
  8. Odds and ends. api_key (exactly one source, never both), learned_ratio (characters per token, learned from jev_cache for the pinned model), and setting.

Try it. cargo test -p postjevsql-core exercises the planner, key and searches this file drives. The judge itself needs a running Postgres, so its tests are the integration tests: nix develop -c buck2 test //tests:cache runs the cache's.

For the people who maintain it

PathWhat
lib.rsSettings, the SQL objects, the functions, and JevScan: begin, afford, explain, judge, settle; sharing, budgets, the searches, the sender.
failure.rsFailure: every way a call fails, the one place each becomes an ERROR with its SQLSTATE and DETAIL, and is_failed_judgment, which decides what jev.on_error = unsure may turn into NULL.
bin/The schema binary cargo-pgrx runs. Chapter 4.

← Previous: Chapter 2, postjevsql/ · Up: postjevsql · Next: Chapter 4, src/bin/ →