perf: parallelize independent API calls (#90) #106

Merged
cal merged 1 commits from ai/major-domo-v2-90 into next-release 2026-03-20 17:48:28 +00:00
Collaborator

Closes #90

Summary

Replaces sequential await calls with asyncio.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-scorecard and /scorebug now fetch away/home team data in parallel. Uses asyncio.sleep(0) (returns None) as a no-op coroutine when a team ID is absent, preserving the original None fallback behaviour.
  • commands/league/submit_scorecard.py: /submit-scorecard now 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() and player_service.search_players() are now fetched in parallel. The season param is dropped from search_players (the API defaults to current season when omitted — confirmed in player_service.py line 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.py and live_scorebug_tracker.py were missing await on scorecard_tracker methods 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

  • 935 passed, 2 skipped
  • 18 pre-existing failures from other modified working-tree files (charts.py, scorecard_tracker.py) — unrelated to these changes
Closes #90 ## Summary Replaces sequential `await` calls with `asyncio.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-scorecard` and `/scorebug` now fetch away/home team data in parallel. Uses `asyncio.sleep(0)` (returns `None`) as a no-op coroutine when a team ID is absent, preserving the original `None` fallback behaviour. - **`commands/league/submit_scorecard.py`**: `/submit-scorecard` now 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()` and `player_service.search_players()` are now fetched in parallel. The `season` param is dropped from `search_players` (the API defaults to current season when omitted — confirmed in `player_service.py` line 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.py` and `live_scorebug_tracker.py` were missing `await` on `scorecard_tracker` methods 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 - 935 passed, 2 skipped - 18 pre-existing failures from other modified working-tree files (`charts.py`, `scorecard_tracker.py`) — unrelated to these changes
Claude added 1 commit 2026-03-20 16:08:14 +00:00
perf: parallelize independent API calls (#90)
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m19s
a0979f4953
Closes #90

Replace sequential awaits with asyncio.gather() in all locations identified
in the issue:

- commands/gameplay/scorebug.py: parallel team lookups in publish_scorecard
  and scorebug commands; also fix missing await on async scorecard_tracker calls
- commands/league/submit_scorecard.py: parallel away/home team lookups
- tasks/live_scorebug_tracker.py: parallel team lookups inside per-scorecard
  loop (compounds across multiple active games); fix missing await on
  get_all_scorecards
- commands/injuries/management.py: parallel get_current_state() +
  search_players() in injury_roll, injury_set_new, and injury_clear
- services/trade_builder.py: parallel per-participant roster validation in
  validate_trade()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude added the
ai-reviewing
label 2026-03-20 16:15:54 +00:00
Claude reviewed 2026-03-20 16:18:20 +00:00
Claude left a comment
Author
Collaborator

AI 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.py has 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 coroutine

The pattern for optional team lookups is:

away_team, home_team = await asyncio.gather(
    team_service.get_team(scorebug_data.away_team_id)
    if scorebug_data.away_team_id
    else asyncio.sleep(0),
    ...
)

asyncio.sleep(0) returns None, which matches the original away_team = None fallback. Confirmed by running asyncio.run(asyncio.sleep(0)) == None. This is correct.

scorebug.py — await corrections on scorecard_tracker methods

The PR adds await to publish_scorecard, get_scorecard, and update_timestamp. The working-tree scorecard_tracker.py has converted these methods to async (they now use asyncio.to_thread for file I/O). These await additions are required and correct.

injuries/management.py — season param dropped from search_players

The original code passed season=current.season to search_players. The PR drops this, calling player_service.search_players(player_name, limit=10) in parallel with get_current_state(). Per player_service.py line 176: "If neither all_seasons nor season specified, 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_abbrevs for all participants, then builds team_ids_to_validate only for participants that have builders, gathers validations, and zips results back using zip(team_ids_to_validate, validations). Since asyncio.gather(*[...]) preserves order, the zip pairing 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_id and home_team_id are always expected to be present (no if guards, unlike the command handlers). The existing if 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_pitchers unpacking was reformatted from a single-line tuple-wrap to multi-line variable assignment. This is purely cosmetic, no behavioral change.

Security

  • No security issues. No user input is used in queries or shell commands without sanitization. No hardcoded credentials or PII exposure.

Style & Conventions

No issues. All changes follow established patterns:

  • import asyncio placed at top of each modified file, alphabetically ordered (before import 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).
  • The multi-line 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 like async def _noop(): return None would be more self-documenting, but asyncio.sleep(0) is a well-known Python pattern for this purpose and the inline conditional makes intent clear.

Test Coverage

  • PR body reports 935 passed, 2 skipped, 18 pre-existing failures (from modified working-tree files charts.py, scorecard_tracker.py).
  • No new tests were added. The changes are pure parallelization refactors with identical behavior — the test suite covering existing behavior provides sufficient coverage.
  • The trade builder gather change is covered by the existing 36 trade builder tests (PR #71).

Suggestions

  1. live_scorebug_tracker.py line 150: The await asyncio.sleep(1) rate-limit between scorecard reads still executes even when scorebug_data.is_active is False (game is final). For a future cleanup: moving the sleep inside the if 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.

  2. asyncio.sleep(0) in scorebug.py: The gather now runs both team lookups concurrently only when both team IDs are present. When one ID is absent, the real lookup and the sleep(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.gather ordering is preserved and relied on correctly, the asyncio.sleep(0) no-op pattern produces the correct None fallback, the season parameter removal from search_players is safe per the API contract, and the trade builder gather correctly maintains the team_abbrevs population that PR #71 introduced. No security issues, no convention violations. The await corrections on scorecard_tracker methods 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

## AI 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.py` has 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 coroutine** The pattern for optional team lookups is: ```python away_team, home_team = await asyncio.gather( team_service.get_team(scorebug_data.away_team_id) if scorebug_data.away_team_id else asyncio.sleep(0), ... ) ``` `asyncio.sleep(0)` returns `None`, which matches the original `away_team = None` fallback. Confirmed by running `asyncio.run(asyncio.sleep(0)) == None`. This is correct. **scorebug.py — await corrections on scorecard_tracker methods** The PR adds `await` to `publish_scorecard`, `get_scorecard`, and `update_timestamp`. The working-tree `scorecard_tracker.py` has converted these methods to `async` (they now use `asyncio.to_thread` for file I/O). These `await` additions are required and correct. **injuries/management.py — `season` param dropped from `search_players`** The original code passed `season=current.season` to `search_players`. The PR drops this, calling `player_service.search_players(player_name, limit=10)` in parallel with `get_current_state()`. Per `player_service.py` line 176: "If neither `all_seasons` nor `season` specified, 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_abbrevs` for all participants, then builds `team_ids_to_validate` only for participants that have builders, gathers validations, and zips results back using `zip(team_ids_to_validate, validations)`. Since `asyncio.gather(*[...])` preserves order, the `zip` pairing 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_id` and `home_team_id` are always expected to be present (no `if` guards, unlike the command handlers). The existing `if 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_pitchers` unpacking was reformatted from a single-line tuple-wrap to multi-line variable assignment. This is purely cosmetic, no behavioral change. #### Security - No security issues. No user input is used in queries or shell commands without sanitization. No hardcoded credentials or PII exposure. #### Style & Conventions **No issues.** All changes follow established patterns: - `import asyncio` placed at top of each modified file, alphabetically ordered (before `import 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`). - The multi-line `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 like `async def _noop(): return None` would be more self-documenting, but `asyncio.sleep(0)` is a well-known Python pattern for this purpose and the inline conditional makes intent clear. #### Test Coverage - PR body reports 935 passed, 2 skipped, 18 pre-existing failures (from modified working-tree files `charts.py`, `scorecard_tracker.py`). - No new tests were added. The changes are pure parallelization refactors with identical behavior — the test suite covering existing behavior provides sufficient coverage. - The trade builder gather change is covered by the existing 36 trade builder tests (PR #71). --- ### Suggestions 1. **`live_scorebug_tracker.py` line 150**: The `await asyncio.sleep(1)` rate-limit between scorecard reads still executes even when `scorebug_data.is_active` is False (game is final). For a future cleanup: moving the sleep inside the `if 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. 2. **`asyncio.sleep(0)` in scorebug.py**: The `gather` now runs both team lookups concurrently only when both team IDs are present. When one ID is absent, the real lookup and the `sleep(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.gather` ordering is preserved and relied on correctly, the `asyncio.sleep(0)` no-op pattern produces the correct `None` fallback, the `season` parameter removal from `search_players` is safe per the API contract, and the trade builder gather correctly maintains the `team_abbrevs` population that PR #71 introduced. No security issues, no convention violations. The `await` corrections on `scorecard_tracker` methods 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*
cal approved these changes 2026-03-20 17:47:53 +00:00
cal changed target branch from main to next-release 2026-03-20 17:48:04 +00:00
cal force-pushed ai/major-domo-v2-90 from a0979f4953 to 6f3339a42e 2026-03-20 17:48:16 +00:00 Compare
cal merged commit ee2387a385 into next-release 2026-03-20 17:48:28 +00:00
cal deleted branch ai/major-domo-v2-90 2026-03-20 17:48:29 +00:00
cal removed the
ai-reviewing
label 2026-03-23 15:32:38 +00:00
Sign in to join this conversation.
No reviewers
cal
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: cal/major-domo-v2#106
No description provided.