- Asyncio-based per-session idle timeout detection - reset() method updates timestamp and creates new timer task - cancel() method stops timer on shutdown/archive - Properties for seconds_since_activity and last_activity - Automatic callback firing after configurable timeout
147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
"""
|
|
Session idle timer management for Claude Code Telegram bot.
|
|
|
|
This module provides the SessionIdleTimer class that manages per-session idle
|
|
timeouts using asyncio. Each session has its own timer that fires a callback
|
|
after a configurable timeout period. Timers reset on activity and can be
|
|
cancelled on session shutdown/archive.
|
|
|
|
Example:
|
|
async def handle_timeout(session_name: str):
|
|
print(f"Session {session_name} timed out")
|
|
|
|
timer = SessionIdleTimer("my-session", timeout_seconds=600, on_timeout=handle_timeout)
|
|
timer.reset() # Start the timer
|
|
# ... activity occurs ...
|
|
timer.reset() # Reset timer on activity
|
|
# ... no activity for 600 seconds ...
|
|
# handle_timeout("my-session") called automatically
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Awaitable, Callable, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SessionIdleTimer:
|
|
"""
|
|
Manages idle timeout for a single session.
|
|
|
|
Provides per-session timeout detection with automatic callback firing
|
|
after inactivity. Timer can be reset on activity and cancelled on shutdown.
|
|
|
|
Attributes:
|
|
session_name: Name of the session this timer tracks
|
|
timeout_seconds: Idle timeout in seconds
|
|
on_timeout: Async callback to invoke when timeout fires
|
|
last_activity: UTC timestamp of last activity
|
|
seconds_since_activity: Float seconds since last activity
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
session_name: str,
|
|
timeout_seconds: int,
|
|
on_timeout: Callable[[str], Awaitable[None]]
|
|
):
|
|
"""
|
|
Initialize SessionIdleTimer.
|
|
|
|
Args:
|
|
session_name: Name of the session to track
|
|
timeout_seconds: Idle timeout in seconds
|
|
on_timeout: Async callback(session_name) to invoke when timeout fires
|
|
"""
|
|
self.session_name = session_name
|
|
self.timeout_seconds = timeout_seconds
|
|
self.on_timeout = on_timeout
|
|
|
|
self._timer_task: Optional[asyncio.Task] = None
|
|
self._last_activity = datetime.now(timezone.utc)
|
|
|
|
logger.debug(
|
|
f"SessionIdleTimer initialized: session={session_name}, "
|
|
f"timeout={timeout_seconds}s"
|
|
)
|
|
|
|
def reset(self) -> None:
|
|
"""
|
|
Reset the idle timer.
|
|
|
|
Updates last activity timestamp to now, cancels any existing timer task,
|
|
and creates a new background task that will fire the timeout callback
|
|
after timeout_seconds of inactivity.
|
|
|
|
Call this whenever activity occurs on the session.
|
|
"""
|
|
# Update last activity timestamp
|
|
self._last_activity = datetime.now(timezone.utc)
|
|
|
|
# Cancel existing timer if running
|
|
if self._timer_task and not self._timer_task.done():
|
|
self._timer_task.cancel()
|
|
logger.debug(f"Cancelled existing timer for session '{self.session_name}'")
|
|
|
|
# Create new timer task
|
|
self._timer_task = asyncio.create_task(self._wait_for_timeout())
|
|
logger.debug(
|
|
f"Started idle timer for session '{self.session_name}': "
|
|
f"{self.timeout_seconds}s"
|
|
)
|
|
|
|
async def _wait_for_timeout(self) -> None:
|
|
"""
|
|
Background task that waits for timeout then fires callback.
|
|
|
|
Sleeps for timeout_seconds, then invokes on_timeout callback with
|
|
session name. Catches asyncio.CancelledError silently (timer was reset).
|
|
"""
|
|
try:
|
|
await asyncio.sleep(self.timeout_seconds)
|
|
|
|
# Timeout fired - call callback
|
|
logger.info(
|
|
f"Session '{self.session_name}' idle timeout fired "
|
|
f"({self.timeout_seconds}s)"
|
|
)
|
|
await self.on_timeout(self.session_name)
|
|
|
|
except asyncio.CancelledError:
|
|
# Timer was reset or cancelled - this is normal
|
|
logger.debug(f"Timer cancelled for session '{self.session_name}'")
|
|
|
|
def cancel(self) -> None:
|
|
"""
|
|
Cancel the idle timer.
|
|
|
|
Stops the background timer task if running. Used on session shutdown
|
|
or archive to prevent timeout callback from firing.
|
|
"""
|
|
if self._timer_task and not self._timer_task.done():
|
|
self._timer_task.cancel()
|
|
logger.debug(f"Cancelled timer for session '{self.session_name}'")
|
|
|
|
@property
|
|
def seconds_since_activity(self) -> float:
|
|
"""
|
|
Get seconds since last activity.
|
|
|
|
Returns:
|
|
Float seconds elapsed since last reset() call
|
|
"""
|
|
now = datetime.now(timezone.utc)
|
|
delta = now - self._last_activity
|
|
return delta.total_seconds()
|
|
|
|
@property
|
|
def last_activity(self) -> datetime:
|
|
"""
|
|
Get timestamp of last activity.
|
|
|
|
Returns:
|
|
UTC datetime of last reset() call
|
|
"""
|
|
return self._last_activity
|