perf: parallelize independent API calls (#90)

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 committed by cal
parent 498fcdfe51
commit 6f3339a42e
5 changed files with 78 additions and 56 deletions

View File

@ -4,6 +4,7 @@ Scorebug Commands
Implements commands for publishing and displaying live game scorebugs from Google Sheets scorecards. Implements commands for publishing and displaying live game scorebugs from Google Sheets scorecards.
""" """
import asyncio
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord import app_commands from discord import app_commands
@ -73,12 +74,18 @@ class ScorebugCommands(commands.Cog):
return return
# Get team data for display # Get team data for display
away_team = None away_team, home_team = await asyncio.gather(
home_team = None (
if scorebug_data.away_team_id: team_service.get_team(scorebug_data.away_team_id)
away_team = await team_service.get_team(scorebug_data.away_team_id) if scorebug_data.away_team_id
if scorebug_data.home_team_id: else asyncio.sleep(0)
home_team = await team_service.get_team(scorebug_data.home_team_id) ),
(
team_service.get_team(scorebug_data.home_team_id)
if scorebug_data.home_team_id
else asyncio.sleep(0)
),
)
# Format scorecard link # Format scorecard link
away_abbrev = away_team.abbrev if away_team else "AWAY" 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})" scorecard_link = f"[{away_abbrev} @ {home_abbrev}]({url})"
# Store the scorecard in the tracker # 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 text_channel_id=interaction.channel_id, # type: ignore
sheet_url=url, sheet_url=url,
publisher_id=interaction.user.id, publisher_id=interaction.user.id,
@ -157,7 +164,7 @@ class ScorebugCommands(commands.Cog):
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
# Check if a scorecard is published in this channel # 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: if not sheet_url:
embed = EmbedTemplate.error( embed = EmbedTemplate.error(
@ -179,12 +186,18 @@ class ScorebugCommands(commands.Cog):
) )
# Get team data # Get team data
away_team = None away_team, home_team = await asyncio.gather(
home_team = None (
if scorebug_data.away_team_id: team_service.get_team(scorebug_data.away_team_id)
away_team = await team_service.get_team(scorebug_data.away_team_id) if scorebug_data.away_team_id
if scorebug_data.home_team_id: else asyncio.sleep(0)
home_team = await team_service.get_team(scorebug_data.home_team_id) ),
(
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 # Create scorebug embed using shared utility
embed = create_scorebug_embed( embed = create_scorebug_embed(
@ -194,7 +207,7 @@ class ScorebugCommands(commands.Cog):
await interaction.edit_original_response(content=None, embed=embed) await interaction.edit_original_response(content=None, embed=embed)
# Update timestamp in tracker # 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: except SheetsException as e:
embed = EmbedTemplate.error( 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) - Remaining: Injury rating (p70, p65, p60, p50, p40, p30, p20)
""" """
import asyncio
import math import math
import random import random
import discord import discord
@ -114,16 +115,14 @@ class InjuryGroup(app_commands.Group):
"""Roll for injury using 3d6 dice and injury tables.""" """Roll for injury using 3d6 dice and injury tables."""
await interaction.response.defer() await interaction.response.defer()
# Get current season # Get current season and search for player in parallel
current = await league_service.get_current_state() current, players = await asyncio.gather(
league_service.get_current_state(),
player_service.search_players(player_name, limit=10),
)
if not current: if not current:
raise BotException("Failed to get current season information") 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: if not players:
embed = EmbedTemplate.error( embed = EmbedTemplate.error(
title="Player Not Found", title="Player Not Found",
@ -530,16 +529,14 @@ class InjuryGroup(app_commands.Group):
await interaction.followup.send(embed=embed, ephemeral=True) await interaction.followup.send(embed=embed, ephemeral=True)
return return
# Get current season # Get current season and search for player in parallel
current = await league_service.get_current_state() current, players = await asyncio.gather(
league_service.get_current_state(),
player_service.search_players(player_name, limit=10),
)
if not current: if not current:
raise BotException("Failed to get current season information") 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: if not players:
embed = EmbedTemplate.error( embed = EmbedTemplate.error(
title="Player Not Found", title="Player Not Found",
@ -717,16 +714,14 @@ class InjuryGroup(app_commands.Group):
await interaction.response.defer() await interaction.response.defer()
# Get current season # Get current season and search for player in parallel
current = await league_service.get_current_state() current, players = await asyncio.gather(
league_service.get_current_state(),
player_service.search_players(player_name, limit=10),
)
if not current: if not current:
raise BotException("Failed to get current season information") 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: if not players:
embed = EmbedTemplate.error( embed = EmbedTemplate.error(
title="Player Not Found", 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. scorecards with play-by-play data, pitching decisions, and game results.
""" """
import asyncio
from typing import Optional, List from typing import Optional, List
import discord import discord
@ -107,11 +108,13 @@ class SubmitScorecardCommands(commands.Cog):
content="🔍 Looking up teams and managers..." content="🔍 Looking up teams and managers..."
) )
away_team = await team_service.get_team_by_abbrev( away_team, home_team = await asyncio.gather(
setup_data["away_team_abbrev"], current.season 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 team_service.get_team_by_abbrev(
setup_data["home_team_abbrev"], current.season
),
) )
if not away_team or not home_team: if not away_team or not home_team:
@ -235,9 +238,13 @@ class SubmitScorecardCommands(commands.Cog):
decision["game_num"] = setup_data["game_num"] decision["game_num"] = setup_data["game_num"]
# Validate WP and LP exist and fetch Player objects # 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: if wp is None or lp is None:
await interaction.edit_original_response( 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. Extends the TransactionBuilder to support multi-team trades and player exchanges.
""" """
import asyncio
import logging import logging
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set
from datetime import datetime, timezone from datetime import datetime, timezone
@ -524,14 +525,22 @@ class TradeBuilder:
# Validate each team's roster after the trade # Validate each team's roster after the trade
for participant in self.trade.participants: for participant in self.trade.participants:
team_id = participant.team.id result.team_abbrevs[participant.team.id] = participant.team.abbrev
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)
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 result.participant_validations[team_id] = roster_validation
if not roster_validation.is_legal: if not roster_validation.is_legal:
result.is_legal = False result.is_legal = False

View File

@ -95,7 +95,7 @@ class LiveScorebugTracker:
# Don't return - still update voice channels # Don't return - still update voice channels
else: else:
# Get all published scorecards # 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: if not all_scorecards:
# No active scorebugs - clear the channel and hide it # No active scorebugs - clear the channel and hide it
@ -119,11 +119,9 @@ class LiveScorebugTracker:
# Only include active (non-final) games # Only include active (non-final) games
if scorebug_data.is_active: if scorebug_data.is_active:
# Get team data # Get team data
away_team = await team_service.get_team( away_team, home_team = await asyncio.gather(
scorebug_data.away_team_id team_service.get_team(scorebug_data.away_team_id),
) team_service.get_team(scorebug_data.home_team_id),
home_team = await team_service.get_team(
scorebug_data.home_team_id
) )
if away_team is None or home_team is None: if away_team is None or home_team is None: