perf(room-worker): count members off denormalized u.isBot instead of per-doc regex#241
Conversation
…per-doc regex
ReconcileMemberCounts ran a $regexMatch on $u.account for every
subscription in the room, on every create/add/remove, to split user vs
app counts. The regex is not index-usable and runs once per document, so
the cost scaled with room size on every membership change.
model.Subscription already carries a u.isBot field that was never
populated. This wires it up:
- Add model.IsBotAccount as the single source of truth for the bot rule
(".bot" suffix or "p_" prefix; equivalent to the (\.bot$|^p_) regex).
- Stamp u.isBot at sub-creation in newSub, and route the add-member
inline build through newSub so there is one construction site. Also
reuse the predicate in determineRoomTypeFromPayload.
- Rewrite ReconcileMemberCounts to two index-backed counts: total subs
and bot subs ({roomId, u.isBot}); UserCount = total - bots. Deriving
by subtraction keeps legacy docs missing the field counted as users.
Recompute-and-$set preserves JetStream-redelivery idempotency.
- Add the {roomId, u.isBot} index in room-service EnsureIndexes.
Deploy ordering: ship this (writers stamp u.isBot) and backfill existing
docs before relying on the new counts, otherwise pre-existing bots count
as users until backfilled. Backfill (run once):
db.subscriptions.updateMany({}, [
{$set: {"u.isBot": {$regexMatch: {input: "$u.account", regex: "(\\.bot$|^p_)"}}}}
])
https://claude.ai/code/session_01KyEPakZnVkZKrPL5j9cjpw
📝 WalkthroughWalkthroughThis PR centralizes bot account classification via a new ChangesBot account classification and member count optimization
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
pkg/model/account_test.go (1)
1-1: ⚡ Quick winUse the package under test here.
This repository’s test convention is same-package tests;
package model_testmakes this an external test and diverges from the rest of the Go test guidelines.As per coding guidelines, "Test files live in the same package (
package main) to access unexported types."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/model/account_test.go` at line 1, Change the test package from the external package "model_test" to the same-package "model" so the test runs as an internal test and can access unexported types; update the top-level package declaration in pkg/model/account_test.go from "package model_test" to "package model" and verify imports/usages in that file still compile (adjust any references that assumed external visibility).room-worker/integration_test.go (1)
473-477: ⚡ Quick winAdd a legacy-doc case for missing
u.isBot.This test now covers the backfilled shape, but the new reconciliation behavior also depends on subscriptions that predate the field being counted as users. A second fixture/assertion for a bot-looking account without
IsBotwould lock in that edge case.As per coding guidelines, "Tests must cover: happy path, error paths, edge cases (empty collections, boundary conditions), and invalid input."
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@pkg/model/account_test.go`:
- Line 1: Change the test package from the external package "model_test" to the
same-package "model" so the test runs as an internal test and can access
unexported types; update the top-level package declaration in
pkg/model/account_test.go from "package model_test" to "package model" and
verify imports/usages in that file still compile (adjust any references that
assumed external visibility).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a6d1254c-0414-442f-97c5-d8837d93c91b
📒 Files selected for processing (8)
pkg/model/account.gopkg/model/account_test.goroom-service/store_mongo.goroom-worker/handler.goroom-worker/integration_test.goroom-worker/store.goroom-worker/store_mongo.goroom-worker/subscription_isbot_test.go
…rop duplicate index Three CodeRabbit findings on commit 5c6d14c: - authorizeRoomAppRead now verifies the room exists before allowing the admin bypass. Without this, a platform admin could query app.tabs/app.cmd-menu for a fabricated roomID and receive a plausible-looking response (global default-tab apps; empty cmd-menu). Order: member-or-admin first; if admin, then GetRoom gates. Non-admin non-members still deny in 2 round-trips. - handleGetRoomAppTabs warning logs (nil ChannelTab, unparseable URL) now carry requestId for trace correlation. - Drop my duplicate (roomId, u.isBot) subscriptions index in EnsureIndexes — PR #241 already added the same index upstream; rebase landed both and the second CreateOne is redundant. Spec doc updated to reflect the room-existence gate. Handler tests cover both new branches (admin + room-exists, admin + room-missing, admin + room-error).
…rop duplicate index Three CodeRabbit findings on commit 5c6d14c: - authorizeRoomAppRead now verifies the room exists before allowing the admin bypass. Without this, a platform admin could query app.tabs/app.cmd-menu for a fabricated roomID and receive a plausible-looking response (global default-tab apps; empty cmd-menu). Order: member-or-admin first; if admin, then GetRoom gates. Non-admin non-members still deny in 2 round-trips. - handleGetRoomAppTabs warning logs (nil ChannelTab, unparseable URL) now carry requestId for trace correlation. - Drop my duplicate (roomId, u.isBot) subscriptions index in EnsureIndexes — PR #241 already added the same index upstream; rebase landed both and the second CreateOne is redundant. Spec doc updated to reflect the room-existence gate. Handler tests cover both new branches (admin + room-exists, admin + room-missing, admin + room-error).
…rop duplicate index Three CodeRabbit findings on commit 5c6d14c: - authorizeRoomAppRead now verifies the room exists before allowing the admin bypass. Without this, a platform admin could query app.tabs/app.cmd-menu for a fabricated roomID and receive a plausible-looking response (global default-tab apps; empty cmd-menu). Order: member-or-admin first; if admin, then GetRoom gates. Non-admin non-members still deny in 2 round-trips. - handleGetRoomAppTabs warning logs (nil ChannelTab, unparseable URL) now carry requestId for trace correlation. - Drop my duplicate (roomId, u.isBot) subscriptions index in EnsureIndexes — PR #241 already added the same index upstream; rebase landed both and the second CreateOne is redundant. Spec doc updated to reflect the room-existence gate. Handler tests cover both new branches (admin + room-exists, admin + room-missing, admin + room-error).
Problem
ReconcileMemberCountsclassified each subscription as bot vs user with a$regexMatchon$u.account, inside a$groupover every subscription in the room, on every create / add / remove.$regexMatchis not index-usable and runs once per document, so the cost scales with room size on every membership change — pure waste, since the user/app split rarely changes.Fix
model.Subscriptionalready carries au.isBotfield that was never populated — the regex existed precisely because the stored field was unreliable. This wires it up and counts off it.model.IsBotAccount(.botsuffix orp_prefix; equivalent to the(\.bot$|^p_)regex used bypkg/pipelinesandroom-service).newSubnow setsu.isBotvia the predicate. The add-member path was building subs inline — routed throughnewSubso there's a single construction site.determineRoomTypeFromPayloadreuses the predicate too. room-worker'sBulkCreateSubscriptionsis the only inserter of subscription docs, so write-path coverage is complete.{roomId}and bot subs{roomId, u.isBot};UserCount = total − bots. Deriving by subtraction keeps legacy docs missing the field counted as users. Still recompute-and-$set, preserving JetStream-redelivery idempotency.{roomId, u.isBot}in room-serviceEnsureIndexesso both counts are index-only.Deploy ordering⚠️
Ship this (writers stamp
u.isBot) and backfill existing docs before the new counts are trustworthy — otherwise pre-existing bots count as users until backfilled. Backfill (run once; this is the only place the regex survives — one pass over the collection, not per-read-per-room):Tests
TestIsBotAccount(table-driven, incl. case-sensitivity androbot/alice.bot.comnegatives) andTestNewSub_SetsIsBotFromAccount— both written test-first (red → green).ReconcileMemberCountsbot-split integration test to seedu.isBoton the directly-inserted bot sub (the test bypasses the handler).make test(whole repo,-race): 0 failures.make lint: 0 issues.gosec: 0 issues.isBotis already documented indocs/client-api.mdand was always serialized, so this is a correctness fix to an existing field — no client-API schema change.🤖 This PR addresses finding #3 from the room-worker performance review; #1 and #2 are in a separate PR (branch
claude/room-worker-performance-yMxIi).Generated by Claude Code
Summary by CodeRabbit
New Features
Performance
Tests