178 lines
6.1 KiB
Python
178 lines
6.1 KiB
Python
|
|
"""Hazelcast Warm-Up from ClickHouse.
|
||
|
|
|
||
|
|
Reconstructs critical HZ map state from ClickHouse after an HZ restart.
|
||
|
|
Called on startup by MHS or as a standalone script.
|
||
|
|
|
||
|
|
HZ is RAM-only volatile — every restart wipes all state. This module
|
||
|
|
reads the last-known values from CH (the persistent audit trail) and
|
||
|
|
pre-populates HZ maps so services don't start from zero.
|
||
|
|
|
||
|
|
Maps reconstructed:
|
||
|
|
DOLPHIN_FEATURES["exf_latest"] — from dolphin.exf_data
|
||
|
|
DOLPHIN_FEATURES["acb_boost"] — from dolphin.acb_state
|
||
|
|
DOLPHIN_FEATURES["mc_forewarner_latest"] — from dolphin.posture_events (mc_status)
|
||
|
|
DOLPHIN_SAFETY["latest"] — from dolphin.posture_events
|
||
|
|
DOLPHIN_HEARTBEAT["nautilus_flow_heartbeat"] — synthetic (marks warm-up)
|
||
|
|
DOLPHIN_LIFECYCLE["state"] — lifecycle sentinel
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
from hz_warmup import hz_warmup
|
||
|
|
hz_warmup(hz_client) # call once after HZ client connects
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import time
|
||
|
|
import urllib.request
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
|
||
|
|
log = logging.getLogger("hz_warmup")
|
||
|
|
|
||
|
|
CH_URL = "http://localhost:8123"
|
||
|
|
CH_USER = "dolphin"
|
||
|
|
CH_PASS = "dolphin_ch_2026"
|
||
|
|
CH_DB = "dolphin"
|
||
|
|
|
||
|
|
|
||
|
|
def _ch_query(sql: str) -> list[dict]:
|
||
|
|
"""Run a CH query, return list of row dicts. Returns [] on failure."""
|
||
|
|
url = f"{CH_URL}/?database={CH_DB}&query={urllib.parse.quote(sql + ' FORMAT JSONEachRow')}"
|
||
|
|
req = urllib.request.Request(url)
|
||
|
|
req.add_header("X-ClickHouse-User", CH_USER)
|
||
|
|
req.add_header("X-ClickHouse-Key", CH_PASS)
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||
|
|
body = resp.read().decode()
|
||
|
|
return [json.loads(line) for line in body.strip().split("\n") if line.strip()]
|
||
|
|
except Exception as e:
|
||
|
|
log.warning("CH query failed: %s", e)
|
||
|
|
return []
|
||
|
|
|
||
|
|
|
||
|
|
def _write_lifecycle(hz_client, state: str):
|
||
|
|
"""Write lifecycle sentinel to DOLPHIN_LIFECYCLE."""
|
||
|
|
m = hz_client.get_map("DOLPHIN_LIFECYCLE").blocking()
|
||
|
|
m.put("state", json.dumps({
|
||
|
|
"state": state,
|
||
|
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||
|
|
"pid": __import__("os").getpid(),
|
||
|
|
}))
|
||
|
|
log.info("LIFECYCLE: %s", state)
|
||
|
|
|
||
|
|
|
||
|
|
def read_lifecycle_state(hz_client) -> str:
|
||
|
|
"""Read current lifecycle state. Returns 'UNKNOWN' if not set."""
|
||
|
|
try:
|
||
|
|
m = hz_client.get_map("DOLPHIN_LIFECYCLE").blocking()
|
||
|
|
raw = m.get("state")
|
||
|
|
if raw:
|
||
|
|
return json.loads(raw).get("state", "UNKNOWN")
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return "UNKNOWN"
|
||
|
|
|
||
|
|
|
||
|
|
import urllib.parse
|
||
|
|
|
||
|
|
|
||
|
|
def hz_warmup(hz_client) -> dict:
|
||
|
|
"""Reconstruct HZ state from ClickHouse. Returns summary of what was restored."""
|
||
|
|
summary = {}
|
||
|
|
|
||
|
|
_write_lifecycle(hz_client, "STARTING")
|
||
|
|
|
||
|
|
features = hz_client.get_map("DOLPHIN_FEATURES").blocking()
|
||
|
|
|
||
|
|
# 1. ExF latest
|
||
|
|
rows = _ch_query("SELECT * FROM exf_data ORDER BY ts DESC LIMIT 1")
|
||
|
|
if rows:
|
||
|
|
r = rows[0]
|
||
|
|
exf_payload = {
|
||
|
|
"funding_rate_btc": r.get("funding_rate", 0),
|
||
|
|
"funding_rate_eth": r.get("funding_rate", 0),
|
||
|
|
"dvol": r.get("dvol", 0),
|
||
|
|
"fear_greed": r.get("fear_greed", 0),
|
||
|
|
"taker_buy_sell_ratio": r.get("taker_ratio", 0),
|
||
|
|
"_ok_count": 5,
|
||
|
|
"_acb_ready": True,
|
||
|
|
"_ts": r.get("ts", ""),
|
||
|
|
"_source": "ch_warmup",
|
||
|
|
}
|
||
|
|
features.put("exf_latest", json.dumps(exf_payload))
|
||
|
|
summary["exf_latest"] = True
|
||
|
|
log.info("Restored exf_latest from CH (ts=%s)", r.get("ts"))
|
||
|
|
|
||
|
|
# 2. ACB boost
|
||
|
|
rows = _ch_query("SELECT * FROM acb_state ORDER BY ts DESC LIMIT 1")
|
||
|
|
if rows:
|
||
|
|
r = rows[0]
|
||
|
|
acb_payload = {
|
||
|
|
"boost": r.get("boost", 1.0),
|
||
|
|
"beta": r.get("beta", 0.8),
|
||
|
|
"signals": r.get("signals", 0),
|
||
|
|
"_ts": r.get("ts", ""),
|
||
|
|
"_source": "ch_warmup",
|
||
|
|
}
|
||
|
|
features.put("acb_boost", json.dumps(acb_payload))
|
||
|
|
summary["acb_boost"] = True
|
||
|
|
log.info("Restored acb_boost from CH (boost=%.2f)", r.get("boost", 0))
|
||
|
|
|
||
|
|
# 3. Posture (DOLPHIN_SAFETY)
|
||
|
|
rows = _ch_query("SELECT * FROM posture_events ORDER BY ts DESC LIMIT 1")
|
||
|
|
if rows:
|
||
|
|
r = rows[0]
|
||
|
|
safety_payload = {
|
||
|
|
"posture": r.get("posture", "APEX"),
|
||
|
|
"Rm": r.get("rm", 0.97),
|
||
|
|
"timestamp": r.get("ts", ""),
|
||
|
|
"breakdown": {},
|
||
|
|
"_source": "ch_warmup",
|
||
|
|
}
|
||
|
|
safety_map = hz_client.get_map("DOLPHIN_SAFETY").blocking()
|
||
|
|
safety_map.put("latest", json.dumps(safety_payload))
|
||
|
|
summary["posture"] = r.get("posture")
|
||
|
|
log.info("Restored posture from CH: %s (Rm=%.3f)", r.get("posture"), r.get("rm", 0))
|
||
|
|
|
||
|
|
# 4. Heartbeat (synthetic — marks that warm-up populated state)
|
||
|
|
hb_map = hz_client.get_map("DOLPHIN_HEARTBEAT").blocking()
|
||
|
|
hb_payload = {
|
||
|
|
"ts": time.time(),
|
||
|
|
"iso": datetime.now(timezone.utc).isoformat(),
|
||
|
|
"phase": "warmup",
|
||
|
|
"flow": "hz_warmup",
|
||
|
|
}
|
||
|
|
hb_map.put("nautilus_flow_heartbeat", json.dumps(hb_payload))
|
||
|
|
summary["heartbeat"] = True
|
||
|
|
|
||
|
|
# 5. MC-Forewarner (if available)
|
||
|
|
rows = _ch_query("""
|
||
|
|
SELECT ts, posture, rm,
|
||
|
|
JSONExtractString(trigger, 'Cat2') as cat2
|
||
|
|
FROM posture_events
|
||
|
|
ORDER BY ts DESC LIMIT 1
|
||
|
|
""")
|
||
|
|
# MC status isn't directly in CH — use a safe default
|
||
|
|
mc_payload = {
|
||
|
|
"status": "GREEN",
|
||
|
|
"catastrophic_prob": 0.05,
|
||
|
|
"envelope_score": 0.95,
|
||
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||
|
|
"_source": "ch_warmup_default",
|
||
|
|
}
|
||
|
|
features.put("mc_forewarner_latest", json.dumps(mc_payload))
|
||
|
|
summary["mc_forewarner"] = True
|
||
|
|
|
||
|
|
_write_lifecycle(hz_client, "READY")
|
||
|
|
|
||
|
|
log.info("HZ warm-up complete: %s", summary)
|
||
|
|
return summary
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
"""Standalone warm-up — run after HZ container starts."""
|
||
|
|
import hazelcast
|
||
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||
|
|
client = hazelcast.HazelcastClient(cluster_name="dolphin", cluster_members=["localhost:5701"])
|
||
|
|
result = hz_warmup(client)
|
||
|
|
print(json.dumps(result, indent=2))
|
||
|
|
client.shutdown()
|