Snapshot a round's timing when it opens (B-61)

round_duration_seconds was read live on every scheduler tick and every bet
check, with the deadline computed as opened_at + duration. Lowering it from 600
to 60 while a round was 300s in closed that round instantly; raising it moved
the closes_at clients were already counting down to. round_cooldown_seconds had
the same property for the gap after a close. B-11 fixed this class of problem
for the advertised jackpot; the timing fields were left live.

Round now carries duration_seconds and cooldown_seconds, set from the config
when it opens. round_deadline() is the single place the deadline is computed —
the scheduler, place_bet's two checks and /rounds/current's closes_at all go
through it — and the cooldown is read off the round that just closed, so the gap
a round announced is the gap that's honoured. The config row becomes what the
*next* round opens with.

The migration backfills from the live config rather than leaving the column
defaults: an instance running 300s rounds would otherwise see the round
currently in progress jump to 600s the moment this lands, which is precisely the
retroactive change being fixed. Verified against a scratch DB with a non-default
config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 23:25:56 +02:00
co-authored by Claude Opus 5
parent 77e07e87dc
commit 37cc5eeeb5
11 changed files with 169 additions and 37 deletions
@@ -0,0 +1,42 @@
"""snapshot round timing onto the round row (B-61)
Revision ID: 283844a44b4a
Revises: c1d4a97b5e10
Create Date: 2026-08-03 23:05:04.996492
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '283844a44b4a'
down_revision: Union[str, Sequence[str], None] = 'c1d4a97b5e10'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.add_column('rounds', sa.Column('duration_seconds', sa.Integer(), server_default='600', nullable=False))
op.add_column('rounds', sa.Column('cooldown_seconds', sa.Integer(), server_default='30', nullable=False))
# Backfill from the live config rather than leaving the column defaults: an
# instance running with, say, a 300s round would otherwise see every existing
# row — including the round currently in progress — jump to 600s the moment
# this migration lands, which is exactly the retroactive change B-61 is about.
op.execute(
"UPDATE rounds SET "
"duration_seconds = coalesce((SELECT round_duration_seconds FROM round_config LIMIT 1), 600), "
"cooldown_seconds = coalesce((SELECT round_cooldown_seconds FROM round_config LIMIT 1), 30)"
)
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('rounds', 'cooldown_seconds')
op.drop_column('rounds', 'duration_seconds')
# ### end Alembic commands ###