Chapter 20½: inside jev-http/src — a road, a clock and a ledger
Four files. lib.rs is the front door (Jev), and the other three
are what it is built from: the road (transport.rs), the
clock (runtime.rs) and the meter (ledger.rs).
flowchart TD
jev["lib.rs: Jev"] --> client["jev_client::Client"]
jev --> ledger["ledger.rs: Ledger"]
client --> transport["transport.rs: HttpsTransport"]
client --> runtime["runtime.rs: TokioRuntime"]
ledger --> file[("$XDG_STATE_HOME/jev/")]
The files, in reading order
1. lib.rs: Jev, the client you keep
KEYis"TYPESAFE_API_KEY", the variable the key arrives in.Jev::from_env(binary, model)reads the key, opens the ledger withLedger::open, and builds a client for the real API. It returnsNonewhen the key is absent or blank.Jev::new(key, binary, ledger, model, endpoint)is the same with every part given, which is how a test points it at jev-mock and a sandbox ledger. It builds the rustls config: compiled-inwebpki-roots, any PEM fromEndpoint::trusting, the RustCrypto provider, ALPNh2.with_guards(guards)swaps the environment'sGuardsfor others.ask(state, questions, why)is the sequence drawn in Chapter 20: admit, ask throughjev_client::Client, then settle, or release only whenClientError::nothing_billedsays so. An out-of-credit API answer also writes the ledger's out-of-credit flag. A ledger write that fails after an answer arrived is logged to stderr and the answer is still returned: the request was already paid for.Answeredadds tojev-client's:took(the whole round trip), the body as text, every header with credentials redacted (any name containingauthorization,cookie,token,secret,-keyorapi-keyshows as«redacted»), the request id, and aRateLimit.FailureisThrottled { why, retry_in },Budget(why)orClient(ClientError). The first two never touch the network.RateLimit { limit, remaining, reset_seconds }is read from the conventionalx-ratelimit-*andratelimit-*headers. The vendor documents none of them on a successful response (digest §10), so it is usuallyNone; it exists so that the day the vendor sends one, it is reported with no call site changed.last_rate_limit()returns the last one seen, and the ledger keeps a copy inlast-rate-limit.json.
2. transport.rs: HttpsTransport and Endpoint
Endpoint::api()isjev_protocol::ENDPOINT;Endpoint::parse(url)accepts anyhttpsURL and refuses anything else;trusting(pem)adds a CA file.HttpsTransportkeeps one HTTP/2 connection warm and reopens it when it has closed, has been idle 300 s (Cloudflare closes idle connections at 400 s), or the client distrusts it.- Connecting tries each resolved address in turn, giving every address but the last 2 s, so one blackholed address (a broken IPv6 route) cannot spend a whole attempt. It sends an HTTP/2 PING after 20 s without a frame and closes the connection if the PING goes 10 s unanswered. Flow-control windows are adaptive, because hyper's fixed windows are smaller than an 8 MiB answer.
- A response body over 8 MiB is
TooLarge. A request hyper hands back unsent, or one refused withREFUSED_STREAMor cut by a server GOAWAY, isNotSent; any other break mid-exchange isInterrupted. A TLS refusal by rustls, or a server that does not negotiateh2, isTls. admitwaits on two per-process token buckets under the vendor's documented ceilings, 1,200 requests a minute and 250,000 tokens a second, counting the body's characters as tokens.
3. runtime.rs: TokioRuntime
now and wall_clock from std, sleep and timeout from tokio, and
jitter from the clock's nanoseconds, which is random enough to spread
retries and needs no dependency.
4. ledger.rs: Ledger, the shared meter
Its doc comment explains the design (why a file, why flock, which guards
heal by themselves, why the total is re-read every time); read it first.
Then:
Ledger::open()uses$XDG_STATE_HOME/jev, or~/.local/state/jev;Ledger::at(dir)uses any directory. Either creates it and, the first time ever, seedsspend.jsonlwith one estimated opening line (OPENING_ESTIMATE_USD, $0.1517).create_newmakes the seed race-free.- Each line of
spend.jsonlis JSON: anEntry(a real cost, or the opening estimate) or aHold(a question admitted and not yet settled), told apart by the hold'shold_id. A line that fails to parse, such as a torn write from a killed process, is skipped with a message on stderr rather than failing every later read. admit(now, guards, worst_case, binary, reason)checks the out-of-credit flag, then, under oneflock, reads every line into aWindow, asksWindow::verdict, and appends theHold.settle(hold, entry)books the real cost;release(hold, now, why)books nothing but still counts the question.Windowis the recent past in one pass:lifetime_usd, the admission times of the last minute, the amounts of the last hour, and the count of unsettled holds.bucket_empty_forandhour_closed_forsay how long until each guard would admit.Guards::from_env()readsJEV_MAX_QUESTIONS_PER_MINUTE(30) andJEV_MAX_DOLLARS_PER_HOUR(0.25).status(now, guards)builds aStatus, withline(),stopped()andstopped_for_good()for display. Its hour-breaker figure is for an ordinary question of a thousand input tokens.mark_out_of_creditwritesout-of-credit.json(the first reason is kept),credit_restoredremoves it, andout_of_creditreads it.now()is seconds since the epoch as anf64. Every guard function takesnowas an argument instead, so the tests run on any clock they like.
Try it
cargo test -p jev-http
The ledger's tests each get their own directory under the system temp dir, so they never touch your real ledger.
For the people who maintain it
In this folder
| File | What |
|---|---|
| lib.rs | Jev, Answered, Failure, RateLimit, KEY; header redaction and rate-limit parsing. |
| transport.rs | Endpoint, HttpsTransport, the courtesy buckets, connecting. |
| runtime.rs | TokioRuntime. |
| ledger.rs | Ledger, Entry, Hold, Guards, Window, Refusal, Status, NoCredit. |
| CLAUDE.md | Invariants for agents. |
← Previous: Chapter 20: jev-http · Up: jev-http · Next: Chapter 20¾: jev-http/tests →