fix: isolate on-chain provider outages
This commit is contained in:
+34
-23
@@ -158,6 +158,27 @@ def load_history():
|
||||
|
||||
# ── Background scraper ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _scrape_onchain_sources():
|
||||
"""Run independent on-chain providers so one outage cannot mask the other."""
|
||||
observations = {}
|
||||
errors = []
|
||||
successful_sources = 0
|
||||
providers = (
|
||||
("LookIntoBitcoin", "scrapers.lookintobitcoin"),
|
||||
("CheckOnChain", "scrapers.checkonchain"),
|
||||
)
|
||||
for display_name, module_name in providers:
|
||||
try:
|
||||
module = __import__(module_name, fromlist=["scrape_all"])
|
||||
observations.update(module.scrape_all())
|
||||
successful_sources += 1
|
||||
except Exception as exc:
|
||||
log.error("%s scraping failed: %s\n%s", display_name, exc, traceback.format_exc())
|
||||
errors.append(f"{display_name}: {exc}")
|
||||
return observations, errors, successful_sources
|
||||
|
||||
|
||||
def run_scrape(force_full=False):
|
||||
"""Run a scrape cycle and update cache.
|
||||
|
||||
@@ -262,30 +283,20 @@ def run_scrape(force_full=False):
|
||||
|
||||
if refresh_onchain:
|
||||
log.info("Refreshing on-chain metrics (forced, missing, or TTL expired)...")
|
||||
try:
|
||||
from scrapers import lookintobitcoin
|
||||
onchain = lookintobitcoin.scrape_all()
|
||||
try:
|
||||
from scrapers import checkonchain
|
||||
onchain.update(checkonchain.scrape_all())
|
||||
except Exception as e:
|
||||
log.error("CheckOnChain scraping failed: %s\n%s", e, traceback.format_exc())
|
||||
cycle_errors.append(f"CheckOnChain: {e}")
|
||||
checkonchain_keys = {"sopr", "sellside_risk", "active_address_momentum",
|
||||
"txcount_momentum", "nvt_price", "vdd_multiple"}
|
||||
for key in onchain_keys:
|
||||
source = "checkonchain" if key in checkonchain_keys else "lookintobitcoin"
|
||||
metrics[key] = merge_observation(
|
||||
existing_cache.get(key), onchain.get(key), source=source,
|
||||
error="metric missing from scrape",
|
||||
)
|
||||
onchain, onchain_errors, successful_sources = _scrape_onchain_sources()
|
||||
cycle_errors.extend(onchain_errors)
|
||||
checkonchain_keys = {"sopr", "sellside_risk", "active_address_momentum",
|
||||
"txcount_momentum", "nvt_price", "vdd_multiple"}
|
||||
for key in onchain_keys:
|
||||
source = "checkonchain" if key in checkonchain_keys else "lookintobitcoin"
|
||||
metrics[key] = merge_observation(
|
||||
existing_cache.get(key), onchain.get(key), source=source,
|
||||
error="metric missing from scrape",
|
||||
)
|
||||
if successful_sources:
|
||||
metrics["_onchain_timestamp"] = datetime.now(timezone.utc).isoformat()
|
||||
except Exception as e:
|
||||
log.error("LookIntoBitcoin scraping failed: %s\n%s", e, traceback.format_exc())
|
||||
_last_error = f"On-chain scraping failed: {e}"
|
||||
for k in onchain_keys:
|
||||
if k in existing_cache:
|
||||
metrics[k] = existing_cache[k]
|
||||
elif "_onchain_timestamp" in existing_cache:
|
||||
metrics["_onchain_timestamp"] = existing_cache["_onchain_timestamp"]
|
||||
else:
|
||||
# Reuse cached on-chain values — they're stored permanently
|
||||
log.info("Reusing cached on-chain data (use Full Refresh to re-scrape)")
|
||||
|
||||
@@ -108,6 +108,21 @@ def test_partial_fast_scrape_preserves_last_known_good_metric(server, monkeypatc
|
||||
assert saved["fear_greed"]["source"] == "alternative.me"
|
||||
|
||||
|
||||
def test_onchain_sources_fail_independently(server, monkeypatch):
|
||||
fake_lib = types.ModuleType("scrapers.lookintobitcoin")
|
||||
setattr(fake_lib, "scrape_all", lambda: (_ for _ in ()).throw(RuntimeError("LIB down")))
|
||||
fake_coc = types.ModuleType("scrapers.checkonchain")
|
||||
setattr(fake_coc, "scrape_all", lambda: {"sopr": {"value": 0.99}})
|
||||
monkeypatch.setitem(sys.modules, "scrapers.lookintobitcoin", fake_lib)
|
||||
monkeypatch.setitem(sys.modules, "scrapers.checkonchain", fake_coc)
|
||||
|
||||
observations, errors, successful_sources = server._scrape_onchain_sources()
|
||||
|
||||
assert observations["sopr"]["value"] == 0.99
|
||||
assert successful_sources == 1
|
||||
assert any("LookIntoBitcoin" in error for error in errors)
|
||||
|
||||
|
||||
def test_expired_onchain_timestamp_triggers_real_refresh(server, monkeypatch, tmp_path):
|
||||
old = datetime.now(timezone.utc) - timedelta(hours=7)
|
||||
cache_path = tmp_path / "cache.json"
|
||||
|
||||
Reference in New Issue
Block a user