Skip to content

fix(etl): four prod-clone-run bugs + #307 test fixes folded in - #308

Merged
raymondjacobson merged 1 commit into
mainfrom
etl/prod-clone-findings
May 22, 2026
Merged

fix(etl): four prod-clone-run bugs + #307 test fixes folded in#308
raymondjacobson merged 1 commit into
mainfrom
etl/prod-clone-findings

Conversation

@raymondjacobson

Copy link
Copy Markdown
Contributor

Summary

Findings from running the indexer for ~3h against a production database clone (audius_discovery on 104.198.147.20) and diffing the result against the live production read replica. Bundles fixes for four prod-only bugs that only surface against populated/prod-schema data, plus the five non-regression fixes from #307 (which this PR supersedes — see "Why this supersedes #307" below).

Bugs and fixes

1. save_type / repost_type mistyped on entity-id collisions — REGRESSION from PR #307

When chain entity_type is "Playlist", PR #307's resolveSaveType / resolveRepostType fell through to inferSaveType, which ranks tracks first. Whenever a track exists with the same numeric id as the playlist being saved (track_id and playlist_id are independent namespaces — collisions are real in prod), the save was written with save_type='track' instead of 'playlist'.

Observed against prod:

  • chain tx: Playlist/Save, entity_id=4281, metadata={"is_save_of_repost":false}
  • replica row: save_type='playlist'
  • clone row (pre-fix): save_type='track'
  • track 4281 and playlist 4281 are unrelated entities by different artists
  • ~1% of our 1,437 saves had this wrong type

Fix: resolveSaveType / resolveRepostType only disambiguate "Playlist" via playlists.is_album — they never cross over to "track". New TestSave_Playlist_WhenTrackIdCollides locks this in: seeds both a track and a playlist with the same id and asserts the Playlist save writes save_type='playlist'.

2. mergeNullStr empty-string semantics

Chain User Update txs commonly include "handle":"" when the client doesn't want to change handle. The old mergeNullStr treated "" as "clear the field" → wiped users.handle to NULL while leaving handle_lc populated (inconsistent row state).

Observed against prod: user richard627 (id 169901003) had handle=NULL on clone but handle='richard627' on replica — same chain tx, different interpretation.

Fix: "" now preserves the existing value, matching the prod indexer's behavior.

3. PRIMARY KEY violations on re-delivered txs (family bug)

shares_pkey, reposts_pkey, saves_pkey, follows_pkey, subscriptions_pkey — all five have (entity_keys..., txhash) row-versioning PKs. The bare INSERTs in social_save / social_repost / social_share / social_follow hit 23505 when the prefetcher re-delivers a chain tx (root cause of re-delivery deferred — separate investigation).

Fix: each INSERT now has ON CONFLICT (entity_keys..., txhash) DO NOTHING. txhash is content-addressable, so re-delivered data is identical by construction.

4. blockhash NOT NULL violations on prod schema

Our migrations declare blockhash ... NOT NULL DEFAULT '' (so omitting the column locally silently writes ''), but the prod schema for 14 tables has no default — every INSERT must include blockhash explicitly. Observed: events.blockhash 23502 violations during prod-clone run.

Fix: added params.BlockHash to INSERTs into events, associated_wallets, muted_users. (Sweeping the other 11 NOT-NULL-blockhash tables is a follow-up; the handlers for them already include blockhash.)

Why this supersedes #307

PR #307 had six test fixes; one of them (the resolveSaveType for TestSave_Album_Success) introduced the save_type regression in bug #1 above. Rather than force-push #307, this PR includes the corrected version of that fix plus the other five (which were good). Recommend closing #307 in favor of this PR.

