collect_visible_ref_updates_inner (api/events.rs:37) loads three whole tables before it filters a single row:
let deduped = db.list_all_repos_deduped().await?;
let quarantined = db.list_quarantined_repos().await?;
let ids: Vec<String> = deduped.iter().map(|r| r.id.clone()).collect();
let rules = db.list_visibility_rules_for_repos(&ids).await?;
That runs per request, on the anonymously reachable GET /api/v1/events/ref-updates and on the GraphQL ref_updates resolver that shares the collector.
What is not the problem
The gate itself is correct and this issue does not dispute it. Loading the inputs once and failing closed on a DB error is deliberate; the keyset paging past withheld rows is the fix from #114 and #112 and is the right shape. The max_scan cap bounds the row walk.
What is unbounded is the preload, which sits above max_scan and grows with the repo table rather than with the request.
Cost
O(repos) rows plus O(repos) String clones for the id vector, plus the visibility-rule fetch keyed on all of them, per request. On a node with a large repo table this is the dominant cost of a feed request, and the caller pays nothing for it — the route is anonymous and outside every rate_limit_by_ip layer in build_router.
Related in kind to #147, which capped an unbounded per-repo fetch on a permissionless read path, and to the GraphQL repository-enumeration and /api/v1/stats paths that load repo records to compute visibility-aware answers. This is the same pattern on the cheapest route to reach.
Fix direction
Two options, probably in this order:
- Cache the three gate inputs behind a short TTL keyed on the repo table's last update. Cheap, no behavior change, and the inputs are already loaded as a unit.
- Push the visibility predicate into SQL so the rows are filtered by the database rather than fetched and dropped in Rust. Larger, and it has to preserve the quarantine-before-visibility ordering that
events.rs:98-107 is careful about — quarantine denies before the owner short-circuit in visibility_check can run, and a naive SQL translation would lose that.
Option 2 is the real fix; option 1 is worth having regardless because the inputs change rarely relative to how often the feed is read.
Validation status
Verified by reading api/events.rs and the db methods it calls. No measurement against a populated repo table is offered, so the point at which this matters in practice is unquantified.
Found during an external audit pass. Duplicate-checked against open and closed issues; no existing coverage found, but the search used a limited keyword set.
collect_visible_ref_updates_inner(api/events.rs:37) loads three whole tables before it filters a single row:That runs per request, on the anonymously reachable
GET /api/v1/events/ref-updatesand on the GraphQLref_updatesresolver that shares the collector.What is not the problem
The gate itself is correct and this issue does not dispute it. Loading the inputs once and failing closed on a DB error is deliberate; the keyset paging past withheld rows is the fix from #114 and #112 and is the right shape. The
max_scancap bounds the row walk.What is unbounded is the preload, which sits above
max_scanand grows with the repo table rather than with the request.Cost
O(repos) rows plus O(repos)
Stringclones for the id vector, plus the visibility-rule fetch keyed on all of them, per request. On a node with a large repo table this is the dominant cost of a feed request, and the caller pays nothing for it — the route is anonymous and outside everyrate_limit_by_iplayer inbuild_router.Related in kind to #147, which capped an unbounded per-repo fetch on a permissionless read path, and to the GraphQL repository-enumeration and
/api/v1/statspaths that load repo records to compute visibility-aware answers. This is the same pattern on the cheapest route to reach.Fix direction
Two options, probably in this order:
events.rs:98-107is careful about — quarantine denies before the owner short-circuit invisibility_checkcan run, and a naive SQL translation would lose that.Option 2 is the real fix; option 1 is worth having regardless because the inputs change rarely relative to how often the feed is read.
Validation status
Verified by reading
api/events.rsand thedbmethods it calls. No measurement against a populated repo table is offered, so the point at which this matters in practice is unquantified.Found during an external audit pass. Duplicate-checked against open and closed issues; no existing coverage found, but the search used a limited keyword set.