A dropped connection used to hang the whole platform permanently, and three defects composed to do it (BUGS.md B-01): The read loop's death was invisible. When the socket closed, _read_loop broke out and finished, but _run_once was blocked on gather() over two notification consumers waiting on queues nobody would ever fill again — it never returned and never raised, so the reconnect-with-backoff logic was unreachable. client.wait_closed() now resolves when the loop ends for any reason, and _run_once races it against the consumers and a keepalive with asyncio.wait(FIRST_COMPLETED). Nothing had a timeout. request() registered a future, wrote to a half-closed socket (drain() often doesn't raise) and awaited a reply that would never come. That hung a POST /bets *while holding the per-user lock*, and could stop the confirmation poller for good. Every request is now bounded at 15s, and a timeout tears the connection down rather than leaving a server that owes us a reply in rotation. There was no keepalive, so on a quiet instance the normal way this connection dies is an idle-timeout drop by the server (~10 minutes for many). A server.ping every 60s makes that observable within a minute. listener.client is also cleared before reconnecting, so callers stop treating a dead connection as live. On top of the finding, the listener now rotates over a list of servers: ELECTRUM_FALLBACK_SERVERS holds comma-separated host:port[:notls] extras, tried after the primary. Everything the platform does goes through this one connection — deposit credits, broadcasts, confirmations, the chain tip the draw waits on — which made a single hardcoded server its biggest point of failure. A failed or dropped session moves to the next server immediately and only sleeps on the backoff once every server has had a turn, so one dead server costs one attempt instead of an outage, while a genuinely offline network still backs off. A malformed entry fails at startup, not during the outage when the fallback is what you need. Also fixes B-19: header handling refuses a height below the current tip and applies height and hex together, since _wait_for_next_block waits for tip_height > tip_at_close (a regression silently added a block to the draw's wait) and that hex is the draw's entropy source, so a mismatched pair would be worse than a stale one. Verified in the live deployment: the log shows the endpoint list, then "Electrum connected to santantonio.sytes.net:50002", and the connection holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
30 lines
1.4 KiB
Bash
30 lines
1.4 KiB
Bash
ELECTRUM_HOST=santantonio.sytes.net
|
|
ELECTRUM_PORT=50002
|
|
ELECTRUM_USE_SSL=true
|
|
|
|
# Additional Electrum servers to fall back to, comma-separated. Each entry is
|
|
# `host:port` (TLS, the normal case) or `host:port:notls`. The listener rotates
|
|
# over the primary above plus these, so one unreachable server costs a single
|
|
# reconnect attempt instead of an outage — every deposit credit, broadcast and
|
|
# confirmation goes through this one connection, which makes a single server the
|
|
# platform's biggest single point of failure. A typo here fails at startup rather
|
|
# than during the outage when the fallback is what you need.
|
|
# Example: ELECTRUM_FALLBACK_SERVERS=node2.example.net:50002,node3.example.net:50001:notls
|
|
ELECTRUM_FALLBACK_SERVERS=
|
|
|
|
# Fernet key protecting the master xprv at rest. Generate with:
|
|
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
|
XPRV_ENCRYPTION_KEY=
|
|
|
|
# Random secret for JWT session signing. Generate with:
|
|
# python -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
JWT_SECRET=
|
|
|
|
# Bearer token required on the admin endpoints (X-Admin-Token header). Generate with:
|
|
# python -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
ADMIN_TOKEN=
|
|
|
|
# Every business/round parameter (bet amount, round duration/cooldown, min
|
|
# amount, fee rate, RBF timeout, fee address) is configured live from the
|
|
# admin panel (/admin) instead of here — see docs/guida-admin.md.
|