Files
blockbook/.github/scripts/wait_for_blockbook_sync.py

188 lines
5.8 KiB
Python

#!/usr/bin/env python3
import json
import os
import ssl
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
def fail(message: str) -> None:
print(f"error: {message}", file=sys.stderr)
raise SystemExit(1)
def parse_requested_coins(raw: str) -> list[str]:
text = raw.strip()
if not text:
fail("COINS_INPUT is empty")
seen = set()
result = []
for part in text.split(","):
coin = part.strip().lower()
if not coin or coin in seen:
continue
seen.add(coin)
result.append(coin)
if not result:
fail("COINS_INPUT resolved to an empty list")
return result
def normalize_http_base(raw: str) -> str:
parsed = urllib.parse.urlparse(raw.strip())
if parsed.scheme not in ("http", "https"):
fail(f"unsupported HTTP scheme {parsed.scheme!r} in {raw!r}")
if not parsed.netloc:
fail(f"missing host in {raw!r}")
return urllib.parse.urlunparse(
(parsed.scheme, parsed.netloc, parsed.path or "/", "", "", "")
).rstrip("/")
def should_upgrade_to_https(status: int, body: bytes, base_url: str) -> bool:
if status != 400:
return False
if "http request to an https server" not in body.decode("utf-8", "replace").lower():
return False
parsed = urllib.parse.urlparse(base_url)
return parsed.scheme == "http"
def upgrade_http_base_to_https(raw: str) -> str:
parsed = urllib.parse.urlparse(raw)
if parsed.scheme != "http":
return raw
return urllib.parse.urlunparse(
("https", parsed.netloc, parsed.path, "", "", "")
).rstrip("/")
def resolve_http_base(coin: str) -> str:
value = os.environ.get("BB_TEST_API_URL_HTTP_" + coin, "").strip()
if not value:
fail(f"missing BB_TEST_API_URL_HTTP_{coin} for selected test coin {coin!r}")
return normalize_http_base(value)
def preview_body(body: bytes, limit: int = 200) -> str:
text = body.decode("utf-8", "replace").strip()
if len(text) <= limit:
return text
return text[: limit - 3] + "..."
def fetch_status(base_url: str, request_timeout: int) -> tuple[int, bytes]:
request = urllib.request.Request(base_url + "/api/status")
context = ssl._create_unverified_context()
with urllib.request.urlopen(request, timeout=request_timeout, context=context) as resp:
return resp.getcode(), resp.read()
def parse_sync_state(body: bytes) -> tuple[bool, str]:
try:
payload = json.loads(body)
except json.JSONDecodeError as exc:
return False, f"invalid JSON: {exc}"
blockbook = payload.get("blockbook")
if not isinstance(blockbook, dict):
return False, "response missing blockbook object"
in_sync = blockbook.get("inSync")
best_height = blockbook.get("bestHeight")
summary = f"inSync={in_sync!r}, bestHeight={best_height!r}"
return in_sync is True, summary
def main() -> None:
coins = parse_requested_coins(os.environ.get("COINS_INPUT", ""))
timeout_seconds = int(os.environ.get("SYNC_TIMEOUT_SECONDS", "1800"))
poll_seconds = int(os.environ.get("SYNC_POLL_SECONDS", "10"))
request_timeout = int(os.environ.get("SYNC_REQUEST_TIMEOUT_SECONDS", "20"))
pending = {}
last_seen = {}
for coin in coins:
if coin in pending:
continue
pending[coin] = resolve_http_base(coin)
last_seen[coin] = "not checked yet"
deadline = time.monotonic() + timeout_seconds
print(
"Waiting for Blockbook sync:",
", ".join(f"{coin} -> {base}" for coin, base in sorted(pending.items())),
flush=True,
)
while pending:
for coin in sorted(list(pending)):
base_url = pending[coin]
try:
status, body = fetch_status(base_url, request_timeout)
except urllib.error.HTTPError as exc:
status = exc.code
body = exc.read()
except Exception as exc:
last_seen[coin] = f"{base_url}/api/status request failed: {exc}"
continue
if should_upgrade_to_https(status, body, base_url):
base_url = upgrade_http_base_to_https(base_url)
pending[coin] = base_url
try:
status, body = fetch_status(base_url, request_timeout)
except urllib.error.HTTPError as exc:
status = exc.code
body = exc.read()
except Exception as exc:
last_seen[coin] = f"{base_url}/api/status request failed: {exc}"
continue
if status != 200:
last_seen[coin] = (
f"{base_url}/api/status returned HTTP {status}: {preview_body(body)}"
)
continue
in_sync, summary = parse_sync_state(body)
last_seen[coin] = f"{base_url}/api/status returned HTTP 200: {summary}"
if in_sync:
print(f"{coin}: synced ({summary})", flush=True)
del pending[coin]
if not pending:
break
remaining_seconds = int(max(0, deadline - time.monotonic()))
if remaining_seconds == 0:
break
details = "; ".join(
f"{coin}: {last_seen[coin]}" for coin in sorted(pending)
)
print(
f"Still waiting for Blockbook sync ({remaining_seconds}s left): {details}",
flush=True,
)
time.sleep(min(poll_seconds, remaining_seconds))
if pending:
details = "; ".join(
f"{coin}: {last_seen[coin]}" for coin in sorted(pending)
)
fail(
f"timed out after {timeout_seconds}s waiting for Blockbook sync. {details}"
)
print("All selected Blockbook instances are synced.", flush=True)
if __name__ == "__main__":
main()