fix(market): back off failed market fetches instead of retrying every frame
The Market tab calls the exchange-list, price-history and candle fetches every frame, so a failed fetch (offline, rate-limited, a venue's API down) was retried on the very next frame, ~20 requests a second. - util::FetchBackoff: 30 s doubling to a 10 min cap, +/-20% jitter. The exchange list, the price history (a partial result counts as a failure) and the per-pair candles each back off on failure. - Candle backoff is per pair. Switching to another pair still loads at once, a failed refresh keeps the on-screen pair's last candles, and switching back to a pair that is backing off shows the aggregate chart rather than the previous pair's candles. - The Market tab's refresh button retries at once (at most every 5 s). A price fetch (60 s timer, no backoff) that succeeds after failing means the network is back, so it lifts all three backoffs. - A partial price history no longer marks the chart loaded: with no fetch timestamp, the 30-minute reload gate measured system uptime and blocked retries for the first 30 minutes after boot. - The exchange list goes through httpGetString (TLS verified, size capped) with short timeouts (5 s connect, 12 s total, as it runs on the shared RPC worker) instead of its own curl code. A failed fetch is retried; it used to be tried only once per session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+79
-40
@@ -2453,8 +2453,18 @@ void App::refreshPrice()
|
||||
return [this, result = std::move(result)]() mutable {
|
||||
if (result.price) {
|
||||
NetworkRefreshService::applyPriceRefreshResult(state_, *result.price, std::chrono::steady_clock::now());
|
||||
if (price_fetch_failed_) {
|
||||
// The price (60 s timer, no backoff) works again after failing: the network is back, so
|
||||
// retry the backed-off market fetches now rather than after up to ~12 min. Failure
|
||||
// counts stay, so an endpoint that keeps failing on its own still waits.
|
||||
price_fetch_failed_ = false;
|
||||
exchanges_backoff_.forceDue();
|
||||
market_chart_backoff_.forceDue();
|
||||
exchange_chart_backoff_.forceDue();
|
||||
}
|
||||
} else {
|
||||
NetworkRefreshService::applyPriceRefreshFailure(state_, result.errorMessage);
|
||||
price_fetch_failed_ = true;
|
||||
}
|
||||
};
|
||||
}, 0);
|
||||
@@ -2470,6 +2480,18 @@ void App::refreshMarketData()
|
||||
refreshExchangeChart();
|
||||
}
|
||||
|
||||
void App::retryMarketDataNow()
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= market_manual_retry_at_) { // at most every 5 s, so click spam can't hammer the APIs
|
||||
market_manual_retry_at_ = now + std::chrono::seconds(5);
|
||||
exchanges_backoff_.forceDue();
|
||||
market_chart_backoff_.forceDue();
|
||||
exchange_chart_backoff_.forceDue();
|
||||
}
|
||||
refreshMarketData();
|
||||
}
|
||||
|
||||
void App::refreshExchangeChart()
|
||||
{
|
||||
if (!settings_ || !settings_->getFetchPrices() || !worker_) return;
|
||||
@@ -2516,8 +2538,13 @@ void App::refreshExchangeChart()
|
||||
exchange_chart_last_fetch_ = it->second.fetchedAt;
|
||||
return;
|
||||
}
|
||||
// Switching pairs: fall back to the aggregate until the new venue's candles arrive.
|
||||
// Switching pairs: fall back to the aggregate until the new venue's candles arrive. Before the
|
||||
// backoff check, so switching back to a pair that is backing off doesn't keep showing the previous
|
||||
// pair's candles.
|
||||
if (key != exchange_chart_key_) state_.market.exchange_chart_active = false;
|
||||
// This pair's last fetch failed: wait out the backoff (switching to another pair still loads at once).
|
||||
if (key == exchange_chart_fail_key_ && !exchange_chart_backoff_.due(std::chrono::steady_clock::now()))
|
||||
return;
|
||||
|
||||
exchange_chart_fetch_in_flight_ = true;
|
||||
const std::string identifier = pair->identifier, base = pair->base, quote = pair->quote;
|
||||
@@ -2553,10 +2580,23 @@ void App::refreshExchangeChart()
|
||||
exchange_chart_last_fetch_ = e.fetchedAt;
|
||||
if (exchange_chart_cache_.size() >= 16) exchange_chart_cache_.clear(); // bound it (many venues)
|
||||
exchange_chart_cache_[key] = std::move(e);
|
||||
if (key == exchange_chart_fail_key_) {
|
||||
exchange_chart_fail_key_.clear();
|
||||
exchange_chart_backoff_.succeed();
|
||||
}
|
||||
} else {
|
||||
// Both ranges empty -> the venue's API failed/changed; keep the aggregate.
|
||||
state_.market.exchange_chart_active = false;
|
||||
exchange_chart_key_.clear();
|
||||
// Both ranges empty: the venue's API failed or changed. Back off for this pair (another
|
||||
// pair still loads at once). A refresh of the pair on screen keeps its last candles; a
|
||||
// first load falls back to the aggregate chart.
|
||||
if (exchange_chart_fail_key_ != key) {
|
||||
exchange_chart_fail_key_ = key;
|
||||
exchange_chart_backoff_.succeed(); // a fresh failure count for this pair
|
||||
}
|
||||
exchange_chart_backoff_.fail(std::chrono::steady_clock::now(), util::FetchBackoff::randomJitter());
|
||||
if (exchange_chart_key_ != key) {
|
||||
state_.market.exchange_chart_active = false;
|
||||
exchange_chart_key_.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -2567,10 +2607,13 @@ void App::refreshMarketChart()
|
||||
if (!settings_ || !settings_->getFetchPrices()) return;
|
||||
if (!worker_) return;
|
||||
if (chart_fetch_in_flight_) return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
// Historical data moves slowly — refresh at most every 30 minutes after the first fetch.
|
||||
if (state_.market.chart_loaded &&
|
||||
std::chrono::steady_clock::now() - state_.market.chart_last_fetch_time < std::chrono::minutes(30))
|
||||
if (state_.market.chart_loaded && now - state_.market.chart_last_fetch_time < std::chrono::minutes(30))
|
||||
return;
|
||||
// The last attempt failed (or got only one range): wait out the backoff instead of retrying on the
|
||||
// next frame, as this is called every frame the Market tab is open.
|
||||
if (!market_chart_backoff_.due(now)) return;
|
||||
chart_fetch_in_flight_ = true;
|
||||
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
@@ -2589,14 +2632,21 @@ void App::refreshMarketChart()
|
||||
|
||||
return [this, intraday = std::move(intraday), daily = std::move(daily)]() mutable {
|
||||
chart_fetch_in_flight_ = false;
|
||||
const bool any = !intraday.empty() || !daily.empty();
|
||||
if (!intraday.empty()) state_.market.price_chart_intraday = std::move(intraday);
|
||||
if (!daily.empty()) state_.market.price_chart_daily = std::move(daily);
|
||||
if (any) {
|
||||
const bool gotIntraday = !intraday.empty();
|
||||
const bool gotDaily = !daily.empty();
|
||||
if (gotIntraday) state_.market.price_chart_intraday = std::move(intraday);
|
||||
if (gotDaily) state_.market.price_chart_daily = std::move(daily);
|
||||
if (gotIntraday && gotDaily) {
|
||||
// chart_loaded only with the timestamp: it arms the 30-minute gate above, which a partial
|
||||
// result must not (with no timestamp it would measure system uptime instead).
|
||||
state_.market.chart_loaded = true;
|
||||
state_.market.chart_last_fetch_time = std::chrono::steady_clock::now();
|
||||
market_chart_backoff_.succeed();
|
||||
} else {
|
||||
// Total or partial failure: show what arrived, and retry (both ranges) after the backoff
|
||||
// rather than on the next frame or, for a partial result, only in 30 minutes.
|
||||
market_chart_backoff_.fail(std::chrono::steady_clock::now(), util::FetchBackoff::randomJitter());
|
||||
}
|
||||
// On total failure, leave chart_loaded=false so the next tick retries.
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2605,42 +2655,31 @@ void App::refreshExchanges()
|
||||
{
|
||||
if (!settings_ || !settings_->getFetchPrices()) return;
|
||||
if (!worker_) return;
|
||||
if (exchanges_fetch_started_) return; // once per session (venues are near-static)
|
||||
if (exchanges_fetch_started_) return; // in flight, or loaded (once per session: venues are near-static)
|
||||
// The last attempt failed: wait out the backoff. This is called every frame the Market tab is open,
|
||||
// and a failure used to retry on the very next frame.
|
||||
if (!exchanges_backoff_.due(std::chrono::steady_clock::now())) return;
|
||||
exchanges_fetch_started_ = true;
|
||||
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
// httpGetString verifies TLS, caps the response size and returns "" on any failure. Short
|
||||
// timeouts: this runs on the shared RPC worker.
|
||||
const std::string body = util::httpGetString(
|
||||
"https://api.coingecko.com/api/v3/coins/dragonx-2/tickers", "[tickers]",
|
||||
/*connectTimeoutSec=*/5, /*timeoutSec=*/12);
|
||||
std::vector<data::ExchangeInfo> exchanges;
|
||||
try {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (curl) {
|
||||
std::string body;
|
||||
const char* url = "https://api.coingecko.com/api/v3/coins/dragonx-2/tickers";
|
||||
auto write_callback = [](void* contents, size_t size, size_t nmemb, std::string* userp) -> size_t {
|
||||
size_t total = size * nmemb;
|
||||
userp->append((char*)contents, total);
|
||||
return total;
|
||||
};
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 12L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "DragonX-Wallet/1.0");
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
curl_easy_cleanup(curl);
|
||||
if (res == CURLE_OK && http_code >= 200 && http_code < 300)
|
||||
exchanges = data::parseCoinGeckoTickers(body);
|
||||
}
|
||||
} catch (...) {}
|
||||
if (!body.empty()) {
|
||||
try { exchanges = data::parseCoinGeckoTickers(body); } catch (...) {}
|
||||
}
|
||||
|
||||
return [this, exchanges = std::move(exchanges)]() mutable {
|
||||
if (!exchanges.empty())
|
||||
if (!exchanges.empty()) {
|
||||
state_.market.exchanges = std::move(exchanges);
|
||||
else
|
||||
exchanges_fetch_started_ = false; // allow a retry if it failed/empty
|
||||
exchanges_backoff_.succeed();
|
||||
} else {
|
||||
exchanges_fetch_started_ = false; // retry once the backoff is over
|
||||
exchanges_backoff_.fail(std::chrono::steady_clock::now(), util::FetchBackoff::randomJitter());
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user