fix: isolate on-chain provider outages
This commit is contained in:
+26
-15
@@ -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,15 +283,8 @@ 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()
|
|
||||||
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",
|
checkonchain_keys = {"sopr", "sellside_risk", "active_address_momentum",
|
||||||
"txcount_momentum", "nvt_price", "vdd_multiple"}
|
"txcount_momentum", "nvt_price", "vdd_multiple"}
|
||||||
for key in onchain_keys:
|
for key in onchain_keys:
|
||||||
@@ -279,13 +293,10 @@ def run_scrape(force_full=False):
|
|||||||
existing_cache.get(key), onchain.get(key), source=source,
|
existing_cache.get(key), onchain.get(key), source=source,
|
||||||
error="metric missing from scrape",
|
error="metric missing from scrape",
|
||||||
)
|
)
|
||||||
|
if successful_sources:
|
||||||
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)")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user