fix(genesis-writer): emit muted users under the User entity type - #489
Merged
Conversation
The muted-user step emitted ManageEntityLegacy with EntityType "MutedUser", but the indexer registers its mute handler under EntityType "User" with Action "Mute" — the key the SDK emits (CommentsAPI.muteUser sends EntityType.USER + Action.MUTE). No handler matched, so the dispatcher routed nothing and all 633 active mutes in the production snapshot were dropped. The writer is the side that is wrong: the indexer matches the live client contract, so only the writer changes here. The handler and its registration are untouched. Adds a test that runs the writer step against a source snapshot and replays the transaction it emits through the real mute handler, asserting the row lands in muted_users. A handler-level test cannot catch this class of bug — only replaying the writer's own key does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged
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.
The bug
The muted-user step emitted
ManageEntityLegacywithEntityType: "MutedUser",Action: "Mute". The indexer registers its mute handler underEntityTypeUser+ActionMute(pkg/etl/processors/entity_manager/muted_user.go:13-14, wired atpkg/etl/indexer.go:77-78). Nothing is registered underMutedUser, soDispatcher.Dispatchmatched no handler and every muted-user transaction was discarded.All 633 active mutes in the production snapshot (
audius_discovery_2026_08_07) were lost. Counted with the writer's own join conditions:Historically the unrouted transaction returned nil silently. Since #480 it returns
ErrNoHandlerand the migration path counts it as a rejection, which is how this surfaced.Why the writer, not the indexer
The live production contract is
User/Mute. The SDK'smuteUsersendsentityType: EntityType.USERwithAction.MUTE/Action.UNMUTE(packages/sdk/src/sdk/api/comments/CommentsAPI.ts,Action.MUTE = 'Mute'inservices/EntityManager/types.ts). The indexer matches the client; the writer was the outlier."MutedUser"appears nowhere else in the Go tree as a routed key. The handler and its registration are untouched.Third instance of this pattern
This is the third writer/indexer key mismatch found in the migration:
CommentReactionvsCommentTip/ReactvsTip/UpdateMutedUservsUserEach was silent: the writer emitted, the indexer ignored, and no count anywhere disagreed. That is the argument for #480's unrouted-transaction reporting — it is the only mechanism that turns this class of bug from invisible into a failed migration run.
Test
cmd/genesis-writer/entities_muted_user_test.gorunswriteMutedUsersagainst a Discovery-Provider-shaped source snapshot, then replays the emitted transaction through the realMuteUser()handler via a dispatcher and asserts the row reachesmuted_userswithis_delete = false. It also asserts a soft-deleted mute and a mute of a user absent from the source produce no transaction.Verified the test actually catches the bug — run against the unfixed writer:
The "found 1 transactions in total" is the point: the writer did emit, under a key nothing answers to.
Unmutes are handled, by omission
The step filters with
WHERE m.is_delete = falseand never emits anUnmute. The 28 soft-deleted rows are dropped, but that is correct rather than lossy: the new chain starts empty, so a pair that ends unmuted needs no transaction to reach the right final state.Worth flagging as an asymmetry rather than a bug —
followsandsubscriptionsmigrate their soft-deleted rows and carryis_deletein metadata, specifically so a parity check against the source can distinguish an intentional omission from real data loss. Muted users cannot be checked that way. Not expanded here.Known dangling constant
EntityTypeMutedUser = "MutedUser"remains inpkg/etl/processors/entity_manager/handler.go:43with nothing registered against it. Left in place deliberately: that block is a catalog of protocol entity types, not a registry of handled ones, and it already lists unhandled types such asEntityTypePlaylistRoute.Conflict note
Diff is confined to the
EntityTypeliteral plus a comment, and adds no newtime.Formatcall, so it should not conflict with #488's.UTC()timestamp work in the same file.🤖 Generated with Claude Code