fix: isolate on-chain provider outages

This commit is contained in:
Hermes Agent
2026-07-26 23:32:54 +00:00
parent a2b9b431c7
commit 2da5d20ccd
2 changed files with 50 additions and 24 deletions
+34 -23
View File
@@ -158,6 +158,27 @@ def load_history():
# ── Background scraper ──────────────────────────────────────────────────── # ── 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): def run_scrape(force_full=False):
"""Run a scrape cycle and update cache. """Run a scrape cycle and update cache.
@@ -262,30 +283,20 @@ def run_scrape(force_full=False):
if refresh_onchain: if refresh_onchain:
log.info("Refreshing on-chain metrics (forced, missing, or TTL expired)...") log.info("Refreshing on-chain metrics (forced, missing, or TTL expired)...")
try: onchain, onchain_errors, successful_sources = _scrape_onchain_sources()
from scrapers import lookintobitcoin cycle_errors.extend(onchain_errors)
onchain = lookintobitcoin.scrape_all() checkonchain_keys = {"sopr", "sellside_risk", "active_address_momentum",
try: "txcount_momentum", "nvt_price", "vdd_multiple"}
from scrapers import checkonchain for key in onchain_keys:
onchain.update(checkonchain.scrape_all()) source = "checkonchain" if key in checkonchain_keys else "lookintobitcoin"
except Exception as e: metrics[key] = merge_observation(
log.error("CheckOnChain scraping failed: %s\n%s", e, traceback.format_exc()) existing_cache.get(key), onchain.get(key), source=source,
cycle_errors.append(f"CheckOnChain: {e}") error="metric missing from scrape",
checkonchain_keys = {"sopr", "sellside_risk", "active_address_momentum", )
"txcount_momentum", "nvt_price", "vdd_multiple"} if successful_sources:
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",
)
metrics["_onchain_timestamp"] = datetime.now(timezone.utc).isoformat() metrics["_onchain_timestamp"] = datetime.now(timezone.utc).isoformat()
except Exception as e: elif "_onchain_timestamp" in existing_cache:
log.error("LookIntoBitcoin scraping failed: %s\n%s", e, traceback.format_exc()) metrics["_onchain_timestamp"] = existing_cache["_onchain_timestamp"]
_last_error = f"On-chain scraping failed: {e}"
for k in onchain_keys:
if k in existing_cache:
metrics[k] = existing_cache[k]
else: else:
# Reuse cached on-chain values — they're stored permanently # Reuse cached on-chain values — they're stored permanently
log.info("Reusing cached on-chain data (use Full Refresh to re-scrape)") log.info("Reusing cached on-chain data (use Full Refresh to re-scrape)")
+15
View File
@@ -108,6 +108,21 @@ def test_partial_fast_scrape_preserves_last_known_good_metric(server, monkeypatc
assert saved["fear_greed"]["source"] == "alternative.me" 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): def test_expired_onchain_timestamp_triggers_real_refresh(server, monkeypatch, tmp_path):
old = datetime.now(timezone.utc) - timedelta(hours=7) old = datetime.now(timezone.utc) - timedelta(hours=7)
cache_path = tmp_path / "cache.json" cache_path = tmp_path / "cache.json"