SessionService.list_sessions can answer with a session in items that is not
counted in total.
Since #1256 (released 0.0.324) "open" means is_active AND expires_at > now(),
and app/repositories/session.py builds that predicate per statement. The page
query and the count query are separate statements, so each evaluates its own
now(): a session that reaches expires_at between them is open to one and
expired to the other, and the response then breaks the pagination invariant with
no concurrent write involved.
Narrow — a session has to expire inside the gap between two queries — but the fix
is small and local: one cutoff computed by the caller and passed to both
statements, or a transaction-stable database timestamp.
How you would know it was fixed
A test that pins one cutoff and asserts len(items) agrees with total across
it. Today the predicate cannot be pinned from outside, which is the thing to
change.
Found reviewing #1279 (Codex, P2), the pull request that introduced the
predicate.
SessionService.list_sessionscan answer with a session initemsthat is notcounted in
total.Since #1256 (released 0.0.324) "open" means
is_active AND expires_at > now(),and
app/repositories/session.pybuilds that predicate per statement. The pagequery and the count query are separate statements, so each evaluates its own
now(): a session that reachesexpires_atbetween them is open to one andexpired to the other, and the response then breaks the pagination invariant with
no concurrent write involved.
Narrow — a session has to expire inside the gap between two queries — but the fix
is small and local: one cutoff computed by the caller and passed to both
statements, or a transaction-stable database timestamp.
How you would know it was fixed
A test that pins one cutoff and asserts
len(items)agrees withtotalacrossit. Today the predicate cannot be pinned from outside, which is the thing to
change.
Found reviewing #1279 (Codex, P2), the pull request that introduced the
predicate.