Repository navigation
fix(unread): keep the last good unread, Activity and followed threads while the read warehouse is behind (task #164) - #3685
Merged
Conversation
… while the read warehouse is behind (task #164) During the 10-24 RW restart, servers/unread-summary, channels/unread, channels/inbox and channels/threads/followed answered 503 rw_overloaded or the server's remembered body (X-Raft-Stale). A fresh open, server switch or sign-in could show blank or zero. - Shared layer: RaftStaleRead.kt reads X-Raft-Stale and the 503 {code:"rw_overloaded"} body (botiverse/slock f6ace2b9) and surfaces them as typed facts: staleReason on InboxSnapshot / ThreadListSnapshot / ActivityUnreadSummarySnapshot, ChannelUnreadSummary.stale, HomeNetworkPayload.unreadStale, ScreenError.serverCode = rw_overloaded. A stale page is always Content, never Empty. - SharedHomeStore: a stale or overloaded unread read keeps the shown counts (task #831 freeze), keeps the cold-start last-known row hints on screen, and sets SharedHomeState.unreadStale. A stale followed-thread reconcile leaves the thread marks. - SharedActivityStore: a stale page never replaces shown rows; an overload keeps them and attaches the typed error; SharedActivityState.stale. With no cache, an overload stays the load-failed state with Retry. - ProcessActivityUnreadStore: a stale summary never replaces a known count. - Unread-thread pill: ids an overloaded/stale list could not resolve may be asked again by the next trigger. - Recovery: one background-priority read per surface with backoff (15 s doubling to 5 min, +/-20 %), skipped while hidden or in flight; a fresh answer clears the flag. - UI (stdrc O2): info PanelNotice with spinner under the Activity head and the phone Home head; counts unchanged. RaftPanelNotice gains `pending` (as RaftBanner's) with goldens. Signed-off-by: Lagoon <lagoon@agents.stamp.build> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ule, retention (task #164) Aligns with botiverse/slock #11156 (head da23419e, StaleCountsNotice): - Copy: the notice strings are the web's own en / zh-CN / ja (thread.staleNotice, layout.sidebar.staleCountsNotice); the drafted en/ja copy is gone. - Recovery: the first read waits for the server's Retry-After (seconds or HTTP-date, 5 s without one), then 5, 10, 20, 40 and 60 s with +/-20 % jitter, never past 60 s. It reads only in the foreground; a timer that ends in the background waits. The app returning to the foreground (updateRaftProcessNetworkVisibility) or the socket rejoining its rooms (rooms:joined) reads at once. - Followed threads: getFollowedThreads / getUnreadFollowedThreads get the inbox's single Retry-After overload retry, and the channel page's unread-thread lookup retries on the same backoff until a fresh answer. - Retention: the last-known copies (Home row counts and previews, Activity first pages) share RaftLastCountsPersistStorage: nothing older than 7 days is shown, at most 5 servers per user are kept (least recently saved first, other users untouched), and sign-out clears the whole family. The Activity first pages were not cleared on sign-out before. The Activity badge cache gets the same 7-day and 5-server limits (it was already cleared on sign-out). - Cold start: the cache shows without the notice; only a server answer raises it (already so; now pinned by tests). Signed-off-by: Lagoon <lagoon@agents.stamp.build> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Task #164: keep the last good numbers while the read warehouse (RW) is behind
On 10-24, between 09:52 and 09:54 UTC, the production RW restart produced 734 × 503 errors on the sidebar unread, channel unread, Activity and followed-threads reads. Clients that already had data kept it. A fresh open, a server switch or a sign-in could show an error, blank or zero. This PR keeps the last good data, says it may be old (stdrc's O2), and recovers by itself.
Server contract (verified at botiverse/slock
f6ace2b9128be9bb6e85e0e46cd066ee17e87530, staging)routes/rwStaleRead.ts: onrw_overloaded, the server answers with a body remembered for up to 5 minutes, plus the headerX-Raft-Stale: rw_overloaded. For the unread summary and the inbox, a soft timeout can also answerX-Raft-Stale: rw_slowwith a body up to 60 s old. The routes areservers/unread-summary,channels/unread(the summary body also carries"stale": true),channels/inbox(first page with no query, body"stale": true) andchannels/threads/followed(first page, body"stale": true).routes/errorResponse.tsrespondToRisingWaveOverload:503,Retry-After: 2, body{"error":"Temporarily overloaded, retry shortly","code":"rw_overloaded","retryable":true}. There is no Postgres fallback.What changed
data/RaftStaleRead.kt): parsesX-Raft-Staleand the 503code. They are surfaced as typed facts:staleReason(InboxSnapshot, ThreadListSnapshot, ActivityUnreadSummarySnapshot),ChannelUnreadSummary.stale,HomeNetworkPayload/HomeSnapshot.unreadStale, andScreenError.serverCode == "rw_overloaded". A stale page is always Content, never Empty. The platform layer only passes headers (the existing Ktor executor already does).servers/unread-summary,channels/unread):rowHintsRetained) instead of rows with no count.SharedHomeState.unreadStale.channels/unread) leaves the thread marks when the answer is stale or overloaded.channels/inbox): a stale page never replaces shown rows. An overload keeps them, attaches the typed error and setsstale. With no cache, an overload stays the existing load-failed state with Retry, never "No activity". A stale page with no cache fills the list and marks it stale.Stale.channels/threads/followed): the repository tags stale and overload answers. The channel page's unread-thread pill releases ids it could not resolve during an outage, so the next trigger asks again (one request per trigger).RaftStaleReadRecovery, same schedule as web #11156rwStaleCounts.ts):Retry-After(seconds or HTTP-date; 5 s without one). After that the waits are 5, 10, 20, 40 and 60 s, each with ±20 % jitter, never past 60 s.updateRaftProcessNetworkVisibility, which both hosts already call.rooms:joined), reads at once.getFollowedThreads/getUnreadFollowedThreads) now get the same single retry, and the unread-thread pill's lookup recovers on the backoff above.RaftLastCountsPersistStorage, weblastCounts.tslimits): applies to the Home last-known row counts and previews and to the Activity first pages.RaftPanelNoticewith the pending spinner, directly under the head. On Activity it sits above the filters; on phone Home, above the list. Counts are unchanged, with no fading or marker. It is a polite live region, so it is announced once, and it leaves when the flag clears.RaftPanelNotice(pending = true)puts the spinner in the icon's place, asRaftBanner(pending)does (raft-ui 0.22 T-wrapper: canonical fact codec + SQLite binding (RFC 043 §9.9, V1 shadow persistence) #709). It has a golden case inbanner_{light,dark}and a README row.Copy
Taken exactly from web PR botiverse/slock #11156 (head
da23419e), keysthread.staleNoticeandlayout.sidebar.staleCountsNotice, as used byStaleCountsNotice:Design-system rules checked
RaftPanelNotice); the missingpendingvariant was added tocompose/designsystemwith goldens, not styled in page code.info-soft), never yellow or warning: waiting is not a problem. The spinner is decorative (cleared semantics) and the text says what waits.line-seambelow, 16dp phone edges.MR.stringsin en, zh-CN and ja.check_string_edge_spacesis clean.Goldens
compose/shared/src/androidUnitTest/screenshots/activity.rw-stale-notice{,.dark}.pngcompose/shared/src/androidUnitTest/screenshots/home.rw-stale-notice{,.dark}.pngcompose/designsystem/screenshots/banner_{light,dark}.png(adds the pending PanelNotice case)Tests (all run locally on Linux, green)
:shared:testDebugUnitTest: 8,700 tests, 0 failures.:designsystem:desktopTest: goldens verified.RaftStaleReadRecoveryTest(5):RaftLastCountsRetentionTest(4):SharedHomeStoreRwStaleTest(8):SharedActivityStoreRwStaleTest(8): the same matrix on the realRealInboxRepository, plus no reads while hidden and no notice before the server answers.FollowedThreadsRwStaleTest(3).SharedThreadUnreadThreadJumpTest(+1): the pill recovers on the backoff until a fresh answer.ProcessActivityUnreadStoreTest(+1).test.ymlscope covers it.Review notes
SharedHomeStore.refreshFollowedThreadUnread, thechannels/unreadread that athread:updatedreply triggers, now skips applying a stale or overloaded answer.RaftSharedStoreProvider'sonRoomsJoined, under the current-grant check, now also callsRaftStaleReadRecoverySignals.onRealtimeRoomsJoined()to nudge recovery.rw_slow(soft timeout, at most 60 s old) also raises the notice and keeps the shown data, as the task specifies and as the web does.rw_overloaded). The web also raises it for any other 5xx or no network; on mobile those keep their existing error handling. Mobile has no desktop sidebar notice.RealInboxRepositorystill issues every inbox read at Visible priority, including background refreshes; that is pre-existing and not changed here.🤖 Generated with Claude Code