From 2da5d20ccd783be63bf3551a4f305fd7836bad10 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 26 Jul 2026 23:32:54 +0000 Subject: [PATCH] fix: isolate on-chain provider outages --- dashboard/server.py | 59 +++++++++++++++++++------------- tests/test_server_reliability.py | 15 ++++++++ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/dashboard/server.py b/dashboard/server.py index d01356d..46644e2 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -158,9 +158,30 @@ 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. - + By default, only refreshes fast data (price, F&G) and reuses cached on-chain data. On-chain metrics (Playwright scrapes) only refresh if: - force_full=True (manual full refresh) @@ -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)") diff --git a/tests/test_server_reliability.py b/tests/test_server_reliability.py index 66e2872..3b86c68 100644 --- a/tests/test_server_reliability.py +++ b/tests/test_server_reliability.py @@ -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"