perf: index, cache, and deflake /api/channels queries - #1887
Conversation
/api/channels and /api/channels/{hash}/messages could take seconds
under load because of five compounding issues:
1. GetChannels' correlated "latest message" subquery and
GetChannelMessages' count/page queries sorted per channel_hash with
no covering index on first_seen. Added
idx_transmissions_channel_hash_payload(channel_hash, payload_type,
first_seen) so these resolve as index-order scans.
2. GetChannelMessages had no cache at all - every page view/poll re-ran
the full paginated query. Added a 10s TTL cache keyed by
hash+limit+offset+region.
3. GetChannels' cache was a single slot keyed by one region string, so
any mix of region values thrashed it into permanent cache misses.
Replaced with a map keyed by region.
4. GetEncryptedChannels had no cache at all. Gave it the same
map-keyed 60s TTL cache as GetChannels.
5. The region filter's UPPER(TRIM(obs.iata)) predicate couldn't use a
plain index. Added an expression index,
idx_observers_iata_norm(UPPER(TRIM(iata))), on observers.
The channel_hash column is added by the ingestor's legacy
'channel_hash_v1' migration, not by dbschema.go, so
ensureChannelIndexes probes for it before indexing rather than
assuming it exists.
Updated TestGetChannelMessagesPerfLargeChannel to clear the new cache
between its warm-up and timed calls so the regression guard still
measures the real SQL path instead of a cache hit.
|
Review from the queue triage. Written 2026-08-30 against the tree at that time; posting now that the maintenance window in #1922 has opened. Verdict: approve with two questions. Solid work: per-region keyed caches instead of a
Two smaller notes: |
|
Recycling this again, and the reason is my mistake rather than anything about your PR. Earlier today I approved the pending workflow run on this PR. That was the wrong order: approving an Closing and reopening now gets a fresh merge commit against current master, which is what the run should have been all along. No action needed from you, and apologies for the second round of noise. |
… (#1936) Continues #1887 by @Jonher937. The commit is theirs, authorship unchanged; I only rebased it onto master. It went CONFLICTING because #1934 (prepared statements, originally @Joel-Claw's #1878) landed in the same `DB` struct. Both PRs add fields there and this one also replaces the single-slot channels cache. Resolution: kept this PR's keyed caches (`channelsCache`, `encChannelsCache`, `msgCache` plus their entry types and TTL constants) and kept master's thirteen prepared-statement fields alongside them. The old single-slot `channelsCacheKey`/`channelsCacheRes`/`channelsCacheExp` trio is gone, which is the point of this PR. Nothing else touched. Verified: `cmd/server` builds and the **full suite passes**, not just the channel tests. My review stands: approve, with two questions that do not block and are worth a look at some point. 1. `msgCache` is keyed by `hash|limit|offset|region`, and `offset` grows without bound as someone pages through a channel. Each entry also holds a full page of message maps, so a full 256-entry cache at `limit=50` holds around 12,800 maps. The other two caches are keyed by region only and genuinely low-cardinality as your comment says; this one is the odd one out. 2. `getMsgCache` returns the cached slice directly, so every hit hands the caller the same message maps. If any handler mutates one before serialising, it corrupts the cache for the next ten seconds. Same class as the finding on #1871, which was fixed there by copying at the two broadcast sites. Co-authored-by: Jonathan Herlin <jonte@jherlin.se>
|
Merged via #1936, which is your commit rebased onto master with your authorship intact. Closing this as the vehicle rather than the work. The conflict was with #1934 in the The two questions from my review carried over to #1936 and are worth a look at some point, neither blocking: |
/api/channels and /api/channels/{hash}/messages could take seconds under load because of five compounding issues:
The channel_hash column is added by the ingestor's legacy 'channel_hash_v1' migration, not by dbschema.go, so ensureChannelIndexes probes for it before indexing rather than assuming it exists.
Updated TestGetChannelMessagesPerfLargeChannel to clear the new cache between its warm-up and timed calls so the regression guard still measures the real SQL path instead of a cache hit.