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:
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
/**
|
||||
* Exponential backoff for a fetch that can keep failing (offline, rate-limited, an API change).
|
||||
*
|
||||
* After the n-th consecutive failure the next attempt waits 30 s * 2^(n-1), capped at 10 min, scaled
|
||||
* by up to +/-20% jitter so many wallets don't all retry in step when an outage ends. Not thread-safe:
|
||||
* keep it on the main thread (check due() before posting, record the outcome in the result callback).
|
||||
*/
|
||||
class FetchBackoff {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
static constexpr int kBaseSeconds = 30;
|
||||
static constexpr int kCapSeconds = 600;
|
||||
|
||||
/// True when a fetch may run: no failure yet, or the wait after the last one is over.
|
||||
bool due(Clock::time_point now) const { return failures_ == 0 || now >= next_; }
|
||||
|
||||
int failures() const { return failures_; }
|
||||
|
||||
void succeed() { failures_ = 0; next_ = {}; }
|
||||
|
||||
/// Record a failure; `jitter` in [-1, 1] scales the wait by up to +/-20%.
|
||||
void fail(Clock::time_point now, double jitter = 0.0)
|
||||
{
|
||||
failures_ = std::min(failures_ + 1, 30);
|
||||
next_ = now + delayFor(failures_, jitter);
|
||||
}
|
||||
|
||||
/// Allow the next attempt right away (a manual retry). The failure count stays, so another
|
||||
/// failure waits longer.
|
||||
void forceDue() { next_ = {}; }
|
||||
|
||||
static Clock::duration delayFor(int failures, double jitter)
|
||||
{
|
||||
if (failures <= 0) return Clock::duration::zero();
|
||||
const int doublings = std::min(failures - 1, 10);
|
||||
const double seconds = std::min(static_cast<double>(kBaseSeconds) * static_cast<double>(1 << doublings),
|
||||
static_cast<double>(kCapSeconds))
|
||||
* (1.0 + 0.2 * std::clamp(jitter, -1.0, 1.0));
|
||||
return std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(seconds));
|
||||
}
|
||||
|
||||
/// A jitter value in [-1, 1] for fail().
|
||||
static double randomJitter()
|
||||
{
|
||||
static std::minstd_rand rng{std::random_device{}()};
|
||||
return std::uniform_real_distribution<double>(-1.0, 1.0)(rng);
|
||||
}
|
||||
|
||||
private:
|
||||
int failures_ = 0;
|
||||
Clock::time_point next_{};
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
@@ -49,7 +49,8 @@ int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string httpGetString(const std::string& url, const char* logTag)
|
||||
std::string httpGetString(const std::string& url, const char* logTag,
|
||||
long connectTimeoutSec, long timeoutSec)
|
||||
{
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return {};
|
||||
@@ -59,8 +60,8 @@ std::string httpGetString(const std::string& url, const char* logTag)
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeoutSec);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, connectTimeoutSec);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(kMaxMetadataBytes)); // reject oversized metadata
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
|
||||
@@ -18,9 +18,11 @@ namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
// GET `url` over TLS (peer + host verified) into a string. Follows redirects; 30s transfer /
|
||||
// 15s connect timeout; browser-agnostic User-Agent. Returns "" on ANY failure (curl error or
|
||||
// non-2xx status). `logTag` (e.g. "[xmrig-updater]") prefixes the debug log on failure.
|
||||
std::string httpGetString(const std::string& url, const char* logTag);
|
||||
// 15s connect timeout by default; browser-agnostic User-Agent; responses over 16 MiB are refused.
|
||||
// Returns "" on ANY failure (curl error or non-2xx status). `logTag` (e.g. "[xmrig-updater]")
|
||||
// prefixes the debug log on failure.
|
||||
std::string httpGetString(const std::string& url, const char* logTag,
|
||||
long connectTimeoutSec = 15, long timeoutSec = 30);
|
||||
|
||||
// Download `url` over TLS (peer + host verified) to `destPath`, refusing bodies larger than
|
||||
// `maxBytes` (a defense ahead of the checksum/signature check). `onProgress(downloaded, total)`
|
||||
|
||||
Reference in New Issue
Block a user