fix(vahter): rebuild trgm index with fastupdate off, mark forensic indexes - #502
Merged
Merged
Conversation
Every vahter 504 in Envoy (response_flags=UT, 15s upstream timeout on the Telegram webhook POST /bot) traces to a single INSERT INTO event stalling for 8-30s, and Tempo + the event table show the stall recurring every 474-526 MessageReceived rows (~600-900 kB of rawMessage), 3-5 times a day. That cadence is the GIN pending list of idx_event_rawmessage_trgm (V47 forensic index, 653 MB, gin_pending_list_limit 4 MB) hitting its limit: the inserting backend flushes it synchronously, and on B1ms/P4 (120 IOPS) that takes tens of seconds while blocking other inserters, so Telegram's 15s redelivery to the second pod stalls on the same flush. Recreate the index CONCURRENTLY with fastupdate=off so trigram entries go straight into the index on each insert instead of accumulating into a periodic flush. Same idempotent DROP/CREATE CONCURRENTLY shape as V49. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rebuilt trigram index becomes idx_forensic_event_rawmessage_trgm so its name records that it is a debug-session index built with fastupdate=off. V52 gives the same prefix to the other indexes no bot query reads: idx_event_username_lower and idx_event_user_ban_by_stream (V30 "debug indexes"), idx_event_mlscored_score_created_at (V48, LLM-band analysis query run by hand), and the three snapshot_user indexes (V38/V43; snapshot_user is only ever written by bot code, the debug/ops queries read it). Verified by grepping DB.fs for each index's predicate; the only low-scan event index that IS read by prod code, idx_event_msg_marked_ham_text (GetUserStats' is_ham subquery), keeps its name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Why
Every vahter 504 in the Envoy access logs (
response_flags=UT, the 15 s upstream timeout on the Telegram webhookPOST /bot) traces to oneINSERT INTO event(the MessageReceived append) stalling for 8–30 s server-side. Nothing else runs on the DB at those moments, SELECTs oneventkeep working, and the second pod's identical INSERT (Telegram's 15 s redelivery) blocks until the first finishes.Counting MessageReceived rows between consecutive stalls (2026-09-14 → 16, from Tempo + the
eventtable):rawMessagebytesA stall every ~500 inserts of ~1.4 kB text is the GIN pending list of
idx_event_rawmessage_trgm(V47 forensic index, 653 MB,gin_pending_list_limit4 MB) hitting its limit. The inserting backend flushes it synchronously and blocks other GIN inserters; on B1ms + P4 (120 IOPS) that takes tens of seconds. Envoy 504 UT on the vahter route: 1–5/day since 2026-08-28 (V47 landed), sporadic before.What
V51 —
DROP INDEX CONCURRENTLY IF EXISTS(old and new name, so a retry after a failed build is clean) +CREATE INDEX CONCURRENTLY idx_forensic_event_rawmessage_trgm ... WITH (fastupdate = off). Trigram entries then go straight into the index on each insert instead of accumulating into a periodic flush. Theidx_forensic_prefix records that this is a debug-session index and that GIN ones are built withfastupdate = off.V52 —
ALTER INDEX IF EXISTS ... RENAME TO idx_forensic_...(metadata only) for the other indexes that no bot query reads:idx_event_username_lowerv_users/v_recent_bansviews; prod looks up usernames bystream_id(GetVahterActionStats)idx_event_user_ban_by_streamSELECTin DB.fs touchesUserBanned/UserUnbannedrows; 418 scans since July come from debug viewsidx_event_mlscored_score_created_atscore; 1 scan everidx_snapshot_user_usernamesnapshot_useris only written by prod code (upsert + rebuild); 0 scansidx_snapshot_user_bannedidx_snapshot_user_spam_protectionKept as-is despite low scan counts:
idx_event_msg_marked_ham_text(0 scans, butGetUserStats'is_hamsubquery filters ondata->>'text'forMessageMarkedHam), thesnapshot_messageindexes (read by the report queries), andix_bot_setting_feature_group(16 kB, not worth a migration).Deploy notes
repairbefore the nextmigrateclears the failed row and V51 retries (the twoIF EXISTSdrops handle any leftover invalid index).Verify
{ resource.service.name="vahter-bot" && name="postgresql" && duration > 2s }should stop showing theINSERT INTO eventspans.{namespace="envoy-gateway-system"} |= "response_timeout" |= "vahter-bot-route"should go to zero per day.SELECT indexrelname, pg_get_indexdef(indexrelid) FROM pg_stat_user_indexes WHERE indexrelname LIKE 'idx_forensic_%'→ 7 rows, the trgm one showingWITH (fastupdate='off').🤖 Generated with Claude Code