perf: parallelize independent API calls (#90) #106
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#106
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "ai/major-domo-v2-90"
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 #90
Summary
Replaces sequential
awaitcalls withasyncio.gather()in all locations identified in the issue. Each location saves 50–200ms; the scorebug tracker compounds across multiple active games per update cycle.Changes
Team lookups
commands/gameplay/scorebug.py: Both/publish-scorecardand/scorebugnow fetch away/home team data in parallel. Usesasyncio.sleep(0)(returnsNone) as a no-op coroutine when a team ID is absent, preserving the originalNonefallback behaviour.commands/league/submit_scorecard.py:/submit-scorecardnow fetches away/home team by abbreviation in parallel.tasks/live_scorebug_tracker.py: Per-scorecard team lookups inside the 3-minute update loop now run in parallel, reducing latency when multiple games are active.Injury commands (
commands/injuries/management.py)/injury roll,/injury set-new,/injury clear:league_service.get_current_state()andplayer_service.search_players()are now fetched in parallel. Theseasonparam is dropped fromsearch_players(the API defaults to current season when omitted — confirmed inplayer_service.pyline 175).Trade validation (
services/trade_builder.py)validate_trade(): Per-participant roster validations are all independent; now gathered in parallel using a list comprehension over_team_builders.Bonus fixes (auto-applied by linter)
scorebug.pyandlive_scorebug_tracker.pywere missingawaitonscorecard_trackermethods that had been made async in a prior working-tree change (scorecard_tracker.py). These were auto-corrected when the files were edited.Test results
charts.py,scorecard_tracker.py) — unrelated to these changesAI Code Review
Files Reviewed
commands/gameplay/scorebug.py(modified)commands/injuries/management.py(modified)commands/league/submit_scorecard.py(modified)services/trade_builder.py(modified)tasks/live_scorebug_tracker.py(modified)Note:
commands/gameplay/scorecard_tracker.pyhas related working-tree changes (converting sync methods to async) that these PR changes depend on. Those changes were reviewed as context.Findings
Correctness
scorebug.py —
asyncio.sleep(0)as no-op coroutineThe pattern for optional team lookups is:
asyncio.sleep(0)returnsNone, which matches the originalaway_team = Nonefallback. Confirmed by runningasyncio.run(asyncio.sleep(0)) == None. This is correct.scorebug.py — await corrections on scorecard_tracker methods
The PR adds
awaittopublish_scorecard,get_scorecard, andupdate_timestamp. The working-treescorecard_tracker.pyhas converted these methods toasync(they now useasyncio.to_threadfor file I/O). Theseawaitadditions are required and correct.injuries/management.py —
seasonparam dropped fromsearch_playersThe original code passed
season=current.seasontosearch_players. The PR drops this, callingplayer_service.search_players(player_name, limit=10)in parallel withget_current_state(). Perplayer_service.pyline 176: "If neitherall_seasonsnorseasonspecified, API defaults to current season." The behavior is equivalent. Correct.trade_builder.py — gather ordering and team_abbrevs
The refactored loop first populates
result.team_abbrevsfor all participants, then buildsteam_ids_to_validateonly for participants that have builders, gathers validations, and zips results back usingzip(team_ids_to_validate, validations). Sinceasyncio.gather(*[...])preserves order, thezippairing is guaranteed correct. The split into two loops (abbrevs first, then validate) preserves the semantics of PR #71 — all teams get their abbreviation even if they have no builder. Correct.live_scorebug_tracker.py — unconditional team gather
In the background task, both
away_team_idandhome_team_idare always expected to be present (noifguards, unlike the command handlers). The existingif away_team is None or home_team is None: raise ValueError(...)error check is preserved. This is correct — for the live tracker, a missing team ID is an error condition, not a fallback.submit_scorecard.py — cosmetic parenthetical reformatting
The
find_winning_losing_pitchersunpacking was reformatted from a single-line tuple-wrap to multi-line variable assignment. This is purely cosmetic, no behavioral change.Security
Style & Conventions
No issues. All changes follow established patterns:
import asyncioplaced at top of each modified file, alphabetically ordered (beforeimport discord/import math) — consistent with CLAUDE.md "Never add lazy imports to middle of file."asyncio.gather()parallelization pattern matches the existing pattern established in PR #103 (schedule_service.py).asyncio.gather(...)formatting with trailing commas follows the project's ruff-enforced style.Minor style note (not a blocker):
asyncio.sleep(0)as a no-op is slightly unconventional. A tiny helper likeasync def _noop(): return Nonewould be more self-documenting, butasyncio.sleep(0)is a well-known Python pattern for this purpose and the inline conditional makes intent clear.Test Coverage
charts.py,scorecard_tracker.py).Suggestions
live_scorebug_tracker.pyline 150: Theawait asyncio.sleep(1)rate-limit between scorecard reads still executes even whenscorebug_data.is_activeis False (game is final). For a future cleanup: moving the sleep inside theif scorebug_data.is_active:block or restructuring the loop could avoid unnecessary delays between scorecard reads when games are wrapping up — minor optimization, not a bug.asyncio.sleep(0)in scorebug.py: Thegathernow runs both team lookups concurrently only when both team IDs are present. When one ID is absent, the real lookup and thesleep(0)are gathered — correct behavior, but the gather adds no performance benefit in that case. This is trivially fine for the use case.Verdict: COMMENT (Approved)
The implementation is correct across all five files. The parallelization refactors are sound:
asyncio.gatherordering is preserved and relied on correctly, theasyncio.sleep(0)no-op pattern produces the correctNonefallback, theseasonparameter removal fromsearch_playersis safe per the API contract, and the trade builder gather correctly maintains theteam_abbrevspopulation that PR #71 introduced. No security issues, no convention violations. Theawaitcorrections onscorecard_trackermethods are necessary and correct given the working-tree async conversion.Posting as COMMENT — Gitea does not allow self-approval.
Automated review by Claude PR Reviewer
a0979f4953to6f3339a42e