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