chore: improve unsub failure and retry logic - #9678
Conversation
| } | ||
| } | ||
|
|
||
| #scheduleUnsubscribeRetry(channel: string): void { |
There was a problem hiding this comment.
I wonder if we can handle the retries to TanStack?
| refCount: number; | ||
| gracePeriodTimer?: ReturnType<typeof setTimeout>; | ||
| retryTimer?: ReturnType<typeof setTimeout>; | ||
| retryCount?: number; |
There was a problem hiding this comment.
Potentially TanStack would remove this kind of trackers and all underlying logic. (gracePeriodTimer, retryTimer, retryCount). I'm not sure about refCount
There was a problem hiding this comment.
Agreed here, maybe we can move away from imperative retry by using either TanStack or cockatiel.
import { ExponentialBackoff, handleAll, retry } from 'cockatiel';
import { QueryClient } from '@tanstack/query-core';
const testFn1 = async () => {
const result = await retry(handleAll, {
maxAttempts: 3,
backoff: new ExponentialBackoff({ initialDelay: 200, maxDelay: 5_000 }),
}).execute(async () => await doSomethingThatCanFail());
}
const testFn2 = async () => {
const result = await new QueryClient().fetchQuery({
queryKey: ['ohlcv-disconnect', someIDIfWannaMakeUnique],
retry: 3,
queryFn: () => doSomethingThatCanFail(),
});
}There was a problem hiding this comment.
could you guys check this refactor here 74a7e01
i went with cockatiel
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 43628d8. Configure here.
Kriys94
left a comment
There was a problem hiding this comment.
Slight preference for re-using TanStack, but I'm also good with the implementation

Summary
OHLCVServiceunsubscribe cleanup so failed WebSocket unsubs no longer leak gateway subscription slots and block future OHLCV streams.1s → 2s → 4s) and callforceReconnectiononly after retries are exhausted.Explanation
Current state: When
#performUnsubscribefailed,OHLCVServiceremoved the channel from local tracking before the gateway unsub succeeded. That left orphaned server-side subscriptions, which could exhaust the gateway's 2market-datasubscription cap and cause later subscribes to fail (clients fall back to REST/latestpolling).Solution:
BackendWebSocketService:forceReconnectionto reset server subscription state.Same-channel grace (3s) is unchanged so users returning to the same token/interval within 3s can reuse the existing subscription.
Important:
forceReconnectionaffects the shared WebSocketBackendWebSocketServiceis a single shared connection on mobile, used by multiple services (not OHLCV-only):AccountActivityServiceOHLCVServiceAssetsController(indirect)AccountActivityService:balanceUpdatedCalling
forceReconnection()closes and reconnects that entire WebSocket. On disconnect it clears all local subscription state; on reconnect each service resubscribes what it needs:AccountActivityService— marks chains down onDISCONNECTED, then resubscribes the selected account onCONNECTED(this service already usesforceReconnectionfor its own cleanup today).OHLCVService— only resubscribes channels withrefCount > 0(active chart consumers). Failed-cleanup / grace entries (refCount === 0) are dropped on reconnect and are not resubscribed.When does OHLCV trigger it?
Only as a last resort, after unsub has failed 4 times total (initial grace-period attempt + 3 retries at 1s / 2s / 4s). Typical navigation (leave chart, switch token, return within grace) uses delete-on-success, flush, or grace reuse — not force reconnect.
Rough timeline after user leaves a chart with a stuck unsub: ~10s of background retries before reconnect fires.
User-visible impact
refCount === 0)Why we still use it
market-datasub hits the gateway 2-sub cap and can break all OHLCV streaming on that connection — worse than a one-time reconnect blip.forceReconnectionis an existing, shared primitive (introduced in fix(core-backend): reconnection logic #6861;AccountActivityServicealready relies on it).Open question for reviewers: If the cross-service reconnect blast radius is unacceptable, alternatives are (a) gateway-side sub cleanup API, or (b) OHLCV-only reconnect/isolation — neither exists today on the shared
BackendWebSocketService.Fixes
References
UNSUBSCRIBE_LEAK_FIX_PLAN.mdin metamask-mobileBackendWebSocketService.forceReconnection()— fix(core-backend): reconnection logic #6861Test plan
yarn workspace @metamask/core-backend run jest --no-coverage packages/core-backend/src/ws/ohlcv/OHLCVService.test.ts(43 tests)forceReconnection→ WS disconnected / connected cyclelimit would be exceeded)Checklist
forceReconnectionin code (JSDoc) — PR description above; happy to add JSDoc in follow-up if reviewers want it in-treeNote
Medium Risk
Touches live WebSocket subscription lifecycle and can trigger shared
forceReconnection, briefly affecting other consumers on the same connection; mitigated by delete-on-success, flush, and retries before reconnect.Overview
Fixes OHLCV chart streaming getting stuck when gateway
market-dataunsubscribe fails by keeping channel entries until the server unsub succeeds, instead of deleting them early and leaking subscription slots.Navigation behavior changes: subscribing to a different asset/interval immediately flushes other channels still in grace or failed cleanup (same-channel grace reuse for back navigation is unchanged). Re-subscribe paths also flush other pending channels first.
Resilience: failed unsubs go through cockatiel backoff (
1s → 2s → 4s, four attempts total), emitsubscriptionError, and callBackendWebSocketService:forceReconnectiononly when retries are exhausted. Retries and grace timers are aborted ondestroyor when the channel is subscribed again; reconnect dropsrefCount === 0entries instead of resubscribing them.Adds
cockatielto@metamask/core-backendand expandsOHLCVServicetests for flush, retry, reconnect, and destroy paths. Changelog also records an unreleased breaking alignment ofV6_DEFI_POSITION_TYPES(no code changes for that in this diff).Reviewed by Cursor Bugbot for commit 54e16dd. Bugbot is set up for automated code reviews on this repo. Configure here.