bb8b71278aa4015cbee2f538beabb562bf737ee4
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c4e9983ea |
Survive a dropped Electrum connection, and fall back to other servers
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> |
||
|
|
daf66fd6bc |
Fix the round-open race, the advertised jackpot, and payout error handling
Round opening (B-09). open_new_round_if_needed now handles the IntegrityError from ix_rounds_single_active (previous commit) by rolling back and using the winner's round. Deviation from the plan in BUGS.md, which proposed making the scheduler the only writer: that would mean the first bet after a cooldown couldn't open a round, so both callers stay and a bounded retry was added instead — a conflict where nothing is active yet just means the winner hadn't committed, and a bet must not fail on that timing. get_active_round also logs loudly if it ever sees more than one active round rather than silently picking the newest. The jackpot (B-11). It was participant_count * the *current* bet_amount_sats, which overstated the pool (each stored bet is already net of that bet's network fee) and silently rewrote the advertised jackpot of a round in progress whenever an operator edited the bet amount. It now sums the participants' stored bet_amount_sats. The remaining imprecision — the payout tx's own fee, deducted from the winner's share and unknowable until the payout is built — is documented in the code rather than promised away, since the comment there claimed exactness. Payout (B-05, B-18). _trigger_payout is split into read / build+broadcast / persist, so no DB session is held across a network call (on SQLite that meant holding the write lock for two unbounded round-trips). That restructuring is also what makes the error handling placeable: it now catches Exception around the chain work and writes a payout_failed audit entry, where a malformed fee_address used to raise EmbitError all the way to the scheduler's catch-all, leaving the round stuck in paying_out with nothing recorded about why. Automatic payout retry remains an open gap. The scheduler also counts "building" participants as in-flight when deciding whether a round may close, matching the two-phase bet write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a857f0e07 |
Show pending-inclusive balance and per-player round outcome reliably
Balance display: place_bet/request_withdrawal spend whole UTXOs and mark them spent at broadcast time, well before confirmation, so the confirmed-only balance could drop by far more than the amount actually moving. Add compute_pending_balance() (app/wallet/balance.py) to fold the unconfirmed change from in-flight bet/withdrawal PendingTransactions back in; GET /users/me now returns pending_balance_sats + has_pending, and the frontend shows it colored green (settled) or amber (still pending) instead of the confirmed-only figure. Round outcome display: the win/lose reveal and the "pagamento al vincitore in corso" status were fighting over the same UI slot, and the reveal broke across a page refresh. Now: - The round-status box (generic phase progress) and the personal win/lose box are independent and can both be visible at once. - The win/lose box only renders for users who actually played in that round (new user_played field on GET /rounds/current, via a new optional-auth dependency so the endpoint stays usable logged-out). - The reveal delay is anchored to the round's server-provided closes_at instead of a client-side "first seen" timestamp, so repeated reloads can't reset it, and the revealed result is persisted in localStorage so it survives a refresh even after the round has fully closed. - GET /users/me/last-round-result is a durable DB-backed backstop for players who miss the live window entirely (backgrounded tab, offline). Also hardens the frontend polling loop: call() now times out instead of hanging forever, and a session-epoch counter stops an in-flight request from a previous login from resurrecting a duplicate poll loop after logout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |