perf: eliminate redundant API calls in trade views (#94) #116
No reviewers
Labels
No Label
ai-changes-requested
ai-pr-opened
ai-reviewed
ai-reviewing
ai-working
in-next-release
status/in-progress
status/pr-open
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/major-domo-v2#116
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "ai/major-domo-v2#94"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #94
Summary
create_trade_embed: movedvalidate_trade()call before theif builder.is_emptyblock so it executes once instead of twice (was called at both color-determination and status-text steps)TradeAcceptanceView: added_checked_team: Optional[Team] = Noneto__init__;interaction_checknow stores the resolved team onself._checked_teamafter validation passes;accept_buttonandreject_buttonread fromself._checked_teaminstead of calling_get_user_team()againFiles Changed
views/trade_embed.pyTest Results
973 passed, 2 skipped (4 pre-existing failures in voice/scorebug tests unrelated to this change)
AI Code Review
Files Reviewed
views/trade_embed.py(modified)Findings
Correctness
create_trade_embedoptimization — Correct ✓Moving
validation = await builder.validate_trade()before theif builder.is_emptyblock is sound. In the original code,validate_trade()was always called at least once (the second call at old line 769 was unconditional). The optimization reduces this to a single call whenbuilder.is_emptyis False. For theis_emptycase, the call count is unchanged (1 → 1). Thevalidationvariable is always defined before theif validation.is_legalblock. No correctness issues here.TradeAcceptanceView._checked_team— Race condition bug ✗self._checked_teamis a single instance variable on the view, which is shared across all users who interact with it. This introduces a race condition because discord.py dispatches each interaction as a separateasyncio.ensure_futuretask. Two coroutines CAN interleave at anyawaitpoint.Vulnerable sequence:
interaction_checkawaits, setsself._checked_team = Team_A, returns Trueaccept_buttoncallback runs, User B clicks Accept → Task B:interaction_checkawaits, setsself._checked_team = Team_B, returns Trueaccept_buttonfires, readsself._checked_team = Team_B— Team A GM's click is credited to Team BResult:
self.builder.accept_trade(team_id)is called with Team B's ID, recording an acceptance for Team B when Team A's GM clicked. Team B's GM would then see "Team B has already accepted" if they try to accept legitimately.The original pattern (calling
_get_user_team(interaction)directly in each handler) was correct becauseinteractionis bound to the specific user who clicked; the view state is not.Required fix — Use a per-user dict instead of a single field:
This is safe because each user's check result is keyed by their Discord user ID, preventing cross-user overwrite.
Security
Style & Conventions
_checked_teamtype annotationOptional[Team]follows existing patterns. ✓create_trade_embedrestructure is clean and readable. ✓Suggestions
_checked_teamsdict grows unboundedly (one entry per user who interacts). For a small league this is negligible, but clearing the entry after the button handler completes would be tidy. Not a blocker.Verdict: REQUEST_CHANGES
The
create_trade_embeddeduplication is correct and should be kept. TheTradeAcceptanceView._checked_teamoptimization introduces a real race condition:asyncio.ensure_futureruns each interaction as a separate concurrent task, so two near-simultaneous button clicks can overwrite the shared_checked_teamand credit the wrong team's acceptance. Replace the singleOptional[Team]field with adict[int, Team]keyed byinteraction.user.id.Automated review by Claude PR Reviewer
Review Feedback Addressed
Addressed the race condition identified in the review:
TradeAcceptanceView._checked_team→_checked_teams: dict[int, Team]_checked_team: Optional[Team]instance variable with_checked_teams: dict[int, Team]keyed byinteraction.user.idinteraction_checknow stores:self._checked_teams[interaction.user.id] = user_teamaccept_buttonandreject_buttonnow read:self._checked_teams.get(interaction.user.id)This prevents the race condition where two concurrent button clicks (dispatched as separate
asyncio.ensure_futuretasks) could interleave at anawaitpoint, causing User B'sinteraction_checkto overwrite the shared field before User A's button handler fired — resulting in the wrong team's ID being credited.The
create_trade_embedoptimization (hoistingvalidate_trade()before theis_emptycheck) was correct and is unchanged.977 passed, 2 skipped.
AI Code Review — Follow-up (addresses prior REQUEST_CHANGES)
Previous review (commit
6b42395) requested changes to fix a race condition inTradeAcceptanceView. This re-review covers the updated branch.Race Condition — FIXED ✓
The prior bug: a single
self._checked_team: Optional[Team]was shared across all concurrent interactions, allowing User B'sinteraction_checkto overwrite the stored team before User A's button handler ran.The fix is correct. The PR now uses:
Each user's resolved
Teamis stored under their Discord user ID. Concurrent interactions from different users can no longer overwrite each other. Theif not user_team: returnguard is a sound safety net (discord.py guaranteesinteraction_checkfires before any handler, but the guard is defensive).create_trade_embedoptimization — Correct ✓validate_trade()is now called once at the top instead of twice (once in theelsebranch, once unconditionally below). For empty builders the call count is unchanged (1 → 1); for non-empty builders it drops from 2 → 1.validationis always assigned beforeif validation.is_legal:. No correctness issues.Bundled unrelated changes
The diff includes
commands/gameplay/scorecard_tracker.py,commands/voice/cleanup_service.py, andtests/test_scorebug_bugs.py(makingScorecardTrackermethods async). These are already onmainvia the hotfix in PR #117 (merge commitbe4213a). Since the branch pre-dates that hotfix, they appear in the diff but will be no-ops on merge (mergeable: trueconfirms no conflict). No action needed, but worth noting that the PR body still describes the old (rejected)_checked_team: Optional[Team] = Noneimplementation and lists onlyviews/trade_embed.pyas changed — both are stale.Verdict: APPROVED ✓
The requested race-condition fix is correctly implemented.
create_trade_embedoptimization is sound. Safe to merge.Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
views/trade_embed.py(modified — primary change)commands/gameplay/scorecard_tracker.py(modified — no-op on merge; already async on main via PR #117)commands/voice/cleanup_service.py(modified — no-op on merge; already awaited on main via PR #117)tests/test_scorebug_bugs.py(modified — no-op on merge; already async on main via PR #117)Findings
Correctness
create_trade_embed— validate_trade() hoist (lines 710–780):validation = await builder.validate_trade()is now called once before theif builder.is_emptybranch. For non-empty builders this reduces two sequential API calls to one. For empty builders the call count is unchanged (was already called once at the bottom). Correct.validationis now defined on all code paths before use at theif validation.is_legalcheck. No NameError risk.builder.is_empty, validate_trade() is still called even though the result is not used for color. This is a minor overhead but not a regression (old code also called it once for empty builders, just later).TradeAcceptanceView— per-user team cache (lines 328–425):self._checked_teams: dict[int, Team] = {}correctly isolates per-user state using the Discord user ID as key.interaction_checkpopulates_checked_teams[interaction.user.id]after both validation guards pass. discord.py guaranteesinteraction_checkruns before any button handler, so the cache is always populated whenaccept_buttonorreject_buttonexecute.self._checked_teams.get(interaction.user.id)with aif not user_team: returnsafety guard. Correct._checked_team: Optional[Team](single slot) could be overwritten by a second user'sinteraction_checkbetween the await point and the button handler execution, crediting the wrong team's acceptance. The dict-keyed approach eliminates this.Scorecard changes: Verified that
scorecard_tracker.pyon main already has all async signatures from PR #117. The hunks in this diff are no-ops on merge.mergeable: trueconfirms no conflicts.Security
interaction.user.id(int) as cache key is correct and safe.Style & Conventions
dict[int, Team](lowercase built-in generic) is consistent with modern Python 3.10+ style used elsewhere in the file.if not user_team:guard in button handlers follows the project's "Raise or Return" pattern noted in CLAUDE.md.Note on PR body vs diff: The PR description mentions
_checked_team: Optional[Team] = Nonebut the actual diff implements_checked_teams: dict[int, Team] = {}. The body describes an earlier revision; the diff is correct. This is cosmetic only.Suggestions
Verdict: COMMENT
(Gitea blocks self-review approval — posting as COMMENT. Verdict: APPROVED.)
Trade embed changes are correct and complete. The
_checked_teamsdict approach properly fixes the race condition and eliminates a redundant API call per button interaction. Scorecard diff hunks are already on main and are no-ops on merge. No issues found.Automated review by Claude PR Reviewer
3195a3c71fto174ce4474d