app(sync): keep backendBusy cooldown in-memory only#7527
Conversation
The "Omi servers are busy" message persisted across app restarts because the SyncRateLimiter saved its until/reason to SharedPreferences for both rate-limit (HTTP 429) and backendBusy (stale-guard) cases. backendBusy reflects transient backend pipeline pressure. If a user restarts the app to retry, the cooldown should clear and the next sync should re-probe the backend instead of trusting a local timer set during an earlier rough patch. Keep persistence for rateLimit (mirrors server-side fair-use enforcement which IS sticky across restarts) and move backendBusy to an in-memory field. On construction, clear any pre-existing persisted backendBusy entry so users coming from older app versions get unstuck on next launch.
Greptile SummaryThis PR fixes a UX regression where the "Omi servers are busy" banner persisted across app restarts because
Confidence Score: 5/5Safe to merge — the change is narrowly scoped to a single service class and correctly separates transient from durable cooldown state. The backendBusy/rateLimit split is implemented correctly across all getter/setter paths. The max-based alignment between No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[markLimited called] --> B{reason?}
B -- backendBusy --> C[_backendBusyUntilMs = now + secs]
B -- rateLimit --> D[SharedPreferences: saveInt + saveString]
C --> E[notifyListeners]
D --> E
F[isLimited getter] --> G{_backendBusyUntilMs > now?}
G -- yes --> H[return true]
G -- no --> I{SharedPrefs until > now?}
I -- yes --> H
I -- no --> J[return false]
K[reason getter] --> L{busyActive?}
L -- yes --> M{rateActive AND persisted > inMemory?}
M -- yes --> N[return rateLimit from prefs]
M -- no --> O[return backendBusy]
L -- no --> P{rateActive?}
P -- yes --> N
P -- no --> Q[return null]
R[App Restart] --> S[_backendBusyUntilMs reset to 0]
R --> T{SharedPrefs reason == backendBusy?}
T -- yes --> U[Migration: clear stuck prefs]
T -- no --> V[Keep rateLimit prefs intact]
Reviews (2): Last reviewed commit: "app(sync): align rate-limiter reason wit..." | Re-trigger Greptile |
| DateTime? get until { | ||
| final ms = SharedPreferencesUtil().getInt(_prefKeyUntil); | ||
| return ms > 0 ? DateTime.fromMillisecondsSinceEpoch(ms) : null; | ||
| final now = DateTime.now().millisecondsSinceEpoch; | ||
| final persisted = SharedPreferencesUtil().getInt(_prefKeyUntil); | ||
| final inMemory = _backendBusyUntilMs; | ||
| final candidates = <int>[if (inMemory > now) inMemory, if (persisted > now) persisted]; | ||
| if (candidates.isEmpty) return null; | ||
| return DateTime.fromMillisecondsSinceEpoch(candidates.reduce((a, b) => a > b ? a : b)); | ||
| } | ||
|
|
||
| RateLimitReason? get reason { | ||
| if (!isLimited) return null; | ||
| final name = SharedPreferencesUtil().getString(_prefKeyReason); | ||
| return RateLimitReason.values.asNameMap()[name] ?? RateLimitReason.rateLimit; | ||
| final now = DateTime.now().millisecondsSinceEpoch; | ||
| if (_backendBusyUntilMs > now) return RateLimitReason.backendBusy; | ||
| final persisted = SharedPreferencesUtil().getInt(_prefKeyUntil); | ||
| if (persisted > now) { | ||
| final name = SharedPreferencesUtil().getString(_prefKeyReason); | ||
| return RateLimitReason.values.asNameMap()[name] ?? RateLimitReason.rateLimit; | ||
| } | ||
| return null; |
There was a problem hiding this comment.
reason and until can report an inconsistent pair when both cooldowns are active
When a backendBusy in-memory cooldown and a persisted rateLimit cooldown are both active (e.g., a stale-guard fired during a 429 window), until returns the max expiry (likely the rateLimit end time), while reason returns backendBusy because _backendBusyUntilMs > now takes priority. The UI would then show "Omi servers are busy — wait until [rateLimit deadline]", which is wrong on both counts: the labelled reason expires sooner than displayed, and the displayed deadline belongs to a different reason. After _backendBusyUntilMs expires, the reason silently flips to rateLimit with no change in until, but until that flip the user sees an incorrect message. The reason getter should return the reason whose expiry matches the value returned by until (i.e., the reason with the later deadline), consistent with until's max-based logic.
kodjima33
left a comment
There was a problem hiding this comment.
thanks — keeping backendBusy cooldown in-memory makes sense
When backendBusy and rateLimit cooldowns are both active, reason now returns the reason of whichever has the later deadline, matching until's max-based pick. Prevents showing a reason and deadline that refer to different cooldowns.
|
@greptile-apps re-review |
Summary
SyncRateLimiternow persists only therateLimitcooldown.backendBusycooldowns live in an in-memory field that clears on app restart.backendBusyentry from older app versions, so users coming from a healthy server get unstuck on next launch.Why
The "Omi servers are busy" banner was surviving app restarts even when the backend had long since recovered. The cooldown was saved to
SharedPreferencesfor bothrateLimit(HTTP 429) andbackendBusy(stale-guard) cases.rateLimitcooldowns SHOULD persist — they mirror server-side fair-use enforcement that is itself sticky across restarts (30-day restrict window). ButbackendBusyreflects transient backend pipeline pressure. If a user restarts the app to retry, the cooldown should clear and the next sync should re-probe the backend, not trust a local timer set during an earlier rough patch.Backend data (last 7 days): zero stale-guard fires globally, yet some users still report seeing the banner. Their app state is stuck from an earlier server hiccup that has long since resolved.
Behavior
Test plan