diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index a79ad66..bdf7e92 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -154,10 +154,25 @@ services: - noter_mnemonic environment: TZ: UTC - RELAY_RPC: ws://217.77.6.126:9944 - ASSET_HUB_RPC: ws://217.77.6.126:40944 - PEOPLE_RPC: ws://217.77.6.126:41944 + # The block-producing nodes bind their RPC to localhost by design — reaching + # them from here needed the ports open to the internet, and closing them (as + # they should be) silently cut this bot off from every chain. These are the + # public endpoints, which is what an external client is supposed to use, and + # they are TLS-terminated rather than plaintext ws:// across the internet. + RELAY_RPC: wss://rpc.pezkuwichain.io + ASSET_HUB_RPC: wss://asset-hub-rpc.pezkuwichain.io + PEOPLE_RPC: wss://people-rpc.pezkuwichain.io SCAN_INTERVAL_MS: "300000" + # The bot writes a heartbeat when a scan completes, not when it merely stays + # up: an endless reconnect loop kept the container "Up" for three weeks while + # no staking data reached the People chain. + healthcheck: + test: ["CMD", "node", "-e", "const fs=require('fs');const i=parseInt(process.env.SCAN_INTERVAL_MS||'300000',10);let t=0;try{t=Number(fs.readFileSync(process.env.HEARTBEAT_FILE||'/tmp/noter-heartbeat','utf8'))}catch{};process.exit(Number.isFinite(t)&&Date.now()-t { finalizeMaturedPending(peopleApi, noterKeypair) .then(() => fullScan(relayApi, assetHubApi, peopleApi, noterKeypair)) + .then(() => touchHeartbeat()) .catch(err => { log('ERROR', 'Periodic scan failed', { error: err.message }); }); }, SCAN_INTERVAL); + + // Exiting is the point. Reconnecting forever looks like the bot is coping; a + // container that keeps dying does not, and the restart policy makes that + // visible in `docker ps` without anyone having to read the logs. + setInterval(() => { + const age = heartbeatAge(); + if (age > STALE_AFTER) { + log('ERROR', 'No scan completed within the staleness window — exiting so the restart policy takes over', { + stale_for_ms: Number.isFinite(age) ? age : null, + stale_after_ms: STALE_AFTER, + }); + process.exit(1); + } + }, Math.min(SCAN_INTERVAL, 60_000)).unref?.(); } main().catch(err => {