perf: parallelize independent API calls (#90)
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m19s

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>
This commit is contained in:
Cal Corum 2026-03-20 11:07:51 -05:00
parent 6c49233392
commit a0979f4953
5 changed files with 80 additions and 57 deletions

View File

@ -4,6 +4,7 @@ Scorebug Commands
Implements commands for publishing and displaying live game scorebugs from Google Sheets scorecards.
"""
import asyncio
import discord
from discord.ext import commands
from discord import app_commands
@ -73,12 +74,18 @@ class ScorebugCommands(commands.Cog):
return
# Get team data for display
away_team = None
home_team = None
if scorebug_data.away_team_id:
away_team = await team_service.get_team(scorebug_data.away_team_id)
if scorebug_data.home_team_id:
home_team = await team_service.get_team(scorebug_data.home_team_id)
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)
),
(
team_service.get_team(scorebug_data.home_team_id)
if scorebug_data.home_team_id
else asyncio.sleep(0)
),
)
# Format scorecard link
away_abbrev = away_team.abbrev if away_team else "AWAY"
@ -86,7 +93,7 @@ class ScorebugCommands(commands.Cog):
scorecard_link = f"[{away_abbrev} @ {home_abbrev}]({url})"
# Store the scorecard in the tracker
self.scorecard_tracker.publish_scorecard(
await self.scorecard_tracker.publish_scorecard(
text_channel_id=interaction.channel_id, # type: ignore
sheet_url=url,
publisher_id=interaction.user.id,
@ -157,7 +164,7 @@ class ScorebugCommands(commands.Cog):
await interaction.response.defer(ephemeral=True)
# Check if a scorecard is published in this channel
sheet_url = self.scorecard_tracker.get_scorecard(interaction.channel_id) # type: ignore
sheet_url = await self.scorecard_tracker.get_scorecard(interaction.channel_id) # type: ignore
if not sheet_url:
embed = EmbedTemplate.error(
@ -179,12 +186,18 @@ class ScorebugCommands(commands.Cog):
)
# Get team data
away_team = None
home_team = None
if scorebug_data.away_team_id:
away_team = await team_service.get_team(scorebug_data.away_team_id)
if scorebug_data.home_team_id:
home_team = await team_service.get_team(scorebug_data.home_team_id)
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)
),
(
team_service.get_team(scorebug_data.home_team_id)
if scorebug_data.home_team_id
else asyncio.sleep(0)
),
)
# Create scorebug embed using shared utility
embed = create_scorebug_embed(
@ -194,7 +207,7 @@ class ScorebugCommands(commands.Cog):
await interaction.edit_original_response(content=None, embed=embed)
# Update timestamp in tracker
self.scorecard_tracker.update_timestamp(interaction.channel_id) # type: ignore
await self.scorecard_tracker.update_timestamp(interaction.channel_id) # type: ignore
except SheetsException as e:
embed = EmbedTemplate.error(

View File

@ -11,6 +11,7 @@ The injury rating format (#p##) encodes both games played and rating:
- Remaining: Injury rating (p70, p65, p60, p50, p40, p30, p20)
"""
import asyncio
import math
import random
import discord
@ -114,16 +115,14 @@ class InjuryGroup(app_commands.Group):
"""Roll for injury using 3d6 dice and injury tables."""
await interaction.response.defer()
# Get current season
current = await league_service.get_current_state()
# Get current season and search for player in parallel
current, players = await asyncio.gather(
league_service.get_current_state(),
player_service.search_players(player_name, limit=10),
)
if not current:
raise BotException("Failed to get current season information")
# Search for player using the search endpoint (more reliable than name param)
players = await player_service.search_players(
player_name, limit=10, season=current.season
)
if not players:
embed = EmbedTemplate.error(
title="Player Not Found",
@ -530,16 +529,14 @@ class InjuryGroup(app_commands.Group):
await interaction.followup.send(embed=embed, ephemeral=True)
return
# Get current season
current = await league_service.get_current_state()
# Get current season and search for player in parallel
current, players = await asyncio.gather(
league_service.get_current_state(),
player_service.search_players(player_name, limit=10),
)
if not current:
raise BotException("Failed to get current season information")
# Search for player using the search endpoint (more reliable than name param)
players = await player_service.search_players(
player_name, limit=10, season=current.season
)
if not players:
embed = EmbedTemplate.error(
title="Player Not Found",
@ -717,16 +714,14 @@ class InjuryGroup(app_commands.Group):
await interaction.response.defer()
# Get current season
current = await league_service.get_current_state()
# Get current season and search for player in parallel
current, players = await asyncio.gather(
league_service.get_current_state(),
player_service.search_players(player_name, limit=10),
)
if not current:
raise BotException("Failed to get current season information")
# Search for player using the search endpoint (more reliable than name param)
players = await player_service.search_players(
player_name, limit=10, season=current.season
)
if not players:
embed = EmbedTemplate.error(
title="Player Not Found",

View File

@ -5,6 +5,7 @@ Implements the /submit-scorecard command for submitting Google Sheets
scorecards with play-by-play data, pitching decisions, and game results.
"""
import asyncio
from typing import Optional, List
import discord
@ -107,11 +108,13 @@ class SubmitScorecardCommands(commands.Cog):
content="🔍 Looking up teams and managers..."
)
away_team = await team_service.get_team_by_abbrev(
setup_data["away_team_abbrev"], current.season
)
home_team = await team_service.get_team_by_abbrev(
setup_data["home_team_abbrev"], current.season
away_team, home_team = await asyncio.gather(
team_service.get_team_by_abbrev(
setup_data["away_team_abbrev"], current.season
),
team_service.get_team_by_abbrev(
setup_data["home_team_abbrev"], current.season
),
)
if not away_team or not home_team:
@ -235,9 +238,13 @@ class SubmitScorecardCommands(commands.Cog):
decision["game_num"] = setup_data["game_num"]
# Validate WP and LP exist and fetch Player objects
wp, lp, sv, holders, _blown_saves = (
await decision_service.find_winning_losing_pitchers(decisions_data)
)
(
wp,
lp,
sv,
holders,
_blown_saves,
) = await decision_service.find_winning_losing_pitchers(decisions_data)
if wp is None or lp is None:
await interaction.edit_original_response(

View File

@ -4,6 +4,7 @@ Trade Builder Service
Extends the TransactionBuilder to support multi-team trades and player exchanges.
"""
import asyncio
import logging
from typing import Dict, List, Optional, Set
from datetime import datetime, timezone
@ -524,14 +525,22 @@ class TradeBuilder:
# Validate each team's roster after the trade
for participant in self.trade.participants:
team_id = participant.team.id
result.team_abbrevs[team_id] = participant.team.abbrev
if team_id in self._team_builders:
builder = self._team_builders[team_id]
roster_validation = await builder.validate_transaction(next_week)
result.team_abbrevs[participant.team.id] = participant.team.abbrev
team_ids_to_validate = [
participant.team.id
for participant in self.trade.participants
if participant.team.id in self._team_builders
]
if team_ids_to_validate:
validations = await asyncio.gather(
*[
self._team_builders[tid].validate_transaction(next_week)
for tid in team_ids_to_validate
]
)
for team_id, roster_validation in zip(team_ids_to_validate, validations):
result.participant_validations[team_id] = roster_validation
if not roster_validation.is_legal:
result.is_legal = False

View File

@ -95,7 +95,7 @@ class LiveScorebugTracker:
# Don't return - still update voice channels
else:
# Get all published scorecards
all_scorecards = self.scorecard_tracker.get_all_scorecards()
all_scorecards = await self.scorecard_tracker.get_all_scorecards()
if not all_scorecards:
# No active scorebugs - clear the channel and hide it
@ -112,17 +112,16 @@ class LiveScorebugTracker:
for text_channel_id, sheet_url in all_scorecards:
try:
scorebug_data = await self.scorebug_service.read_scorebug_data(
sheet_url, full_length=False # Compact view for live channel
sheet_url,
full_length=False, # Compact view for live channel
)
# Only include active (non-final) games
if scorebug_data.is_active:
# Get team data
away_team = await team_service.get_team(
scorebug_data.away_team_id
)
home_team = await team_service.get_team(
scorebug_data.home_team_id
away_team, home_team = await asyncio.gather(
team_service.get_team(scorebug_data.away_team_id),
team_service.get_team(scorebug_data.home_team_id),
)
if away_team is None or home_team is None: