perf: reuse persistent aiohttp.ClientSession in GiphyService (#26)
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m5s

Both get_disappointment_gif and get_gif previously created a new
ClientSession per call. Replace with a lazily-initialised shared
session stored on the instance, eliminating per-call TCP handshake
and DNS overhead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-03-05 01:32:52 -06:00
parent f7a65706a1
commit e9dd318e5a

View File

@ -98,6 +98,13 @@ class GiphyService:
self.api_key = self.config.giphy_api_key self.api_key = self.config.giphy_api_key
self.translate_url = self.config.giphy_translate_url self.translate_url = self.config.giphy_translate_url
self.logger = get_contextual_logger(f"{__name__}.GiphyService") self.logger = get_contextual_logger(f"{__name__}.GiphyService")
self._session: Optional[aiohttp.ClientSession] = None
def _get_session(self) -> aiohttp.ClientSession:
"""Return the shared aiohttp session, creating it lazily if needed."""
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
def get_tier_for_seconds(self, seconds_elapsed: Optional[int]) -> str: def get_tier_for_seconds(self, seconds_elapsed: Optional[int]) -> str:
""" """
@ -181,55 +188,53 @@ class GiphyService:
# Shuffle phrases for variety and retry capability # Shuffle phrases for variety and retry capability
shuffled_phrases = random.sample(phrases, len(phrases)) shuffled_phrases = random.sample(phrases, len(phrases))
async with aiohttp.ClientSession() as session: session = self._get_session()
for phrase in shuffled_phrases: for phrase in shuffled_phrases:
try: try:
url = f"{self.translate_url}?s={quote(phrase)}&api_key={quote(self.api_key)}" url = f"{self.translate_url}?s={quote(phrase)}&api_key={quote(self.api_key)}"
async with session.get( async with session.get(
url, timeout=aiohttp.ClientTimeout(total=5) url, timeout=aiohttp.ClientTimeout(total=5)
) as resp: ) as resp:
if resp.status == 200: if resp.status == 200:
data = await resp.json() data = await resp.json()
# Filter out Trump GIFs (legacy behavior) # Filter out Trump GIFs (legacy behavior)
gif_title = data.get("data", {}).get("title", "").lower() gif_title = data.get("data", {}).get("title", "").lower()
if "trump" in gif_title: if "trump" in gif_title:
self.logger.debug( self.logger.debug(
f"Filtered out Trump GIF for phrase: {phrase}" f"Filtered out Trump GIF for phrase: {phrase}"
)
continue
# Get the actual GIF image URL, not the web page URL
gif_url = (
data.get("data", {})
.get("images", {})
.get("original", {})
.get("url")
) )
if gif_url: continue
self.logger.info(
f"Successfully fetched GIF for phrase: {phrase}", # Get the actual GIF image URL, not the web page URL
gif_url=gif_url, gif_url = (
) data.get("data", {})
return gif_url .get("images", {})
else: .get("original", {})
self.logger.warning( .get("url")
f"No GIF URL in response for phrase: {phrase}" )
) if gif_url:
self.logger.info(
f"Successfully fetched GIF for phrase: {phrase}",
gif_url=gif_url,
)
return gif_url
else: else:
self.logger.warning( self.logger.warning(
f"Giphy API returned status {resp.status} for phrase: {phrase}" f"No GIF URL in response for phrase: {phrase}"
) )
else:
self.logger.warning(
f"Giphy API returned status {resp.status} for phrase: {phrase}"
)
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
self.logger.error( self.logger.error(f"HTTP error fetching GIF for phrase '{phrase}': {e}")
f"HTTP error fetching GIF for phrase '{phrase}': {e}" except Exception as e:
) self.logger.error(
except Exception as e: f"Unexpected error fetching GIF for phrase '{phrase}': {e}"
self.logger.error( )
f"Unexpected error fetching GIF for phrase '{phrase}': {e}"
)
# All phrases failed # All phrases failed
error_msg = f"Failed to fetch any GIF for tier: {tier_key}" error_msg = f"Failed to fetch any GIF for tier: {tier_key}"
@ -264,58 +269,58 @@ class GiphyService:
elif phrase_options is not None: elif phrase_options is not None:
search_phrase = random.choice(phrase_options) search_phrase = random.choice(phrase_options)
async with aiohttp.ClientSession() as session: session = self._get_session()
attempts = 0 attempts = 0
while attempts < 3: while attempts < 3:
attempts += 1 attempts += 1
try: try:
url = f"{self.translate_url}?s={quote(search_phrase)}&api_key={quote(self.api_key)}" url = f"{self.translate_url}?s={quote(search_phrase)}&api_key={quote(self.api_key)}"
async with session.get( async with session.get(
url, timeout=aiohttp.ClientTimeout(total=3) url, timeout=aiohttp.ClientTimeout(total=3)
) as resp: ) as resp:
if resp.status != 200: if resp.status != 200:
self.logger.warning( self.logger.warning(
f"Giphy API returned status {resp.status} for phrase: {search_phrase}" f"Giphy API returned status {resp.status} for phrase: {search_phrase}"
)
continue
data = await resp.json()
# Filter out Trump GIFs (legacy behavior)
gif_title = data.get("data", {}).get("title", "").lower()
if "trump" in gif_title:
self.logger.debug(
f"Filtered out Trump GIF for phrase: {search_phrase}"
)
continue
# Get the actual GIF image URL, not the web page URL
gif_url = (
data.get("data", {})
.get("images", {})
.get("original", {})
.get("url")
) )
if gif_url: continue
self.logger.info(
f"Successfully fetched GIF for phrase: {search_phrase}",
gif_url=gif_url,
)
return gif_url
else:
self.logger.warning(
f"No GIF URL in response for phrase: {search_phrase}"
)
except aiohttp.ClientError as e: data = await resp.json()
self.logger.error(
f"HTTP error fetching GIF for phrase '{search_phrase}': {e}" # Filter out Trump GIFs (legacy behavior)
) gif_title = data.get("data", {}).get("title", "").lower()
except Exception as e: if "trump" in gif_title:
self.logger.error( self.logger.debug(
f"Unexpected error fetching GIF for phrase '{search_phrase}': {e}" f"Filtered out Trump GIF for phrase: {search_phrase}"
)
continue
# Get the actual GIF image URL, not the web page URL
gif_url = (
data.get("data", {})
.get("images", {})
.get("original", {})
.get("url")
) )
if gif_url:
self.logger.info(
f"Successfully fetched GIF for phrase: {search_phrase}",
gif_url=gif_url,
)
return gif_url
else:
self.logger.warning(
f"No GIF URL in response for phrase: {search_phrase}"
)
except aiohttp.ClientError as e:
self.logger.error(
f"HTTP error fetching GIF for phrase '{search_phrase}': {e}"
)
except Exception as e:
self.logger.error(
f"Unexpected error fetching GIF for phrase '{search_phrase}': {e}"
)
# All attempts failed # All attempts failed
error_msg = f"Failed to fetch any GIF for phrase: {search_phrase}" error_msg = f"Failed to fetch any GIF for phrase: {search_phrase}"