Skip to content

chore: improve unsub failure and retry logic - #9678

Merged
sahar-fehri merged 12 commits into
mainfrom
chore/enhance-unsub-failure-cleanup-logic-ohlcv
Aug 4, 2026
Merged

chore: improve unsub failure and retry logic#9678
sahar-fehri merged 12 commits into
mainfrom
chore/enhance-unsub-failure-cleanup-logic-ohlcv

Conversation

@sahar-fehri

@sahar-fehrisahar-fehri commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix OHLCVService unsubscribe cleanup so failed WebSocket unsubs no longer leak gateway subscription slots and block future OHLCV streams.
  • Flush grace-period channels immediately when subscribing to a different asset/interval, while keeping same-channel grace reuse for rapid back navigation.
  • Retry failed unsubs with backoff (1s → 2s → 4s) and call forceReconnection only after retries are exhausted.

Explanation

Current state: When #performUnsubscribe failed, OHLCVService removed the channel from local tracking before the gateway unsub succeeded. That left orphaned server-side subscriptions, which could exhaust the gateway's 2 market-data subscription cap and cause later subscribes to fail (clients fall back to REST /latest polling).

Solution:

  1. Delete-on-success — keep channel entries until WS unsub succeeds.
  2. Grace-period flush — when subscribing to a different channel, immediately unsub other channels in grace/failed-cleanup instead of holding two live subs for up to 3s. Same-channel grace reuse also flushes other channels first (Bugbot fix).
  3. Retry + last-resort reconnect — on unsub failure, retry with backoff; after 3 retries, call BackendWebSocketService:forceReconnection to 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: forceReconnection affects the shared WebSocket

BackendWebSocketService is a single shared connection on mobile, used by multiple services (not OHLCV-only):

ConsumerPurpose
AccountActivityServiceReal-time account balance / activity push
OHLCVServiceChart OHLCV streaming
AssetsController (indirect)Forwards AccountActivityService:balanceUpdated

Calling 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 on DISCONNECTED, then resubscribes the selected account on CONNECTED (this service already uses forceReconnection for its own cleanup today).
  • OHLCVService — only resubscribes channels with refCount > 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

ScenarioImpact
User left chart, unsub stuck → force reconnect (Test 5)Brief (~1–2s) WS gap for account activity push; chart unaffected (hook unmounted). OHLCV does not resubscribe.
User has chart open when reconnect fires (unlikely — retries run after refCount === 0)Brief WS gap; OHLCV resubscribes active channel; mobile hook may poll REST briefly during staleness window.
Normal chart navigationNo force reconnect

