perf: parallelize schedule_service API calls with asyncio.gather #103
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#103
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "ai/major-domo-v2-88"
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?
Fixes #88
Summary
for week in week_range: await self.get_week_schedule(...)loops withasyncio.gather()inget_team_schedule(),get_recent_games(), andget_upcoming_games()get_team_schedule(): up to 18 sequential HTTP requests → all concurrentget_recent_games(): 2 sequential requests → concurrentget_upcoming_games(): up to 18 sequential requests with early exit → all concurrent (early exit removed — unnecessary when all weeks fetch in parallel)test_services_schedule.py(first coverage for this service)Test plan
python -m pytest tests/test_services_schedule.py -v— 16/16 passpython -m pytest --tb=short -q— full suite passes (18 pre-existing failures in unrelated chart/scorebug tests)/simplifyreview — refactored test helpers to use existingGameFactory/TeamFactory🤖 Generated with Claude Code
perf: parallelize schedule_service week fetches with asyncio.gather (#88)to perf: parallelize schedule_service API calls with asyncio.gatherAI Code Review
Files Reviewed
services/schedule_service.py(modified)tests/test_services_schedule.py(added)bot.py(modified — maintenance tree + duplicate attribute bug)commands/admin/management.py(modified — type: ignore comment)tests/test_bot_maintenance_tree.py(added)Findings
Correctness
schedule_service.py — all three parallelization changes are correct.
get_team_schedule: Sequential loop overweek_rangereplaced withasyncio.gather(*[...]). The gather result is a tuple of lists in the same order as the coroutines, so the downstream filter loop is correct.get_recent_games: List comprehension[(10 - offset) for offset in range(weeks_back) if (10 - offset) > 0]correctly filters non-positive week numbers before constructing the gather. Semantically equivalent to the oldif week <= 0: breakguard, but without the early-exit behaviour — which is fine because the filter is applied before any I/O.get_upcoming_games: Early-exit (if upcoming_games and len(upcoming_games) >= 20: break) correctly removed. It was an optimisation that no longer makes sense when all weeks are fetched in a single parallel batch. Theweeks_aheadparameter in the signature still exists but is now ignored — the implementation always fetches all 18 weeks regardless of its value. This is a latent behaviour mismatch between the docstring/signature and the implementation (see Suggestions below).bot.py — pre-existing duplicate attribute assignment confirmed still present.
This was flagged in the PR #84 review and is a merge artifact from PR #83. Lines 121 and 123 of
bot.pyboth assignself.maintenance_mode: bool = False. This PR's diff adds theMaintenanceAwareTreeclass and thetree_clskwarg but does not introduce the duplicate — it was already innext-release. The duplicate is harmless at runtime (second assignment overwrites first with the same value) but is dead code. Thetest_maintenance_mode_default_is_falsetest passes despite this because it only asserts the final value, not the number of assignments.bot.py — dead decorator block in setup_hook still present.
The old
@self.tree.interaction_check/maintenance_checkdecorator block (lines 129–143 in the currentbot.py) is still insetup_hook. It was supposed to be removed as part of the fix introduced in PR #83, and was flagged as still-present dead code in the PR #84 review. This PR does not touchsetup_hookand so does not remove it. It generates a RuntimeWarning (unawaited coroutine) whenever the bot starts. This is a pre-existing issue, not introduced by this PR.test_services_schedule.py — correctness of test logic verified.
test_filters_by_team_case_insensitive:mock.side_effect = [week1, week2]withweeks=2triggersasyncio.gatherover two coroutines. WithAsyncMock,side_effectas a list pops one value per call, which works correctly even insidegather. Result count assertion (len(result) == 2) is correct.test_skips_negative_weeksassertion comment says "only 10 valid weeks" (weeks 10 down to 1).10 - 0 = 10,10 - 14 = -4— the filter removes offsets 10–14, leaving offsets 0–9, which is 10 weeks. Correct.test_returns_only_incomplete_gamesassertslen(result) == 18becausemock.return_value = [completed, upcoming]is called for all 18 weeks, returning 1 incomplete game each time. This is a valid whitebox assertion but tightly couples the test to the implementation detail of always fetching exactly 18 weeks. If that constant changes, the assertion will break. Low risk, worth noting.test_bot_maintenance_tree.py —
test_maintenance_on_message_has_no_emojiis overly strict.The test checks
unicodedata.category(ch) != "So"(Other Symbol) for each character in the maintenance message. This correctly catches most emoji. However, the project's "no emoji in bot messages" rule (fromdiscord-app-v2/CLAUDE.md) specifically applies toEmbedTemplate.success/error/warning/info/loading()title parameters — it is a pattern about double-emoji in embeds, not a blanket ban on all Unicode symbols in all messages. The current test would falsely fail if the message were changed to include a legitimate typographic symbol (e.g., a copyright sign,©, categorySo). This is a minor over-constraint, not a blocking issue._patch_discord_app_commandsautouse fixture is a no-op.The fixture at
tests/test_bot_maintenance_tree.py:68yields immediately with no setup or teardown. The docstring says it stubs the parentCommandTree.__init__, but the body does nothing. The tests pass becauseinteraction_checkis called as an unbound method (tree_cls.interaction_check(MagicMock(), interaction)) without ever constructing aMaintenanceAwareTreeinstance, so__init__is never invoked. The fixture is not harmful but is misleading — it promises patching it does not perform.Security
No security issues. The
getattr(bot, "maintenance_mode", False)safe-access is correct and avoidsAttributeErrorduring startup races. Admin check usesguild_permissions.administrator, which is the appropriate guild-level flag.Style & Conventions
All modified files follow the project's double-quote, trailing-comma style (enforced by the linter in previous PRs).
asyncioimport is at module level, not lazy-imported mid-file, consistent with CLAUDE.md. Docstrings are present on all new methods and test classes.@logged_commandandself.loggerare not applicable here (schedule_service and tests, not a cog command).Suggestions
weeks_aheadparameter inget_upcoming_gamesis now ignored. The signature isget_upcoming_games(self, season: int, weeks_ahead: int = 6)but the body always gathersrange(1, 19). Either remove the parameter (breaking change for callers) or restore the behaviour by usingweeks_aheadin the gather range. The PR body notes the early exit was "unnecessary when all weeks fetch in parallel" — butweeks_aheadwas a different concept (how far ahead to look, not an optimisation gate). Worth a follow-up to reconcile the signature and implementation.Dead code in
bot.py— duplicateself.maintenance_modeassignment and stale@self.tree.interaction_checkblock insetup_hook. These are pre-existing issues from PR #83 that survived the merge. A targeted cleanup PR removing lines 123 (self.maintenance_mode: bool = Falseduplicate) and the entiremaintenance_checkinner function block (lines 129–143) would be appropriate._patch_discord_app_commandsfixture. Either remove it (the tests work without it) or give it a real body that patchesdiscord.app_commands.CommandTree.__init__to a no-op, matching its stated intent.test_returns_only_incomplete_gamesmagic number. Consider replacing the literal18with a named constant or deriving it from the implementation'srange(1, 19)to make the coupling explicit.Verdict: COMMENT
The schedule service parallelization is correct and well-tested. The 16 new tests cover the parallelization paths, edge cases, and the
group_games_by_seriesgrouping logic. No security issues. The suggestions above (particularly theweeks_aheadparameter mismatch) are worth addressing in a follow-up, but none of them block merging. Posting as COMMENT per convention — Gitea does not allow self-approval.Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
services/schedule_service.py(modified)commands/league/schedule.py(modified — reformatting + call-site update)bot.py(modified — maintenance-mode duplicate assignment still present)commands/admin/management.py(modified —type: ignoreadded)tests/test_services_schedule.py(added)tests/test_bot_maintenance_tree.py(added)Findings
Correctness
schedule_service.py —
get_recent_games: hardcoded "current week = 10"This is a pre-existing design limitation, not introduced by this PR, but the parallelization makes it more visible. The weeks-to-fetch list is computed as:
Week 10 is still hardcoded as the anchor, so this method is only correct when the league is actually at week 10. This was true in the sequential version too; the PR preserves the behaviour faithfully. No change required here, but worth a follow-up issue if the service is ever used in production for real-time recent games.
test_sorted_descending_by_week_and_game_num— test usesmock.side_effectwith a list againstasyncio.gatherasyncio.gathercallsget_week_schedule(season, 10)andget_week_schedule(season, 9)concurrently.side_effectas a list pops values in call order, which for a list-basedside_effectis the ordergatherdispatches them. Becausegatherdispatches in argument order and Python's mock pops in FIFO order, week 10 gets[game_w10]and week 9 gets[game_w9]. This is technically correct but relies onasyncio.gatherpreserving dispatch order (which it does — the coroutines are started in argument order). The assertion passes and is trustworthy.bot.py— duplicateself.maintenance_mode: bool = Falseassignment (lines 121 and 123)This is a pre-existing merge artifact noted in the PR #84 review. This PR adds
test_bot_maintenance_tree.pywhich importsbot.py, so the duplicate assignment is now tested code. Thetest_maintenance_mode_default_is_falsetest still passes because assigning the attribute twice has no runtime effect — but the dead line is still a code smell. Not introduced by this PR.bot.py— dead@self.tree.interaction_checkdecorator block (lines 129–143) NOT removedThe diff shows
bot.pywas modified only to addMaintenanceAwareTreeand thetree_clskwarg plus the duplicatemaintenance_modeline. The dead decorator block insetup_hook(lines 129–143) is still present in the file. This meansbot.pynow has two maintenance-check paths: the correct one (viaMaintenanceAwareTree.interaction_check) and the dead decorator that generates aRuntimeWarning: coroutine 'maintenance_check' was never awaitedat startup because@self.tree.interaction_checkis not a valid decorator in discord.py.This was the core bug of issue #82 / PR #84. The new
test_bot_maintenance_tree.pythoroughly tests theMaintenanceAwareTreeclass but does not test or exercise the dead decorator block, so its continued presence is not caught by the new tests. The decorator block should be removed before merge.get_upcoming_games— early-exit removal trade-offThe old code had an early exit at 20 games to limit unnecessary requests. The new version always fetches all 18 weeks. For a mid-season call where weeks 1–10 are complete and weeks 11–18 are upcoming, this is strictly better (all 18 fetches run in parallel vs. the old sequential scan that would have stopped early anyway). The trade-off is that an all-season call now always costs 18 parallel HTTP requests rather than stopping as soon as a sufficient window of upcoming games is found. For this league size this is acceptable, and the PR body documents it. No objection.
commands/league/schedule.pyline 160 —weeks_aheadremoved from callerThe old call passed
weeks_ahead=1, meaning only 1 week ahead was scanned. After the parameter removal, the new call fetches all 18 weeks. This is a behaviour change: the/schedulecommand will now return upcoming games from all future weeks rather than just the next week. This may or may not be desirable depending on what the embed displays. The embed caps output at 5 games, so the visual change is minor; the extra API calls are the real cost. Worth noting for manual testing.Security
No issues found. No user input is used in API parameters beyond integers that are passed directly. No credentials or tokens are touched.
Style & Conventions
commands/league/schedule.py— the diff is predominantly mechanical reformatting (trailing whitespace, trailing commas, line-length wrapping). All changes conform to project style. The formatter also fixed a missing newline at end-of-file.commands/admin/management.py— adding# type: ignore[attr-defined]is correct sincecommands.Botdoes not declaremaintenance_mode; the attribute is set dynamically inSBABot.__init__.Test files — docstrings explain both "what" and "why" as required by CLAUDE.md. Factory usage is correct.
autousefixtures are not needed here (no module-level state) but the pattern is harmless.Suggestions
Remove the dead
@self.tree.interaction_checkblock fromsetup_hook(lines 129–143 inbot.py). This is the actual fix for issue #82 and is still outstanding. Without it, aRuntimeWarningfires at startup and the decorator silently competes with the tree subclass.Fix the duplicate
self.maintenance_mode: bool = Falseassignment (lines 121 and 123 inbot.py). One of these lines should be removed.get_recent_gamesanchoring at week 10 is a pre-existing design gap. A follow-up issue to use the current week fromleague_servicewould make recent-games queries correct across the full season.Verdict: COMMENT
The
asyncio.gatherparallelization inschedule_service.pyis correctly implemented — gather dispatch order is preserved, all three methods produce the same results as the sequential versions, and the 16 new tests are well-structured and cover the critical paths.However,
bot.pystill contains the dead@self.tree.interaction_checkdecorator block (lines 129–143) that was the root cause of issue #82, and the duplicatemaintenance_modeassignment (lines 121/123). Both of these were flagged in the PR #84 review. This PR adds tests that importbot.pyand thoroughly testMaintenanceAwareTree, but does not clean up the dead code. These two issues inbot.pyshould be resolved before merge — they are one-line and one-block removals.The schedule service changes themselves are ready to merge.
Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
services/schedule_service.py(modified)commands/league/schedule.py(modified)bot.py(modified)commands/admin/management.py(modified)tests/test_services_schedule.py(added)tests/test_bot_maintenance_tree.py(added)Findings
Correctness
asyncio.gatherparallelization — correct in all three methods.get_week_schedulewraps its body in try/except and returns[]on any error, so individual week failures degrade gracefully and won't propagate throughgather.weeks_aheadparameter removal — correct. The old parameter was dead code:get_upcoming_gamesalways scannedrange(1, 19)regardless of theweeks_aheadvalue. Removing it and updating theschedule.pycall site is the honest fix.if upcoming_games and len(upcoming_games) >= 20: breakis no longer applicable once all 18 weeks are fetched in parallel. Removing it is correct. Tradeoff: always 18 API calls vs. potentially fewer. For a standard mid-season scenario this is acceptable.get_recent_gamessorts reverse=True (most recent first),get_upcoming_gamessorts ascending (soonest first). Both preserved correctly.get_recent_gameshardcoded week 10 — Pre-existing issue, not introduced here.weeks_to_fetch = [(10 - offset) for offset in range(weeks_back) if (10 - offset) > 0]still hardcodes the "current week" as 10. This was already a known limitation in the original code.Bundled Unrelated Changes
This "perf: parallelize schedule_service" PR also includes changes to
bot.py,commands/admin/management.py, and a newtests/test_bot_maintenance_tree.py. These are from a different concern (theMaintenanceAwareTreefix) and should ideally have been separate. They appear here because theai/branch was cut before PR #83 hotfixedmaindirectly — sonext-releaselegitimately lackedMaintenanceAwareTreeand needed it. The implementation is correct. Thetest_bot_maintenance_tree.pytests are thorough (8 cases covering maintenance-off, maintenance-on/non-admin, maintenance-on/admin, non-Member user, missing attribute fallback, and default-false attribute).Pre-existing Bug (not introduced here)
bot.pylines 121 and 123 assignself.maintenance_mode: bool = Falsetwice — this is a merge artifact from PR #83 that was already noted in the previous review. Not introduced by this PR, but also not cleaned up.Security
Style & Conventions
commands/league/schedule.pyreceived cosmetic reformatting (single→double quotes, trailing commas, blank-line cleanup). Consistent with linter style seen in previous PRs — cosmetic only.GameFactory/TeamFactorypatterns as required per the/simplifyreview mentioned in the PR body.Suggestions
week = 10 - offsetinget_recent_gamesshould eventually be replaced with a call toleague_service.get_current_week()so the "recent games" window is accurate regardless of where in the season the bot is. Could be a follow-up issue.test_services_schedule.pyTestGetRecentGames.test_sorted_descending_by_week_and_game_numverifies sort order but only checks the.weekattribute. Consider also asserting on.game_numfor the descending sort to fully exercise the composite key.Verdict: COMMENT
Schedule parallelization is correct and well-tested — 16 new tests cover all three methods plus
group_games_by_series. TheMaintenanceAwareTreechanges bundled here were necessary fornext-release(since the PR #83 hotfix targetedmaindirectly) and are also correct. No blocking issues.Automated review by Claude PR Reviewer