feat(03-01): create SessionIdleTimer module
- 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
This commit is contained in:
parent
88cd339a54
commit
488d94ebcf
1 changed files with 147 additions and 0 deletions
147
telegram/idle_timer.py
Normal file
147
telegram/idle_timer.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
"""
|
||||
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
|
||||
Loading…
Add table
Reference in a new issue