Skip to content

fix(genesis-writer): emit muted users under the User entity type - #489

Merged
rickyrombo merged 1 commit into
mainfrom
fix/genesis-writer-muted-users
Aug 10, 2026
Merged

fix(genesis-writer): emit muted users under the User entity type#489
rickyrombo merged 1 commit into
mainfrom
fix/genesis-writer-muted-users

Conversation

@rickyrombo

Copy link
Copy Markdown
Contributor

The bug

The muted-user step emitted ManageEntityLegacy with EntityType: "MutedUser", Action: "Mute". The indexer registers its mute handler under EntityTypeUser + ActionMute (pkg/etl/processors/entity_manager/muted_user.go:13-14, wired at pkg/etl/indexer.go:77-78). Nothing is registered under MutedUser, so Dispatcher.Dispatch matched 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:

muted_users total                    661
  is_delete = false                  633   <- what the writer emits
  is_delete = true                    28
writer-emitted (after user joins)    633

Historically the unrouted transaction returned nil silently. Since #480 it returns ErrNoHandler and 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's muteUser sends entityType: EntityType.USER with Action.MUTE/Action.UNMUTE (packages/sdk/src/sdk/api/comments/CommentsAPI.ts, Action.MUTE = 'Mute' in services/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:

Mismatch Rows Status
CommentReaction vs Comment 73,201 fixed in #444
Tip/React vs Tip/Update 16,424 being removed in #483 (tips are a cut feature)
MutedUser vs User 633 this PR

Each 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.go runs writeMutedUsers against a Discovery-Provider-shaped source snapshot, then replays the emitted transaction through the real MuteUser() handler via a dispatcher and asserts the row reaches muted_users with is_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:

=== RUN   TestWriteMutedUsers_ReachesMutedUsersTable
    entities_muted_user_test.go:120: emitted 0 User/Mute transactions, want 1 (found 1 transactions in total)
--- FAIL: TestWriteMutedUsers_ReachesMutedUsersTable (0.77s)

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 = false and never emits an Unmute. 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 — follows and subscriptions migrate their soft-deleted rows and carry is_delete in 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 in pkg/etl/processors/entity_manager/handler.go:43 with 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 as EntityTypePlaylistRoute.

Conflict note

Diff is confined to the EntityType literal plus a comment, and adds no new time.Format call, so it should not conflict with #488's .UTC() timestamp work in the same file.

🤖 Generated with Claude Code

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>
@rickyrombo
rickyrombo merged commit e590681 into main Aug 10, 2026
3 checks passed
@rickyrombo
rickyrombo deleted the fix/genesis-writer-muted-users branch August 10, 2026 17:23
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