From f1ddc1231c475258008dc35cbb2f99827c55b5be Mon Sep 17 00:00:00 2001 From: kirillDevPro <113171057+kirillDevPro@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:17:35 +0200 Subject: [PATCH] docs(architecture): record the bounded report readers The architecture still described report reads as an unbounded open. Readers now take one of eight permits, attach companion databases per query, and classify CANTOPEN as retryable exhaustion. Unix startup raises the soft open-file limit before the install's own stores open. --- docs/ARCHITECTURE.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 47facb97..f408305f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # MoonTerminal Architecture -Last updated: 2026-08-09. +Last updated: 2026-09-21. This document describes the terminal's current public architecture and deliberately omits old experimental migration plans. @@ -534,6 +534,37 @@ of operations rests on one rule: **only the transaction that applied the map wri Rolling back a batch moves neither the checkpoint nor the published start state — the next connection simply repeats catch-up. +### Report readers + +Query reads go through `open_reader` / `open_reader_with` and come back as a `ReportReader`. A +process-wide permit (`db::reader_budget`) allows eight at once. The connection field is declared +before the permit, so the descriptors close before the slot returns. The permit owns the budget: +`read_snapshot` borrows a transaction from the caller's connection, so a shared worker cannot sit +in its place. A reader that finds every slot taken waits up to three seconds. If one frees, the +open continues. If the wait expires, the open fails as `FailKind::Exhausted`. A wait cancelled +because the request was superseded is `FailKind::Other`, not exhaustion. + +`SQLITE_CANTOPEN` is classified as `Exhausted` too, separately from that synthetic budget +timeout. The class is retryable — the descriptor budget, or something else holding the file — +and it is not an empty period and not a damaged replica. The uid-floor probe (`open_readonly`) +and the background integrity scan open a bare read-only connection and do not take a slot. + +Which companion databases are attached is a property of the query (`AttachSet`), chosen before +the snapshot because SQLite refuses `ATTACH` inside a transaction. `open_reader()` is +`AttachSet::ALL` (strategies and the valuation cache). Chart trade history never references the +valuation schema and uses `AttachSet::STRATEGIES_ONLY` (`CHART_TRADE_HISTORY_ATTACH`). A missing +strategies file, or a missing or unhealthy valuation cache, attaches nothing for that companion +and the open still returns. When every requested database does attach, one reader holds nine +descriptors for `ALL` and six for strategies only: each database in WAL is the file plus `-wal` +and `-shm`. + +On Unix, `startup::open_file_limit::raise_to_hard_limit` runs once after the logger is installed +and before the install's locks, migrations, and database opens. It raises the soft +`RLIMIT_NOFILE` to the hard limit. macOS caps the request at Darwin `OPEN_MAX` (10240), because +`setrlimit` returns `EINVAL` above that even when the hard limit is infinity. A failed raise is +logged and does not stop startup. A `--fixture` run can open the relocated replica during +bootstrap, which is earlier than this raise. Windows has no such limit. + ### The order-trace archive (`order_traces.sqlite`) The core serves traces of closed trades on `request_traces(ReportUID)` from an archive