Why we still use it

  • A permanently leaked market-data sub hits the gateway 2-sub cap and can break all OHLCV streaming on that connection — worse than a one-time reconnect blip.
  • forceReconnection is an existing, shared primitive (introduced in fix(core-backend): reconnection logic #6861; AccountActivityService already relies on it).
  • Retries are attempted first; reconnect is the escalation path only when repeated unsub attempts fail.

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

  • Related mobile investigation: UNSUBSCRIBE_LEAK_FIX_PLAN.md in metamask-mobile
  • Gateway behavior confirmed with backend: steady state is 1 live OHLCV sub per connection; cap of 2 is a race buffer for unsub→sub overlap
  • BackendWebSocketService.forceReconnection()fix(core-backend): reconnection logic #6861

Test plan

  • yarn workspace @metamask/core-backend run jest --no-coverage packages/core-backend/src/ws/ohlcv/OHLCVService.test.ts (43 tests)
  • Manual (mobile, debug build): unsub retry — fail 4×, succeed on 5th
  • Manual: same-token return within grace — reuse WS sub, no new subscribe
  • Manual: token switch within grace — flush old channel immediately
  • Manual: grace reuse flushes other channels first (Bugbot scenario)
  • Manual: exhausted retries → forceReconnection → WS disconnected / connected cycle
  • Manual: rapid token hopping on TDP — verify third subscribe succeeds (no limit would be exceeded)

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've documented the shared-WebSocket impact of forceReconnection in code (JSDoc) — PR description above; happy to add JSDoc in follow-up if reviewers want it in-tree
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

High Risk
Changes real-time OHLCV subscription lifecycle and can trigger shared WebSocket forceReconnection, affecting other consumers (e.g. account activity) after repeated unsub failures.

Overview
Fixes OHLCV WebSocket unsubscribe leaks that could leave orphaned gateway market-data subscriptions and hit the 2-sub cap, breaking later chart streams.

OHLCVService now keeps channel entries until the server unsub succeeds (delete-on-success). When subscribing to a different asset/interval, it flushes other channels still in grace or failed cleanup instead of holding two live subs for up to 3s; same-channel grace reuse is unchanged but also flushes other pending channels first. Failed unsubs are retried via cockatiel with 1s → 2s → 4s backoff; only after retries are exhausted does it call BackendWebSocketService:forceReconnection on the shared connection. Retries are cancellable via AbortController (resubscribe, destroy, reconnect drops refCount === 0 entries). destroy clears grace and retry timers; reconnect no longer resurrects stuck cleanup channels.

Adds cockatiel to @metamask/core-backend and expands OHLCVService.test.ts for flush, retry, destroy races, and connection-state edge cases.

Reviewed by Cursor Bugbot for commit f44eea8. Bugbot is set up for automated code reviews on this repo. Configure here.

@sahar-fehri
sahar-fehri marked this pull request as ready for review July 28, 2026 13:39
@sahar-fehri
sahar-fehri requested review from a team as code ownersJuly 28, 2026 13:39
Comment threadpackages/core-backend/src/ws/ohlcv/OHLCVService.ts
Comment threadpackages/core-backend/src/ws/ohlcv/OHLCVService.ts
Comment threadpackages/core-backend/src/ws/ohlcv/OHLCVService.ts Outdated
Comment threadpackages/core-backend/src/ws/ohlcv/OHLCVService.ts
Prithpal-Sooriya
Prithpal-Sooriya previously approved these changes Jul 29, 2026
Kriys94
Kriys94 previously approved these changes Jul 29, 2026

@Kriys94Kriys94 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight preference for re-using TanStack, but I'm also good with the implementation

@sahar-fehri

Copy link
Copy Markdown
ContributorAuthor

@metamaskbot publish-preview

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 5a03e1b. Configure here.

Comment threadpackages/core-backend/src/ws/ohlcv/OHLCVService.ts
@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.5.5-preview-5a03e1b92
@metamask-previews/accounts-controller@39.0.6-preview-5a03e1b92
@metamask-previews/address-book-controller@7.1.2-preview-5a03e1b92
@metamask-previews/ai-controllers@0.8.0-preview-5a03e1b92
@metamask-previews/analytics-controller@2.0.0-preview-5a03e1b92
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-5a03e1b92
@metamask-previews/announcement-controller@8.1.0-preview-5a03e1b92
@metamask-previews/app-metadata-controller@2.0.1-preview-5a03e1b92
@metamask-previews/approval-controller@9.0.2-preview-5a03e1b92
@metamask-previews/assets-controller@13.1.0-preview-5a03e1b92
@metamask-previews/assets-controllers@110.1.0-preview-5a03e1b92
@metamask-previews/authenticated-user-storage@3.0.1-preview-5a03e1b92
@metamask-previews/base-controller@9.1.0-preview-5a03e1b92
@metamask-previews/base-data-service@0.1.3-preview-5a03e1b92
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-5a03e1b92
@metamask-previews/bridge-controller@78.0.3-preview-5a03e1b92
@metamask-previews/bridge-status-controller@74.6.1-preview-5a03e1b92
@metamask-previews/build-utils@3.0.4-preview-5a03e1b92
@metamask-previews/chain-agnostic-permission@1.7.0-preview-5a03e1b92
@metamask-previews/chomp-api-service@4.0.0-preview-5a03e1b92
@metamask-previews/claims-controller@0.5.3-preview-5a03e1b92
@metamask-previews/client-controller@1.0.1-preview-5a03e1b92
@metamask-previews/client-utils@1.5.0-preview-5a03e1b92
@metamask-previews/compliance-controller@2.1.0-preview-5a03e1b92
@metamask-previews/composable-controller@12.0.1-preview-5a03e1b92
@metamask-previews/config-registry-controller@2.0.0-preview-5a03e1b92
@metamask-previews/connectivity-controller@0.3.0-preview-5a03e1b92
@metamask-previews/controller-utils@12.3.0-preview-5a03e1b92
@metamask-previews/core-backend@8.1.0-preview-5a03e1b92
@metamask-previews/delegation-controller@3.0.2-preview-5a03e1b92
@metamask-previews/earn-controller@12.2.3-preview-5a03e1b92
@metamask-previews/eip-5792-middleware@3.0.5-preview-5a03e1b92
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-5a03e1b92
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-5a03e1b92
@metamask-previews/ens-controller@19.1.6-preview-5a03e1b92
@metamask-previews/eth-block-tracker@15.0.1-preview-5a03e1b92
@metamask-previews/eth-json-rpc-middleware@23.1.3-preview-5a03e1b92
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-5a03e1b92
@metamask-previews/foundryup@1.0.1-preview-5a03e1b92
@metamask-previews/gas-fee-controller@26.3.1-preview-5a03e1b92
@metamask-previews/gator-permissions-controller@5.0.1-preview-5a03e1b92
@metamask-previews/geolocation-controller@1.0.0-preview-5a03e1b92
@metamask-previews/java-tron-up@1.0.0-preview-5a03e1b92
@metamask-previews/json-rpc-engine@10.5.0-preview-5a03e1b92
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-5a03e1b92
@metamask-previews/keyring-controller@27.1.0-preview-5a03e1b92
@metamask-previews/local-node-utils@1.0.0-preview-5a03e1b92
@metamask-previews/logging-controller@8.0.2-preview-5a03e1b92
@metamask-previews/message-manager@14.1.2-preview-5a03e1b92
@metamask-previews/messenger@2.0.0-preview-5a03e1b92
@metamask-previews/messenger-cli@0.2.0-preview-5a03e1b92
@metamask-previews/money-account-api-data-service@0.4.0-preview-5a03e1b92
@metamask-previews/money-account-balance-service@2.4.1-preview-5a03e1b92
@metamask-previews/money-account-controller@0.3.3-preview-5a03e1b92
@metamask-previews/money-account-upgrade-controller@3.0.1-preview-5a03e1b92
@metamask-previews/money-account-utils@1.0.0-preview-5a03e1b92
@metamask-previews/multichain-account-service@13.0.0-preview-5a03e1b92
@metamask-previews/multichain-api-middleware@4.0.2-preview-5a03e1b92
@metamask-previews/multichain-network-controller@3.2.2-preview-5a03e1b92
@metamask-previews/multichain-transactions-controller@7.1.1-preview-5a03e1b92
@metamask-previews/name-controller@9.1.2-preview-5a03e1b92
@metamask-previews/network-connection-banner-controller@0.1.2-preview-5a03e1b92
@metamask-previews/network-controller@35.0.0-preview-5a03e1b92
@metamask-previews/network-enablement-controller@6.0.2-preview-5a03e1b92
@metamask-previews/notification-services-controller@26.0.0-preview-5a03e1b92
@metamask-previews/passkey-controller@3.0.0-preview-5a03e1b92
@metamask-previews/permission-controller@13.1.1-preview-5a03e1b92
@metamask-previews/permission-log-controller@5.1.0-preview-5a03e1b92
@metamask-previews/perps-controller@10.0.0-preview-5a03e1b92
@metamask-previews/phishing-controller@17.3.1-preview-5a03e1b92
@metamask-previews/platform-api-docs@0.0.0-preview-5a03e1b92
@metamask-previews/polling-controller@16.0.9-preview-5a03e1b92
@metamask-previews/preferences-controller@23.1.0-preview-5a03e1b92
@metamask-previews/profile-metrics-controller@4.0.2-preview-5a03e1b92
@metamask-previews/profile-sync-controller@28.3.0-preview-5a03e1b92
@metamask-previews/ramps-controller@18.0.1-preview-5a03e1b92
@metamask-previews/rate-limit-controller@7.0.1-preview-5a03e1b92
@metamask-previews/react-data-query@0.2.2-preview-5a03e1b92
@metamask-previews/remote-feature-flag-controller@5.0.0-preview-5a03e1b92
@metamask-previews/sample-controllers@5.0.4-preview-5a03e1b92
@metamask-previews/seedless-onboarding-controller@10.1.0-preview-5a03e1b92
@metamask-previews/selected-network-controller@26.1.6-preview-5a03e1b92
@metamask-previews/sentinel-api-service@1.0.0-preview-5a03e1b92
@metamask-previews/shield-controller@5.1.3-preview-5a03e1b92
@metamask-previews/signature-controller@39.2.9-preview-5a03e1b92
@metamask-previews/smart-transactions-controller@25.0.2-preview-5a03e1b92
@metamask-previews/snap-account-service@2.1.1-preview-5a03e1b92
@metamask-previews/social-controllers@2.7.0-preview-5a03e1b92
@metamask-previews/solana-test-validator-up@1.0.0-preview-5a03e1b92
@metamask-previews/stellar-quickstart-up@0.0.0-preview-5a03e1b92
@metamask-previews/storage-service@1.0.2-preview-5a03e1b92
@metamask-previews/subscription-controller@6.2.1-preview-5a03e1b92
@metamask-previews/transaction-controller@69.4.0-preview-5a03e1b92
@metamask-previews/transaction-pay-controller@26.1.1-preview-5a03e1b92
@metamask-previews/user-operation-controller@41.2.8-preview-5a03e1b92
@metamask-previews/wallet@9.0.0-preview-5a03e1b92
@metamask-previews/wallet-cli@0.0.0-preview-5a03e1b92

@sahar-fehri

Copy link
Copy Markdown
ContributorAuthor

@metamaskbot publish-preview

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.5.5-preview-8c1692925
@metamask-previews/accounts-controller@39.0.6-preview-8c1692925
@metamask-previews/address-book-controller@7.1.2-preview-8c1692925
@metamask-previews/ai-controllers@0.8.0-preview-8c1692925
@metamask-previews/analytics-controller@2.0.0-preview-8c1692925
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-8c1692925
@metamask-previews/announcement-controller@8.1.0-preview-8c1692925
@metamask-previews/app-metadata-controller@2.0.1-preview-8c1692925
@metamask-previews/approval-controller@9.0.2-preview-8c1692925
@metamask-previews/assets-controller@13.1.0-preview-8c1692925
@metamask-previews/assets-controllers@110.1.0-preview-8c1692925
@metamask-previews/authenticated-user-storage@3.0.1-preview-8c1692925
@metamask-previews/base-controller@9.1.0-preview-8c1692925
@metamask-previews/base-data-service@0.1.3-preview-8c1692925
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-8c1692925
@metamask-previews/bridge-controller@78.0.3-preview-8c1692925
@metamask-previews/bridge-status-controller@74.6.1-preview-8c1692925
@metamask-previews/build-utils@3.0.4-preview-8c1692925
@metamask-previews/chain-agnostic-permission@1.7.0-preview-8c1692925
@metamask-previews/chomp-api-service@4.0.0-preview-8c1692925
@metamask-previews/claims-controller@0.5.3-preview-8c1692925
@metamask-previews/client-controller@1.0.1-preview-8c1692925
@metamask-previews/client-utils@1.5.0-preview-8c1692925
@metamask-previews/compliance-controller@2.1.0-preview-8c1692925
@metamask-previews/composable-controller@12.0.1-preview-8c1692925
@metamask-previews/config-registry-controller@2.0.0-preview-8c1692925
@metamask-previews/connectivity-controller@0.3.0-preview-8c1692925
@metamask-previews/controller-utils@12.3.0-preview-8c1692925
@metamask-previews/core-backend@8.1.0-preview-8c1692925
@metamask-previews/delegation-controller@3.0.2-preview-8c1692925
@metamask-previews/earn-controller@12.2.3-preview-8c1692925
@metamask-previews/eip-5792-middleware@3.0.5-preview-8c1692925
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-8c1692925
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-8c1692925
@metamask-previews/ens-controller@19.1.6-preview-8c1692925
@metamask-previews/eth-block-tracker@15.0.1-preview-8c1692925
@metamask-previews/eth-json-rpc-middleware@23.1.3-preview-8c1692925
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-8c1692925
@metamask-previews/foundryup@1.0.1-preview-8c1692925
@metamask-previews/gas-fee-controller@26.3.1-preview-8c1692925
@metamask-previews/gator-permissions-controller@5.0.1-preview-8c1692925
@metamask-previews/geolocation-controller@1.0.0-preview-8c1692925
@metamask-previews/java-tron-up@1.0.0-preview-8c1692925
@metamask-previews/json-rpc-engine@10.5.0-preview-8c1692925
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-8c1692925
@metamask-previews/keyring-controller@27.1.0-preview-8c1692925
@metamask-previews/local-node-utils@1.0.0-preview-8c1692925
@metamask-previews/logging-controller@8.0.2-preview-8c1692925
@metamask-previews/message-manager@14.1.2-preview-8c1692925
@metamask-previews/messenger@2.0.0-preview-8c1692925
@metamask-previews/messenger-cli@0.2.0-preview-8c1692925
@metamask-previews/money-account-api-data-service@0.4.0-preview-8c1692925
@metamask-previews/money-account-balance-service@2.4.1-preview-8c1692925
@metamask-previews/money-account-controller@0.3.3-preview-8c1692925
@metamask-previews/money-account-upgrade-controller@3.0.1-preview-8c1692925
@metamask-previews/money-account-utils@1.0.0-preview-8c1692925
@metamask-previews/multichain-account-service@13.0.0-preview-8c1692925
@metamask-previews/multichain-api-middleware@4.0.2-preview-8c1692925
@metamask-previews/multichain-network-controller@3.2.2-preview-8c1692925
@metamask-previews/multichain-transactions-controller@7.1.1-preview-8c1692925
@metamask-previews/name-controller@9.1.2-preview-8c1692925
@metamask-previews/network-connection-banner-controller@0.1.2-preview-8c1692925
@metamask-previews/network-controller@35.0.0-preview-8c1692925
@metamask-previews/network-enablement-controller@6.0.2-preview-8c1692925
@metamask-previews/notification-services-controller@26.0.0-preview-8c1692925
@metamask-previews/passkey-controller@3.0.0-preview-8c1692925
@metamask-previews/permission-controller@13.1.1-preview-8c1692925
@metamask-previews/permission-log-controller@5.1.0-preview-8c1692925
@metamask-previews/perps-controller@10.0.0-preview-8c1692925
@metamask-previews/phishing-controller@17.3.1-preview-8c1692925
@metamask-previews/platform-api-docs@0.0.0-preview-8c1692925
@metamask-previews/polling-controller@16.0.9-preview-8c1692925
@metamask-previews/preferences-controller@23.1.0-preview-8c1692925
@metamask-previews/profile-metrics-controller@4.0.2-preview-8c1692925
@metamask-previews/profile-sync-controller@28.3.0-preview-8c1692925
@metamask-previews/ramps-controller@18.0.1-preview-8c1692925
@metamask-previews/rate-limit-controller@7.0.1-preview-8c1692925
@metamask-previews/react-data-query@0.2.2-preview-8c1692925
@metamask-previews/remote-feature-flag-controller@5.0.0-preview-8c1692925
@metamask-previews/sample-controllers@5.0.4-preview-8c1692925
@metamask-previews/seedless-onboarding-controller@10.1.0-preview-8c1692925
@metamask-previews/selected-network-controller@26.1.6-preview-8c1692925
@metamask-previews/sentinel-api-service@1.0.0-preview-8c1692925
@metamask-previews/shield-controller@5.1.3-preview-8c1692925
@metamask-previews/signature-controller@39.2.9-preview-8c1692925
@metamask-previews/smart-transactions-controller@25.0.2-preview-8c1692925
@metamask-previews/snap-account-service@2.1.1-preview-8c1692925
@metamask-previews/social-controllers@2.7.0-preview-8c1692925
@metamask-previews/solana-test-validator-up@1.0.0-preview-8c1692925
@metamask-previews/stellar-quickstart-up@0.0.0-preview-8c1692925
@metamask-previews/storage-service@1.0.2-preview-8c1692925
@metamask-previews/subscription-controller@6.2.1-preview-8c1692925
@metamask-previews/transaction-controller@69.4.0-preview-8c1692925
@metamask-previews/transaction-pay-controller@26.1.1-preview-8c1692925
@metamask-previews/user-operation-controller@41.2.8-preview-8c1692925
@metamask-previews/wallet@9.0.0-preview-8c1692925
@metamask-previews/wallet-cli@0.0.0-preview-8c1692925

const subscribePromise = service.subscribe(SUB_OPTS);

jest.advanceTimersByTime(3000);
await flushPromises();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we see flakiness here, then let's replace with waitFor(). Not blocking

Comment on lines +640 to +648
jest.advanceTimersByTime(3000);
await flushPromises();

service.destroy();
releaseUnsub(new Error('ws gone'));
await completeAsyncOperations();

jest.advanceTimersByTime(1000);
await completeAsyncOperations();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, not blocking but the test specs need a proper refactor

// no-op
});
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.#performUnsubscribe(channel);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add back the catch noop, protects from throwing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return;
}