The five non-regression fixes carried forward:

  • AssociatedWalletCreate SOL base58 case preservation (new canonicalizeWallet helper)
  • AssociatedWalletDelete UPDATE drops the bogus chain = $4 filter that matched empty string
  • dashboard_wallet_test.go queries use lowercase wallet (matches handler's canonical storage)
  • track_update_test.go seeds user wallet matching the signer

Test plan

  • go build ./... + go vet ./... clean
  • All six previously-failing entity_manager tests now pass (including the original TestSave_Album_Success)
  • New TestSave_Playlist_WhenTrackIdCollides covers the regression
  • Full go test ./pkg/etl/... clean except for the pre-existing migrate-down/up flake on TestTrackCreate_AppliesAccessNormalization (unrelated)

Deferred follow-ups

  • no rows in result set raw error from some handler (needs context attribution — indexer.go:425 log line doesn't carry entity_type/action; sweeping handlers for unwrapped QueryRow.Scan will catch it)
  • Transient TCP "can't assign requested address" — environmental, pgxpool tuning if it recurs under sustained load
  • Prefetcher re-delivery investigation (root cause of all the PK violations we just papered over)
  • Sweep the other 11 NOT-NULL-blockhash tables (comment_*, playlist_routes, track_routes, etc.) — quick scan showed they already pass blockhash, but worth a full audit

🤖 Generated with Claude Code

Findings from running the indexer against a production database clone
for ~3h and diffing the result against a live production read replica.
This PR includes the corrected versions of #307's test fixes (one had a
regression we caught in the diff) plus four new prod-only bugs.

1. save_type/repost_type mistyped on entity-id collisions (REGRESSION
   from PR #307). When chain entity_type was "Playlist", #307 fell
   through to inferSaveType, which ranks tracks first. Whenever a track
   existed with the same numeric id as the playlist being saved
   (track_id and playlist_id are independent namespaces — collisions are
   real), the save was written with save_type='track' instead of
   'playlist'. Observed against prod: ~1% of saves mistyped. Fix:
   resolveSaveType / resolveRepostType only disambiguate "Playlist" via
   playlists.is_album; never cross over to "track". New
   TestSave_Playlist_WhenTrackIdCollides locks this in.

2. mergeNullStr treats "" as "clear field" instead of "no change". Chain
   User Update txs commonly include "handle":"" when the client doesn't
   want to change handle; our handler was wiping users.handle to NULL
   while leaving handle_lc populated. Real data corruption observed
   against prod (e.g. user_id=169901003 'richard627'). Fix: empty string
   now preserves the existing value, matching the prod indexer.

3. PRIMARY KEY violations on re-delivered txs (shares_pkey,
   reposts_pkey, saves_pkey, follows_pkey, subscriptions_pkey). Bare
   INSERTs into row-versioned (entity_keys..., txhash) PKs hit 23505
   when the prefetcher re-delivers a chain tx (root cause deferred). All
   five domain inserts now have ON CONFLICT (...) DO NOTHING — txhash
   is content-addressable, so re-delivered data is identical.

4. blockhash NOT NULL on prod schema. Our migrations declare
   `blockhash ... NOT NULL DEFAULT ''` (so omitting the column locally
   silently writes ''), but prod schemas have no default — every
   INSERT must include it. Observed events.blockhash 23502 violation in
   prod-clone run. Added blockhash to INSERTs into events,
   associated_wallets, muted_users.

Also folded in from #307 (the five non-regression fixes):

5. AssociatedWalletCreate: SOL base58 case preservation. ETH addresses
   lowercase; SOL preserve case (uppercase `L` → invalid base58 `l`).
   New `canonicalizeWallet` helper handles both based on chain or
   `0x` prefix.

6. AssociatedWalletDelete: drop bogus `chain = $4` filter from the
   UPDATE — delete tx doesn't include chain, so the filter was always
   matching empty string and updating nothing. Validation already keys
   on (user_id, wallet); UPDATE now matches.

7. dashboard_wallet_test.go: lowercase wallet address in test queries to
   match handler's canonical storage form.

8. track_update_test.go: seed user wallet matching the signer so the
   test reaches the existence check it means to exercise.

Closes #307 (superseded). Deferred: "no rows in result set" raw error
(needs context attribution), TCP "can't assign requested address"
(environmental).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@raymondjacobson
raymondjacobson merged commit 4e0c89b into main May 22, 2026
5 checks passed
@raymondjacobson
raymondjacobson deleted the etl/prod-clone-findings branch May 22, 2026 00:26
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.

1 participant