Merge pull request 'perf: parallelize N+1 player/creator lookups with asyncio.gather (#89)' (#118) from ai/major-domo-v2-89 into main

Reviewed-on: #118
This commit is contained in:
cal 2026-03-31 19:45:01 +00:00
commit deb40476a4
2 changed files with 56 additions and 36 deletions

View File

@ -4,6 +4,7 @@ Custom Commands Service for Discord Bot v2.0
Modern async service layer for managing custom commands with full type safety. Modern async service layer for managing custom commands with full type safety.
""" """
import asyncio
import math import math
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Optional, List, Any, Tuple from typing import Optional, List, Any, Tuple
@ -466,21 +467,28 @@ class CustomCommandsService(BaseService[CustomCommand]):
commands_data = await self.get_items_with_params(params) commands_data = await self.get_items_with_params(params)
creators = await asyncio.gather(
*[
self.get_creator_by_id(cmd_data.creator_id)
for cmd_data in commands_data
],
return_exceptions=True,
)
commands = [] commands = []
for cmd_data in commands_data: for cmd_data, creator in zip(commands_data, creators):
try: if isinstance(creator, BotException):
creator = await self.get_creator_by_id(cmd_data.creator_id)
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
except BotException as e:
# Handle missing creator gracefully
self.logger.warning( self.logger.warning(
"Skipping popular command with missing creator", "Skipping popular command with missing creator",
command_id=cmd_data.id, command_id=cmd_data.id,
command_name=cmd_data.name, command_name=cmd_data.name,
creator_id=cmd_data.creator_id, creator_id=cmd_data.creator_id,
error=str(e), error=str(creator),
) )
continue continue
if isinstance(creator, BaseException):
raise creator
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
return commands return commands
@ -662,21 +670,28 @@ class CustomCommandsService(BaseService[CustomCommand]):
commands_data = await self.get_items_with_params(params) commands_data = await self.get_items_with_params(params)
creators = await asyncio.gather(
*[
self.get_creator_by_id(cmd_data.creator_id)
for cmd_data in commands_data
],
return_exceptions=True,
)
commands = [] commands = []
for cmd_data in commands_data: for cmd_data, creator in zip(commands_data, creators):
try: if isinstance(creator, BotException):
creator = await self.get_creator_by_id(cmd_data.creator_id)
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
except BotException as e:
# Handle missing creator gracefully
self.logger.warning( self.logger.warning(
"Skipping command with missing creator", "Skipping command with missing creator",
command_id=cmd_data.id, command_id=cmd_data.id,
command_name=cmd_data.name, command_name=cmd_data.name,
creator_id=cmd_data.creator_id, creator_id=cmd_data.creator_id,
error=str(e), error=str(creator),
) )
continue continue
if isinstance(creator, BaseException):
raise creator
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
return commands return commands
@ -688,21 +703,28 @@ class CustomCommandsService(BaseService[CustomCommand]):
commands_data = await self.get_items_with_params(params) commands_data = await self.get_items_with_params(params)
creators = await asyncio.gather(
*[
self.get_creator_by_id(cmd_data.creator_id)
for cmd_data in commands_data
],
return_exceptions=True,
)
commands = [] commands = []
for cmd_data in commands_data: for cmd_data, creator in zip(commands_data, creators):
try: if isinstance(creator, BotException):
creator = await self.get_creator_by_id(cmd_data.creator_id)
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
except BotException as e:
# Handle missing creator gracefully
self.logger.warning( self.logger.warning(
"Skipping command with missing creator", "Skipping command with missing creator",
command_id=cmd_data.id, command_id=cmd_data.id,
command_name=cmd_data.name, command_name=cmd_data.name,
creator_id=cmd_data.creator_id, creator_id=cmd_data.creator_id,
error=str(e), error=str(creator),
) )
continue continue
if isinstance(creator, BaseException):
raise creator
commands.append(CustomCommand(**cmd_data.model_dump(), creator=creator))
return commands return commands

View File

@ -4,6 +4,7 @@ Decision Service
Manages pitching decision operations for game submission. Manages pitching decision operations for game submission.
""" """
import asyncio
from typing import List, Dict, Any, Optional, Tuple from typing import List, Dict, Any, Optional, Tuple
from utils.logging import get_contextual_logger from utils.logging import get_contextual_logger
@ -124,22 +125,19 @@ class DecisionService:
if int(decision.get("b_save", 0)) == 1: if int(decision.get("b_save", 0)) == 1:
bsv_ids.append(pitcher_id) bsv_ids.append(pitcher_id)
# Second pass: Fetch Player objects # Second pass: Fetch all Player objects in parallel
wp = await player_service.get_player(wp_id) if wp_id else None # Order: [wp_id, lp_id, sv_id, *hold_ids, *bsv_ids]; None IDs resolve immediately
lp = await player_service.get_player(lp_id) if lp_id else None ordered_ids = [wp_id, lp_id, sv_id] + hold_ids + bsv_ids
sv = await player_service.get_player(sv_id) if sv_id else None results = await asyncio.gather(
*[
player_service.get_player(pid) if pid else asyncio.sleep(0, result=None)
for pid in ordered_ids
]
)
holders = [] wp, lp, sv = results[0], results[1], results[2]
for hold_id in hold_ids: holders = [p for p in results[3 : 3 + len(hold_ids)] if p]
holder = await player_service.get_player(hold_id) blown_saves = [p for p in results[3 + len(hold_ids) :] if p]
if holder:
holders.append(holder)
blown_saves = []
for bsv_id in bsv_ids:
bsv = await player_service.get_player(bsv_id)
if bsv:
blown_saves.append(bsv)
return wp, lp, sv, holders, blown_saves return wp, lp, sv, holders, blown_saves