mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-06-06 13:22:12 +00:00
scripts: add docker-compose resolver setup (#1793)
This commit is contained in:
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sync progress for the Reth + Nimbus stack.
|
||||
|
||||
Usage:
|
||||
./progress.py # continuous (Ctrl-C to exit, auto-exits when synced)
|
||||
./progress.py --once # single snapshot
|
||||
|
||||
Requires Nimbus REST port exposed at 127.0.0.1:5052 (add --rest flag in compose).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from collections import deque
|
||||
from datetime import timedelta
|
||||
from urllib.error import URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RETH = "http://127.0.0.1:8545"
|
||||
NIMBUS = "http://127.0.0.1:5052"
|
||||
INTERVAL = 5
|
||||
WINDOW = 60
|
||||
BAR_W = 40
|
||||
|
||||
# ANSI helpers
|
||||
def c(s, code): return f"\033[{code}m{s}\033[0m"
|
||||
GREEN, YELLOW, RED, DIM, BOLD = "32", "33", "31", "2;37", "1"
|
||||
|
||||
|
||||
def rpc(method):
|
||||
body = json.dumps({"jsonrpc": "2.0", "method": method, "params": [], "id": 1}).encode()
|
||||
req = Request(RETH, data=body, headers={"Content-Type": "application/json"})
|
||||
return json.loads(urlopen(req, timeout=5).read())["result"]
|
||||
|
||||
|
||||
def get_reth():
|
||||
try:
|
||||
r = rpc("eth_syncing")
|
||||
try:
|
||||
peers = int(rpc("net_peerCount"), 16)
|
||||
except Exception:
|
||||
peers = -1 # net namespace not exposed
|
||||
if r is False:
|
||||
head = int(rpc("eth_blockNumber"), 16)
|
||||
return {"state": "synced", "current": head, "target": head, "peers": peers,
|
||||
"stage": None, "stages": {}, "err": None}
|
||||
current = int(r["currentBlock"], 16)
|
||||
highest = int(r["highestBlock"], 16)
|
||||
# Build stage map (name -> block).
|
||||
stages = {s["name"]: int(s["block"], 16) for s in r.get("stages", [])}
|
||||
active_stages = {k: v for k, v in stages.items() if v > 0}
|
||||
# Headers download phase: nothing has progressed yet.
|
||||
if current == 0 and highest == 0 and not active_stages:
|
||||
return {"state": "headers", "current": 0, "target": 0, "peers": peers,
|
||||
"stage": "Headers", "stages": stages, "err": None}
|
||||
# Derive progress from the stages pipeline.
|
||||
# Bottleneck (rate-limiting stage) = stage with lowest non-zero block.
|
||||
# Target = leading stage block (typically Headers = chain tip).
|
||||
# Reth's top-level currentBlock/highestBlock are unreliable during initial
|
||||
# sync (often 0 until execution stage runs), so prefer stages-derived values.
|
||||
if active_stages:
|
||||
bottleneck = min(active_stages, key=active_stages.get)
|
||||
stage_current = active_stages[bottleneck]
|
||||
stage_target = max(stages.values()) if stages else 0
|
||||
# Trust the stages-derived values if highest is unset or stages tip is higher.
|
||||
if highest <= 0 or stage_target > highest:
|
||||
current = stage_current
|
||||
highest = stage_target
|
||||
elif current <= 0:
|
||||
current = stage_current
|
||||
else:
|
||||
bottleneck = None
|
||||
return {"state": "syncing", "current": current, "target": highest,
|
||||
"peers": peers, "stage": bottleneck, "stages": stages, "err": None}
|
||||
except URLError as e:
|
||||
return {"state": "down", "current": 0, "target": 0, "peers": 0,
|
||||
"stage": None, "stages": {}, "err": str(e.reason)}
|
||||
except Exception as e:
|
||||
return {"state": "error", "current": 0, "target": 0, "peers": 0,
|
||||
"stage": None, "stages": {}, "err": str(e)}
|
||||
|
||||
|
||||
def get_nimbus():
|
||||
try:
|
||||
d = json.loads(urlopen(f"{NIMBUS}/eth/v1/node/syncing", timeout=5).read())["data"]
|
||||
peers_d = json.loads(urlopen(f"{NIMBUS}/eth/v1/node/peer_count", timeout=5).read())["data"]
|
||||
head = int(d["head_slot"])
|
||||
dist = int(d["sync_distance"])
|
||||
peers = int(peers_d.get("connected", "0"))
|
||||
return {"state": "synced" if not d["is_syncing"] else "syncing",
|
||||
"current": head, "target": head + dist, "peers": peers,
|
||||
"optimistic": bool(d.get("is_optimistic", False)),
|
||||
"el_offline": bool(d.get("el_offline", False)),
|
||||
"err": None}
|
||||
except URLError as e:
|
||||
return {"state": "down", "current": 0, "target": 0, "peers": 0,
|
||||
"optimistic": False, "el_offline": False, "err": str(e.reason)}
|
||||
except Exception as e:
|
||||
return {"state": "error", "current": 0, "target": 0, "peers": 0,
|
||||
"optimistic": False, "el_offline": False, "err": str(e)}
|
||||
|
||||
|
||||
def format_num(n): return f"{n:,}"
|
||||
|
||||
|
||||
def format_eta(seconds):
|
||||
if seconds is None: return "?"
|
||||
if seconds < 0: return "?"
|
||||
if seconds < 60: return f"{int(seconds)}s"
|
||||
if seconds < 3600:
|
||||
return f"{int(seconds // 60)}m {int(seconds % 60)}s"
|
||||
if seconds < 86400:
|
||||
return f"{int(seconds // 3600)}h {int((seconds % 3600) // 60)}m"
|
||||
return f"{int(seconds // 86400)}d {int((seconds % 86400) // 3600)}h"
|
||||
|
||||
|
||||
def rate_per_sec(history):
|
||||
if len(history) < 2: return None
|
||||
t0, c0 = history[0]
|
||||
t1, c1 = history[-1]
|
||||
if t1 <= t0: return None
|
||||
return (c1 - c0) / (t1 - t0)
|
||||
|
||||
|
||||
def eta_seconds(history, target):
|
||||
r = rate_per_sec(history)
|
||||
if r is None or r <= 0: return None
|
||||
remaining = target - history[-1][1]
|
||||
if remaining <= 0: return 0
|
||||
return remaining / r
|
||||
|
||||
|
||||
def progress_bar(pct):
|
||||
pct = max(0.0, min(100.0, pct))
|
||||
filled = int(pct / 100 * BAR_W)
|
||||
return c("█" * filled, GREEN) + c("░" * (BAR_W - filled), DIM)
|
||||
|
||||
|
||||
def peers_label(peers):
|
||||
if peers < 0:
|
||||
return c("· peers unknown (enable net namespace)", DIM)
|
||||
return c(f"· {peers} peers", DIM)
|
||||
|
||||
|
||||
def stages_summary(stages):
|
||||
"""One-line view: stages that have progressed, with their block numbers."""
|
||||
if not stages:
|
||||
return ""
|
||||
advanced = [(n, b) for n, b in stages.items() if b > 0]
|
||||
if not advanced:
|
||||
return c(" stages: all 0 (headers downloading)", DIM)
|
||||
advanced.sort(key=lambda kv: kv[1], reverse=True)
|
||||
parts = [f"{n}={format_num(b)}" for n, b in advanced[:4]]
|
||||
return c(" stages: " + ", ".join(parts), DIM)
|
||||
|
||||
|
||||
def render_one(name, x, hist):
|
||||
state = x["state"]
|
||||
peers = x.get("peers", 0)
|
||||
extras = []
|
||||
if name == "Nimbus":
|
||||
if x.get("optimistic"):
|
||||
extras.append(c("(optimistic head — Reth not yet verifying)", YELLOW))
|
||||
if x.get("el_offline"):
|
||||
extras.append(c("⚠ EL OFFLINE", RED))
|
||||
if state == "synced":
|
||||
out = [f" {c(name, BOLD):<14s} {c('✓ synced', GREEN)} {c(format_num(x['current']), BOLD)} {peers_label(peers)}"]
|
||||
elif state == "headers":
|
||||
out = [
|
||||
f" {c(name, BOLD):<14s} {c('⧗ headers', YELLOW)} {c('downloading initial chain', DIM)} {peers_label(peers)}",
|
||||
f" {c('(per-block progress unavailable until headers validated — see docker logs)', DIM)}",
|
||||
]
|
||||
elif state == "syncing" and x["target"] <= 0:
|
||||
out = [f" {c(name, BOLD):<14s} {c('⧗ syncing', YELLOW)} {c('waiting for fork-choice', DIM)} {peers_label(peers)}"]
|
||||
elif state == "syncing":
|
||||
pct = x["current"] / x["target"] * 100
|
||||
r = rate_per_sec(hist)
|
||||
eta = eta_seconds(hist, x["target"])
|
||||
rate_s = f"{format_num(int(r))} /s" if r and r > 0 else c("stalled", RED)
|
||||
eta_s = format_eta(eta) if eta is not None else "?"
|
||||
stage = x.get("stage")
|
||||
stage_s = c(f"[{stage}]", DIM) if stage else ""
|
||||
out = [
|
||||
f" {c(name, BOLD):<14s} {c('⧗ syncing', YELLOW)} {format_num(x['current'])} / {format_num(x['target'])} {stage_s} {peers_label(peers)}",
|
||||
f" {progress_bar(pct)} {c(f'{pct:6.2f}%', BOLD)}",
|
||||
f" {c(rate_s, DIM)} ETA {c(eta_s, BOLD)}",
|
||||
]
|
||||
else:
|
||||
out = [
|
||||
f" {c(name, BOLD):<14s} {c('✗ ' + state, RED)}",
|
||||
f" {c(x.get('err') or '', DIM)}",
|
||||
]
|
||||
# Reth-only: stages summary
|
||||
if name == "Reth" and x.get("stages"):
|
||||
out.append(f" {stages_summary(x['stages'])}")
|
||||
for e in extras:
|
||||
out.append(f" {e}")
|
||||
return out
|
||||
|
||||
|
||||
def render(reth, nimbus, reth_hist, nim_hist):
|
||||
print("\033[2J\033[H", end="")
|
||||
width = 64
|
||||
title = f"Reth + Nimbus sync"
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
print()
|
||||
print(f" {c(title, BOLD)} {c(ts, DIM)}")
|
||||
print(f" {c('─' * width, DIM)}")
|
||||
print()
|
||||
for line in render_one("Reth", reth, reth_hist):
|
||||
print(line)
|
||||
print()
|
||||
for line in render_one("Nimbus", nimbus, nim_hist):
|
||||
print(line)
|
||||
print()
|
||||
win_s = (len(reth_hist) - 1) * INTERVAL if len(reth_hist) > 1 else 0
|
||||
print(f" {c(f'window {win_s}s · refresh {INTERVAL}s · Ctrl-C to exit', DIM)}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
once = "--once" in sys.argv
|
||||
reth_hist = deque(maxlen=WINDOW)
|
||||
nim_hist = deque(maxlen=WINDOW)
|
||||
try:
|
||||
while True:
|
||||
r = get_reth()
|
||||
n = get_nimbus()
|
||||
now = time.time()
|
||||
if r["target"] > 0 or r["state"] == "syncing":
|
||||
reth_hist.append((now, r["current"]))
|
||||
if n["target"] > 0 or n["state"] == "syncing":
|
||||
nim_hist.append((now, n["current"]))
|
||||
render(r, n, reth_hist, nim_hist)
|
||||
if once:
|
||||
break
|
||||
if r["state"] == "synced" and n["state"] == "synced":
|
||||
print(f" {c('✓ all synced.', GREEN)}\n")
|
||||
break
|
||||
time.sleep(INTERVAL)
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user