Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/gw2/cogs/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ async def session(ctx):
else:
# End data missing and user stopped playing - finalize the session now
progress_msg = await gw2_utils.send_progress_embed(ctx)
await gw2_utils.end_session(ctx.bot, ctx.message.author, api_key)
await gw2_utils.end_session(ctx.bot, ctx.message.author, api_key, skip_delay=True)
rs_session = await gw2_session_dal.get_user_last_session(user_id)
if not rs_session or rs_session[0]["end"] is None:
await progress_msg.delete()
Expand Down Expand Up @@ -179,7 +179,7 @@ async def session(ctx):

if not is_live_snapshot and is_playing:
still_playing_msg = f"{ctx.message.author.mention}\n {gw2_messages.SESSION_USER_STILL_PLAYING}"
await gw2_utils.end_session(ctx.bot, ctx.message.author, api_key)
await gw2_utils.end_session(ctx.bot, ctx.message.author, api_key, skip_delay=True)
await ctx.send(still_playing_msg)

await progress_msg.delete()
Expand Down
1 change: 1 addition & 0 deletions src/gw2/constants/gw2_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Gw2Settings(BaseSettings):
api_retry_max_attempts: int | None = Field(default=5)
api_retry_delay: float | None = Field(default=3.0)
api_session_retry_bg_delay: float | None = Field(default=30.0)
api_session_end_delay: float | None = Field(default=300.0)


@lru_cache(maxsize=1)
Expand Down
14 changes: 12 additions & 2 deletions src/gw2/tools/gw2_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,18 @@ async def _do_start_session(bot: Bot, member: discord.Member, api_key: str, sess
await insert_start_char_deaths(bot, member, api_key, session_id)


async def end_session(bot: Bot, member: discord.Member, api_key: str) -> None:
"""End a GW2 session for a member."""
async def end_session(bot: Bot, member: discord.Member, api_key: str, *, skip_delay: bool = False) -> None:
"""End a GW2 session for a member.

Waits for the GW2 API cache to refresh before fetching end stats,
so that wallet/achievement values reflect in-game changes.
Set skip_delay=True for user-facing commands where the user is actively waiting.
"""
end_delay = _gw2_settings.api_session_end_delay
if not skip_delay and end_delay and end_delay > 0:
bot.log.debug(f"Waiting {end_delay}s for GW2 API cache to refresh before ending session for user {member.id}")
await asyncio.sleep(end_delay)

session = await get_user_stats(bot, api_key)
if not session:
bot.log.warning(f"Failed to end session for user {member.id}: unable to fetch stats from GW2 API")
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_session_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ async def test_session_start_end_lifecycle(db_session, log):
mock_gw2_api_end.call_api = AsyncMock(side_effect=_mock_call_api_end)

with patch("src.gw2.tools.gw2_utils.Gw2Client", return_value=mock_gw2_api_end):
await gw2_utils.end_session(mock_bot, mock_member, API_KEY)
await gw2_utils.end_session(mock_bot, mock_member, API_KEY, skip_delay=True)

# Verify session end JSONB was populated
sessions = await sessions_dal.get_user_last_session(USER_ID)
Expand Down Expand Up @@ -186,7 +186,7 @@ async def test_end_session_without_start_is_noop(db_session, log):
mock_gw2_api.call_api = AsyncMock(side_effect=_mock_call_api_end)

with patch("src.gw2.tools.gw2_utils.Gw2Client", return_value=mock_gw2_api):
await gw2_utils.end_session(mock_bot, mock_member, API_KEY)
await gw2_utils.end_session(mock_bot, mock_member, API_KEY, skip_delay=True)

mock_log.warning.assert_called_once()
assert "999888" in mock_log.warning.call_args[0][0]
Expand Down
37 changes: 34 additions & 3 deletions tests/unit/gw2/tools/test_gw2_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,7 @@ async def test_get_user_stats_returns_none_schedules_bg_retry(self, mock_bot, mo
mock_task = MagicMock()
mock_create_task.return_value = mock_task

await end_session(mock_bot, mock_member, "api-key")
await end_session(mock_bot, mock_member, "api-key", skip_delay=True)

mock_create_task.assert_called_once()
mock_task.add_done_callback.assert_called_once()
Expand All @@ -1031,7 +1031,7 @@ async def test_successful_end_session(self, mock_bot, mock_member):
with patch("src.gw2.tools.gw2_utils.update_end_char_deaths") as mock_update_char:
mock_update_char.return_value = None

await end_session(mock_bot, mock_member, "api-key")
await end_session(mock_bot, mock_member, "api-key", skip_delay=True)

mock_instance.update_end_session.assert_called_once()
call_arg = mock_instance.update_end_session.call_args[0][0]
Expand Down Expand Up @@ -1059,12 +1059,43 @@ async def test_end_session_no_active_session(self, mock_bot, mock_member):
mock_instance.update_end_session = AsyncMock(return_value=None)

with patch("src.gw2.tools.gw2_utils.update_end_char_deaths") as mock_update_char:
await end_session(mock_bot, mock_member, "api-key")
await end_session(mock_bot, mock_member, "api-key", skip_delay=True)

mock_instance.update_end_session.assert_called_once()
mock_update_char.assert_not_called()
mock_bot.log.warning.assert_called_once()

@pytest.mark.asyncio
async def test_end_session_waits_for_api_cache(self, mock_bot, mock_member):
"""Test that end_session waits for the API cache delay before fetching stats."""
with patch("src.gw2.tools.gw2_utils._gw2_settings") as mock_settings:
mock_settings.api_session_end_delay = 300.0

with patch("src.gw2.tools.gw2_utils.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
with patch("src.gw2.tools.gw2_utils.get_user_stats") as mock_stats:
mock_stats.return_value = None

with patch("src.gw2.tools.gw2_utils.asyncio.create_task") as mock_task:
mock_task.return_value = MagicMock()

await end_session(mock_bot, mock_member, "api-key")

mock_sleep.assert_called_once_with(300.0)

@pytest.mark.asyncio
async def test_end_session_skip_delay(self, mock_bot, mock_member):
"""Test that skip_delay=True bypasses the API cache wait."""
with patch("src.gw2.tools.gw2_utils.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
with patch("src.gw2.tools.gw2_utils.get_user_stats") as mock_stats:
mock_stats.return_value = None

with patch("src.gw2.tools.gw2_utils.asyncio.create_task") as mock_task:
mock_task.return_value = MagicMock()

await end_session(mock_bot, mock_member, "api-key", skip_delay=True)

mock_sleep.assert_not_called()


class TestGetUserStats:
"""Test cases for get_user_stats function."""
Expand Down