perf: mitigate slow follow/tag/rank queries (2026-07-12 outage) - #373
Merged
Conversation
get_followers, get_followers_by_page, and get_following_by_page hit the DB on every call (only get_following had a 30s cache). The parameterized `state IN :state` tuple binding also prevented the planner from matching the v23 partial index idx_follows_follower_state_created_desc (WHERE state IN (1,3)), forcing a fallback to the full-ASC ix5a with a cross-value sort -- the 21.9s query in the 2026-07-12 outage. Hardcode `state IN (1,3)` / `state IN (2,3)` literals so the planner can use the partial index, add a 30s cache to the three uncached functions, and complete the INNER JOIN migration for get_following_by_page (missed in 3274329 and 67c6d5e where its three siblings were converted).
Both tag-filtering queries ran uncached on every call. pids_by_blog in the same file already uses query_col with cache_key/cache_ttl=30; apply the same pattern with a 60s TTL. Hot tags (e.g. dmania) match tens of thousands of post_ids via the `IN (subquery)` and re-running it per request was the 12.9s query in the 2026-07-12 outage.
get_followers queries `WHERE following = :id ORDER BY created_at DESC` but the only partial index idx_follows_follower_state_created_desc is follower-led (serves get_following). Add a following-led counterpart idx_follows_following_state_created_desc so get_followers no longer falls back to the full-ASC ix5a. Also drop hive_follows_5a/5b: legacy v9 duplicates of ix5a/ix5b (identical column sets), pure write-amplification burden, never managed by _disableable_indexes. Bumps DB_VERSION 28->29.
Accounts.fetch_ranks runs `SELECT id FROM hive_accounts ORDER BY vote_weight DESC` (full-table sort of million-scale rows, ~12s) on the indexer's sync connection. Although separate from the API pool, it competes for RDS IOPS (which hit the 3000 cap during the 2026-07-12 outage). Rank is an approximate score for notification buckets; a 6h stale window is acceptable.
Address PR #373 audit feedback: - The state-filter comment claimed hardcoding "matches the partial index" for both branches, but the ignore branch (state IN (2,3)) cannot match any WHERE state IN (1,3) partial index. Rewrite the 4 comments to state precisely: normal branch matches the partial index; ignore branch falls back to ix5a/ix5b (no regression) but still benefits from a hardcoded literal via better cardinality estimates vs a tuple bind. - Cache key used `str(start_id) if start_id else ''`, which collapses both None and a (theoretical) id 0 to ''. Switch to an explicit 'none' sentinel for None so it can never collide with any integer id.
The ignore branch (state IN (2,3)) cannot match any WHERE state IN (1,3) partial index and falls back to the full-ASC ix5a/ix5b scan (3.5-9s per query in production, observed via Scalyr). Although already cached at 30s TTL, the cache churns fast: each account/start_id/limit combination is a distinct key, and 30s expiry forces frequent cold-cache DB hits on this slow path. Muted (ignore) relationships change rarely compared to normal blog follows, so a 300s (5min) TTL is acceptable -- at worst a 5min delay reflecting an un-mute. Normal follows stay at 30s. Applies uniformly to get_followers, get_followers_by_page, get_following, get_following_by_page.
kuny0707
approved these changes
Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Short-term mitigation for the three slow-query classes that caused the 2026-07-12 beta-hivemind outage (ELB MaxResponseTime 30s, jussi 502s, ~2h duration). Targets the 21.9s / 12.9s / 12.7s queries identified in the RDS slow log. No architecture changes — maximally reuses existing patterns (the
@cacherdecorator,pids_by_blogcaching, theCONCURRENTLYmigration style from #367/#371).Investigation report:
agent-share/notes/hivemind/2026-07-12-beta-hivemind-slow-query-investigation.mdChanges
1. Follow queries — hardcode state + cache (
5f566a6)get_followers,get_followers_by_page,get_following_by_pagewere uncached (onlyget_followinghad a 30s cache). The parameterizedstate IN :statetuple binding also prevented the planner from matching the v23 partial indexidx_follows_follower_state_created_desc(WHERE state IN (1,3)), forcing a fallback to the full-ASCix5awith a cross-value sort — the 21.9s query.state IN (1,3)/state IN (2,3)literals so the planner can use the partial indexget_following)get_following_by_page(missed in3274329and67c6d5ewhere its three siblings were converted)2. Tag filtering — cache (
eed7f5e)pids_by_category(bridge_api) andpids_by_query(condenser_api) ran uncached. Hot tags match tens of thousands ofpost_ids viaIN (subquery)— the 12.9s query. Apply the samequery_col+cache_keypattern already used bypids_by_blogin the same file, with a 60s TTL.3. Symmetric follows index + drop duplicates (
d35e4a4, v28→v29)get_followers(WHERE following = :id ORDER BY created_at DESC) had nofollowing-led partial index — the only one (idx_follows_follower_state_created_desc) isfollower-led and servesget_following. Hardcodingstate(change #1) is not enough without a symmetric index; this is the real fix for the 21.9s query.idx_follows_following_state_created_desc(following, state, created_at DESC, follower) WHERE state IN (1,3)hive_follows_5a/5b: legacy v9 duplicates ofix5a/ix5b(identical columns), pure write-amplification; never managed by_disableable_indexesDB_VERSION28→294. Reduce
fetch_ranksfrequency (56a60e7)Accounts.fetch_ranksrunsSELECT id FROM hive_accounts ORDER BY vote_weight DESC(full-table sort of million-scale rows, ~12s) hourly on the indexer's sync connection — the 12.7s query. Separate from the API pool but competes for RDS IOPS (which hit the 3000 cap during the outage). Reduced to every 6h; rank is an approximate notification-bucket score, a 6h stale window is acceptable.Verification
py_compile)state=bindings removed from all 4 follow funcs; cache_key+cache_ttl on all target functions; DB_VERSION=29; migration block completemake test-allnot run — local env has no PostgreSQL and theaiopggit-tarball dependency times out to install. Test suite requires a live DB; deferred to CI.Out of scope (mid/long-term, per investigation report)
Deployment notes
hive syncstartup viaCREATE INDEX CONCURRENTLY(no table lock), but consumes IOPS while building on the largehive_followstable — recommend deploying during a low-traffic window. Migration logs:[HIVE] Creating idx_follows_following_state_created_desc index...REDIS_URLto be configured (the@cacherdecorator skips cache whenredis_cache is None)