entry.retryAbort?.abort();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not - Hmm I wonder if this additional abort logic can be captured by a "debounce"?

@sahar-fehrisahar-fehriAug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timing side is actually already a debounce: the grace-period timer waits GRACE_PERIOD_MS before tearing down and a fast re-subscribe cancels it. But the bit flagged here (retryAbort) is doing something a debounce can't: cancelling an in-flight retry loop mid-backoff (the 1s → 2s → 4s window before we fall back to forceReconnection).

A debounce only reacts to repeats of the same event and only delays the start. Here we need to interrupt work that's already running, triggered by three different external events:

1- User returns to the chart (#subscribeInner): abort the teardown and reuse the still-alive WS subscription instead of unsubscribing the channel they're now viewing.

2- Service teardown (destroy() → #clearChannelTimers): abort so an orphaned retry can't fire forceReconnection on the shared socket after teardown (the resurrection bug this PR fixes).

3- Reconnect cleanup (#resubscribeActiveChannels): abort a retry that's targeting the pre-reconnect subscription so we don't trigger a redundant reconnect storm.

): Promise<void> {
try {
await unsubRetryPolicy.execute(async () => {
const releaseLock = await this.#mutex.acquire();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locking worries me.

  1. Do we need locks for this logic?
  2. Are we confident that we cover all possible unlocks?
  3. Is this shared mutex used on other operations. Does that mean if lock is "stuck" other operations are blocked?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair questions

1- Do we need locks? Yes; #channels and refCount are mutated from several concurrent async entrypoints (UI subscribe/unsubscribe, the grace-timer callback, the retry loop, and reconnect resubscribe). Without serialization we'd race on refCount and on WS subscribe/unsubscribe ordering (e.g. a double-subscribe, or unsubscribing a channel mid-subscribe).

2- Are all unlocks covered? Yes; every acquire() is paired with try { … } finally { releaseLock() }: subscribe, unsubscribe, #performUnsubscribe, the #runUnsubRetryLoop execute callback, and #resubscribeActiveChannels. Release always runs, even on throw.

3- Is it a shared mutex that could block other ops? It's private per OHLCVService instance (readonly #mutex = new Mutex()), not shared with BackendWebSocketService or any other service so worst case it only serializes OHLCV's own subscribe/unsubscribe, nothing else. Also worth noting the lock is released between retries: Cockatiel's backoff waits happen outside the execute callback, so the 1s/2s/4s delays don't hold the mutex.

@sahar-fehri
sahar-fehri added this pull request to the merge queueAug 4, 2026
Merged via the queue into main with commit 5d777e3Aug 4, 2026
140 checks passed
@sahar-fehri
sahar-fehri deleted the chore/enhance-unsub-failure-cleanup-logic-ohlcv branch August 4, 2026 08:59
Sign up for freeto 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.

3 participants

@sahar-fehri@Prithpal-Sooriya@Kriys94