fix: stop push for expired device sessions (Closes #553)
CI / lint (pull_request) Successful in 1m8s
CI / build-release (pull_request) Successful in 4s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-11 07:54:56 +00:00
1 parent 1f05521094
commit 91e5b1e0c9
6 files changed
+152 -2

No files matched your search

+9
View File
@@ -205,6 +205,15 @@ async def session_management_id(session: Session) -> str:
return await asyncio.to_thread(_session_store().management_id, session.session_id)
async def managed_session_active(management_id: str) -> bool:
status = await asyncio.to_thread(
_session_store().managed_status,
management_id,
idle_timeout_seconds=idle_timeout_seconds(),
)
return status == "active"
async def touch_session(session: Session) -> bool:
return await asyncio.to_thread(
_session_store().touch,
+1
View File
@@ -94,6 +94,7 @@ async def _push_poll_loop() -> None:
_push_subscription_store,
_push_configuration(),
notifications,
session_active=dashboard_auth.managed_session_active,
lease_seconds=lease_seconds,
send_timeout_seconds=send_timeout,
)
+18 -1
View File
@@ -40,6 +40,7 @@ async def dispatch_unread_updates(
unread: Callable[[], Awaitable[dict]],
send: Callable[[dict, str], Awaitable[None]] | None = None,
*,
session_active: Callable[[str], Awaitable[bool]] | None = None,
lease_seconds: float = 60.0,
send_timeout_seconds: float = 10.0,
) -> int:
@@ -61,8 +62,24 @@ async def dispatch_unread_updates(
for item in page.get("items", [])
if isinstance(item, dict) and str(item.get("notification_id", "")).isdigit()
}
deliveries = await asyncio.to_thread(store.claim_unseen, thread_ids)
if session_active is not None:
try:
authorized = [await session_active(item.session_id) for item in deliveries]
except Exception:
# Authorization state is mandatory for delivery. Preserve subscriptions
# so a temporary registry failure can be retried safely.
return 0
for delivery, active in zip(deliveries, authorized):
if not active:
await asyncio.to_thread(store.delete_session, delivery.session_id)
deliveries = [
delivery
for delivery, active in zip(deliveries, authorized)
if active
]
count = 0
for delivery in await asyncio.to_thread(store.claim_unseen, thread_ids):
for delivery in deliveries:
for thread_id in delivery.thread_ids:
still_owner = await asyncio.to_thread(
store.acquire_dispatch_lease,
+18
View File
@@ -271,6 +271,24 @@ class SessionStore:
raise SessionStoreError("Session is no longer active")
return row[0]
def managed_status(self, management_id: str, *, idle_timeout_seconds: int) -> str:
"""Return the authorization state for a durable device identifier."""
now = int(self.clock())
try:
with self._connect() as connection:
row = connection.execute(
"SELECT expires_at, last_active_at FROM active_sessions "
"WHERE management_id = ?",
(management_id,),
).fetchone()
except (OSError, sqlite3.Error) as exc:
raise SessionStoreError("Session registry is temporarily unavailable") from exc
if row is None or row[0] <= now:
return "revoked"
if row[1] + max(1, idle_timeout_seconds) <= now:
return "idle"
return "active"
def revoke_managed(self, management_id: str) -> bool:
try:
with self._connect() as connection: