Split out of #294, whose other two halves (the TOCTOU cache read and the pre-commit invalidation) closed in #311.
User.session_version is cached per process for SESSION_VERSION_TTL_SECONDS. The worker that performs a revocation drops its own entry immediately; every other worker keeps honouring the revoked sessions until its cached entry expires. Acceptable for "sign out everywhere"; less so for a password change made because an account is believed compromised.
#311 made the window an operator choice (SM_USERS_SESSION_VERSION_TTL_SECONDS, 0 disables caching and pays one indexed read per request), which is an honest knob but not the fix.
Why it was not fixed there
The obvious mechanism — publish the bump on Redis pub/sub so every worker drops its entry at once — is not reachable from users:
- Redis belongs to the
background_tasks plugin. users does not own that connection or its configuration, and duplicating it there means two modules with independent Redis settings that can disagree.
- The framework
EventBus is explicitly in-process — "Async in-process event bus backed by pyee" (framework/core/simple_module_core/events.py).
SM009 makes a framework→plugin import an error, so the framework cannot reach into background_tasks for a client either.
What this actually needs
A framework-level transport that any module can publish invalidations on, with an in-process default and an optional shared backend — so users hangs the session_version bump on it and file_storage's aggregate cache (#307) and the settings hydration cache get the same guarantee for free. Those three all currently have per-process caches with the same staleness shape.
Worth designing rather than bolting a Redis client onto users.
Split out of #294, whose other two halves (the TOCTOU cache read and the pre-commit invalidation) closed in #311.
User.session_versionis cached per process forSESSION_VERSION_TTL_SECONDS. The worker that performs a revocation drops its own entry immediately; every other worker keeps honouring the revoked sessions until its cached entry expires. Acceptable for "sign out everywhere"; less so for a password change made because an account is believed compromised.#311 made the window an operator choice (
SM_USERS_SESSION_VERSION_TTL_SECONDS,0disables caching and pays one indexed read per request), which is an honest knob but not the fix.Why it was not fixed there
The obvious mechanism — publish the bump on Redis pub/sub so every worker drops its entry at once — is not reachable from
users:background_tasksplugin.usersdoes not own that connection or its configuration, and duplicating it there means two modules with independent Redis settings that can disagree.EventBusis explicitly in-process — "Async in-process event bus backed by pyee" (framework/core/simple_module_core/events.py).SM009makes a framework→plugin import an error, so the framework cannot reach intobackground_tasksfor a client either.What this actually needs
A framework-level transport that any module can publish invalidations on, with an in-process default and an optional shared backend — so
usershangs thesession_versionbump on it andfile_storage's aggregate cache (#307) and the settings hydration cache get the same guarantee for free. Those three all currently have per-process caches with the same staleness shape.Worth designing rather than bolting a Redis client onto
users.