Skip to content

perf: index, cache, and deflake /api/channels queries (rebase of #1887) - #1936

Merged
efiten merged 1 commit into
Kpa-clawbot:masterfrom
efiten:fix/1887-rebased
Sep 2, 2026
Merged

perf: index, cache, and deflake /api/channels queries (rebase of #1887)#1936
efiten merged 1 commit into
Kpa-clawbot:masterfrom
efiten:fix/1887-rebased

Conversation

@efiten

@efiten efiten commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 perf: use cached ParsedDecoded() instead of repeated json.Unmarshal #1871, which was fixed there by copying at the two broadcast sites.

/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.
@efiten
efiten enabled auto-merge (squash) September 2, 2026 13:03
@efiten
efiten merged commit 89544b1 into Kpa-clawbot:master Sep 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants