Skip to content

Fix app stuck after process death: re-validate persisted session - #2

Merged
courville merged 2 commits into
courville:masterfrom
paulgreg:fix/stale-session-after-process-death
Sep 26, 2026
Merged

courville merged 2 commits into
courville:masterfrom
paulgreg:fix/stale-session-after-process-death

Conversation

@paulgreg

Copy link
Copy Markdown

Commit 5e5d64e (tt-rss#68, first shipped in v26.04) made Application persist the session id to SharedPreferences and rehydrate it in onCreate(). The intent was to allow an activity restored after background process death to skip a forced re-login. However this broke cold start after a force-close/reopen:

  • Application.onCreate() rehydrates the old (possibly expired) sid.
  • OnlineActivity.onResume() saw getSessionId() != null and called loginSuccess(false), skipping the login entirely.
  • Every subsequent API call used the stale sid and failed with NOT_LOGGED_IN, leaving the app stuck on a blank list.
  • Clearing app data wiped the persisted sid, which is why that worked around the problem.

Add an m_sessionValid flag to Application, false on cold start (and intentionally left false when rehydrating the persisted sid). It is set true only after a successful op:login in this process. Gate onResume()'s skip-login branch on isSessionValid() so a rehydrated stale session triggers a fresh login instead of being trusted.

This preserves the intended background-process-death recovery from 5e5d64e (the HeadlinesFragment empty-list refresh fix is untouched) while closing the stale-session hole.

@courville

Copy link
Copy Markdown
Owner

@paulgreg, thanks for the PR. Note that maintaining the fork for my own fixes and features additions that I contribute back to master project https://github.com/tt-rss/tt-rss-android. Perhaps it is worth doing your MR there.

@courville

Copy link
Copy Markdown
Owner

Thanks for this — nice bit of root-cause work. Splitting "we have a session id"
from "we authenticated this process" is exactly the right distinction, and I
like that m_sessionValid is intentionally not persisted so a cold start always
re-validates. The comments explaining the 5e5d64e4 history are genuinely
helpful.

One thing I want to make sure we cover before merging: the re-validation only
runs when OnlineActivity.onResume() fires. But after a successful login,
loginSuccess() starts MasterActivity and finishes OnlineActivity, so if the
process is killed while the user is on MasterActivity (or DetailActivity) and
they come back from recents, Android recreates that activity directly —
OnlineActivity.onResume() never runs. In that case
Application.load(savedInstanceState) rehydrates the sid and nothing
re-validates it, which is the same stale-session situation. So this fixes
force-close → reopen, but not process-death → restore from recents. Do you think
we should gate MasterActivity too, or handle it centrally (e.g. re-login on the
first NOT_LOGGED_IN)?

Two smaller ones:

  • initMenu() still checks getSessionId() != null rather than validity, so
    during the cold-start re-login the authenticated menu is briefly shown and a
    tap could fire an API call with the stale sid. Easy to align it with
    isSessionValid().
  • Invalidating the flag is duplicated in logout(), loginFailure(), and the
    LoginRequest failure tail. It's all correct today, but it would be more
    robust to clear m_sessionValid inside setSessionId(...) whenever the id
    changes, and only set it true from the successful-login path.

Nits: m_sessionValid could be volatile (or a comment noting it's main-thread
only). And it'd be good to add a small test around the gating predicate, since
this area has regressed twice now (tt-rss#68 and this).

Net effect is that cold starts no longer skip login, i.e. tt-rss#68's optimization is
reverted for the OnlineActivity path — that seems intended from the
description, just worth stating so we don't accidentally "fix" it back later.

Happy to help test the recents/process-death path if useful.

Commit 5e5d64e (tt-rss#68, first shipped in v26.04) made Application persist
the session id to SharedPreferences and rehydrate it in onCreate(). The
intent was to allow an activity restored after background process
death to skip a forced re-login. However this broke cold start after a
force-close/reopen:

- Application.onCreate() rehydrates the old (possibly expired) sid.
- OnlineActivity.onResume() saw getSessionId() != null and called
  loginSuccess(false), skipping the login entirely.
- Every subsequent API call used the stale sid and failed with
  NOT_LOGGED_IN, leaving the app stuck on a blank list.
- Clearing app data wiped the persisted sid, which is why that
  worked around the problem.

Add an m_sessionValid flag to Application, false on cold start (and
intentionally left false when rehydrating the persisted sid). It is set
true only after a successful op:login in this process. Gate
onResume()'s skip-login branch on isSessionValid() so a rehydrated
stale session triggers a fresh login instead of being trusted.

This preserves the intended background-process-death recovery from
5e5d64e (the HeadlinesFragment empty-list refresh fix is untouched)
while closing the stale-session hole.
@paulgreg
paulgreg force-pushed the fix/stale-session-after-process-death branch from fb46f07 to 218c55c Compare September 26, 2026 10:05
 -  initMenu race — now gates on isSessionValid() instead of getSessionId() != null, so the authenticated menu is hidden during the cold-start re-login window and a stray tap can't fire a call with the stale sid.
 - centralize invalidation — setSessionId() now clears m_sessionValid whenever the id changes; only the successful-op:login path sets it back to true. Dropped the three redundant setSessionValid(false) calls in logout(), loginFailure(), and the LoginRequest failure tail. The success path keeps the correct order (setSessionId(sid) then setSessionValid(true))
 - volatile — m_sessionValid is now volatile with a comment noting it's touched from background ApiRequest threads.
@paulgreg
paulgreg force-pushed the fix/stale-session-after-process-death branch from 218c55c to 7d522d5 Compare September 26, 2026 10:11
@paulgreg

Copy link
Copy Markdown
Author

On targeting the upstream repo — makes sense; I'll open the MR against tt-rss/tt-rss-android as well after a few days of testing my changes.

Thanks for the careful review — all three points addressed:

  • initMenu race — now gates on isSessionValid() instead of getSessionId() != null, so the authenticated menu is hidden during the cold-start re-login window and a stray tap can't fire a call with the stale sid.

  • centralize invalidation — setSessionId() now clears m_sessionValid whenever the id changes; only the successful-op:login path sets it back to true. Dropped the three redundant setSessionValid(false) calls in logout(), loginFailure(), and the LoginRequest failure tail. The success path keeps the correct order (setSessionId(sid) then setSessionValid(true)).

  • volatile — m_sessionValid is now volatile with a comment noting it's touched from background ApiRequest threads.

  • Recents/process-death path — good catch, but it's already covered: MasterActivity and DetailActivity both extend OnlineActivity and don't override the login logic in onResume(), so your gate fires when Android recreates those activities directly after process death. The one behavioral nuance: OnlineActivity.loginSuccess() does startActivity(MasterActivity) + finish(), so on that path the activity stack restarts rather than quietly resuming — acceptable IMO since the session was actually dead, but let me know if you'd prefer a refresh-only re-login when the current activity is already MasterActivity/DetailActivity. I'd lean toward keeping it simple for now and revisiting if anyone hits a visible glitch.

@courville

Copy link
Copy Markdown
Owner

Seems good to me thanks. I can merge on my tree but do you want to create a PR on upstream main repo (on which I rebase my changes)?

@courville
courville merged commit 7fb46f4 into courville:master Sep 26, 2026
1 check passed
@paulgreg

Copy link
Copy Markdown
Author

Thanks for the merge 🙏 !
Yes, I’ll open a PR on upstream main repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants