CORE-446: fix LoadMarkets marketsLoaded invariant#4
Merged
Conversation
This commit was created by changelog-from-release in 'On release published (pushback changelog)' CI workflow
* fix(blofin): createOrder TPSL * static tests * minor * sl tp * upd * lint
* fix(binance): parse spot order type * binance: update test
* fix(aster): multi watchOrderBook sub * fix market detection Co-authored-by: Copilot <copilot@github.com> * other fixes Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
* fix(bitmart): ws subscriptions fix * fix hash * symbol unif * fix multi sub * base add subExistForRawHash * sub rnm * sub transpilable * mov * move to base * exchange-wide * upd
* fix(krakenfutures): watch unique subscriptions * len
* refactor: safe dict first value * comm * refix * go bool
Exchange.LoadMarkets() unconditionally set this.marketsLoaded = true after LoadMarketsHelper returned, even when the helper panicked internally and this.Markets was never populated. The next caller would hit the fast path, receive a typed-nil *sync.Map that does not satisfy IsError(), and the next this.Market(symbol) call would panic with "markets not loaded" — masking the real upstream HTTP error and permanently corrupting the exchange instance until destroyed. Fix: only set marketsLoaded = true when this.Markets is non-nil after the helper returns. When the helper panicked, leave marketsLoaded = false so subsequent LoadMarkets() calls retry through the slow path. The panic-string still flows to subscribers via the existing broadcast loop, so error observability is unchanged. Refs: 3commas-io/platform-umbrella CORE-446 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
morozov-commas
approved these changes
May 11, 2026
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.
Summary
Exchange.LoadMarkets()ingo/v4/exchange.gounconditionally setsthis.marketsLoaded = trueafterLoadMarketsHelperreturns. When the helper's deferred recover catches an internal panic (e.g.,FetchMarketsreturns a 502 from the exchange),SetMarketsnever runs andthis.Marketsstays nil — but the flag is flipped anyway. Every subsequentLoadMarkets()call then takes the fast path, returns a typed-nil*sync.Mapthat does not satisfyIsError(), and the nextMarket(symbol)panics with the misleading "markets not loaded" message. The exchange instance is permanently broken until destroyed.This is observed weekly in production during Hyperliquid's scheduled maintenance windows: 502 responses from
/infopoison freshly-spawned CCXT instances, the supervised actor's circuit breaker trips, and pods stay silent until manual restart.Fix
Only set
marketsLoaded = truewhenthis.Markets != nil(i.e.,SetMarketsactually ran inside the helper). When the helper panicked,marketsLoadedstaysfalseso subsequent calls retry through the slow path. The panic-string still flows to subscribers via the existing broadcast loop, so error observability is unchanged.9 lines of code (one
ifguard + comment).What this PR contains
ccxt/ccxt master(v4.5.47 → v4.5.52) — no conflicts.marketsLoadedinvariant fix.Test plan
go build ./...fromgo/v4/passesOut of scope
go.modpin (separate PR there)ccxt/ccxt(separate PR upstream)References
🤖 Generated with Claude Code