12 KiB
Ranking Snapshot Read Model Plan
Objective
Define a snapshot-backed read model for weekly and monthly public ranking so GET /api/ranking does not depend on runtime aggregation over materialized RCON match stats on every request.
The design follows the same philosophy as the current annual ranking snapshot path:
- generate outside the public request path
- serve stable snapshot rows through a small read model
- expose clear metadata for
ready,missingand controlled fallback states
Scope
Impacted public endpoint:
GET /api/ranking?timeframe=weekly|monthly|annual&server_id=<scope>&metric=<metric>&limit=<n>&year=<year-when-annual>
Primary target of this plan:
- weekly ranking snapshots
- monthly ranking snapshots
Compatibility target:
- the model must also represent annual snapshots so the repository can converge on one ranking snapshot vocabulary over time
TASK-191should keep annual requests on the existing annual snapshot path until migration is explicitly implemented
Why This Is Needed
TASK-188 showed:
- weekly/monthly ranking still performs repeated window counting and grouped aggregation at request time
- stats/ranking runtime reads depend on
rcon_materialized_matchesandrcon_match_player_stats - the empty June 2026 window hides the structural cost, but the query plans still show scans and temp B-trees
TASK-189 reduced some scan risk with indexes, but it did not remove the core public-request recomputation pattern.
Proposed Tables
ranking_snapshots
Purpose:
- one snapshot header per
(timeframe, server_id, metric, window_start, window_end)
Proposed fields:
idtimeframe- allowed values:
weekly,monthly,annual
- allowed values:
server_idall-servers,comunidad-hispana-01,comunidad-hispana-02
metric- V1.1 weekly/monthly:
kills,deaths,teamkills,matches_considered,kd_ratio,kills_per_match - annual: keep
killsas the required supported metric until annual expansion is explicitly implemented
- V1.1 weekly/monthly:
window_startwindow_endgenerated_atsource- expected current value:
rcon-materialized-admin-log
- expected current value:
snapshot_status- expected values:
ready,building,failed
- expected values:
item_countlimit_sizesource_matches_countfreshness- example values:
fresh,stale
- example values:
error_message- nullable, operational only
Key rules:
- unique key on
(timeframe, server_id, metric, window_start, window_end) - keep only one
readysnapshot per exact window and metric scope item_countreflects stored rows, not the request limitlimit_sizerecords how many positions were generated, for example top 20
ranking_snapshot_items
Purpose:
- ordered player rows for one ranking snapshot
Proposed fields:
idsnapshot_idranking_positionplayer_idplayer_namemetric_valuematches_consideredkillsdeathsteamkillskd_ratiokills_per_match
Key rules:
- unique key on
(snapshot_id, ranking_position) - unique key on
(snapshot_id, player_id) - all item rows must carry the common display totals even when the requested metric is different
Snapshot Window Rules
Weekly
Window logic:
- preserve the current weekly selection policy already exposed by
select_leaderboard_window(...) - snapshot window is whichever week the public runtime policy would have served:
current-weekwhen the current week has sufficient closed matches- otherwise
previous-week
Stored metadata:
timeframe=weeklywindow_startwindow_endsnapshot_statusgenerated_atsource='rcon-materialized-admin-log'
Monthly
Window logic:
- preserve the current monthly selection policy already exposed by
select_leaderboard_window(...) - snapshot window is:
previous-monthuntil day 7 inclusivecurrent-monthafter day 7
Stored metadata:
timeframe=monthlywindow_startwindow_endsnapshot_statusgenerated_atsource='rcon-materialized-admin-log'
Annual
Window logic:
year-01-01T00:00:00Zto(year+1)-01-01T00:00:00Z
Transition note:
- the new table design supports annual snapshots
TASK-191should keep annual requests on the existingrcon_annual_ranking_snapshotspath until a dedicated migration task consolidates storage
Refresh Policy
Current windows:
- weekly current: refresh every
5to15minutes - monthly current: refresh every
15to30minutes - annual current: manual or daily
Closed windows:
- previous week: treated as closed and stable
- previous month: treated as closed and stable
- annual closed windows: treated as stable after generation
Operational rules:
- generation happens outside the public request path
- one refresh job should upsert the header row and replace the corresponding item set atomically
- if refresh fails, keep the last
readysnapshot until a newreadysnapshot is produced
Public Fallback Policy
Public read priority:
- serve snapshot when a matching
readysnapshot exists - if no matching snapshot exists:
- return controlled
snapshot_status='missing' - or use runtime fallback only when configuration explicitly enables it
- return controlled
- never recalculate by default on every public request
Required policy fields in API response:
sourcesnapshot_statusgenerated_atfreshnessfallback_usedwindow_startwindow_end
Expected meanings:
source='ranking-snapshot'when serving stored weekly/monthly snapshot rowssource='rcon-materialized-runtime-fallback'only when configuration allows fallback and snapshot is missingsnapshot_status='ready'when snapshot rows were servedsnapshot_status='missing'when no snapshot exists for the requested window and runtime fallback is disabled
Expected API Metadata
Weekly/monthly responses should expose:
page_kindtimeframeserver_idmetriclimitrequested_limitwindow_startwindow_endwindow_kindwindow_labelsnapshot_statusgenerated_atfreshnessfallback_usedsourceitems
Snapshot item contract:
ranking_positionplayer_idplayer_namemetric_valuematches_consideredkillsdeathsteamkillskd_ratiokills_per_match
Generation Source
Authoritative source for weekly/monthly/annual ranking snapshots:
rcon_materialized_matchesrcon_match_player_stats
Source filter:
matches.source_basis = 'admin-log-match-ended'
Generation logic:
- reuse the same metric formulas and tie-break ordering already documented for
Ranking - do not use public scoreboard as the primary ranking source
- do not reintroduce Comunidad Hispana #03
Transition Notes For TASK-191
Implementation order:
- add snapshot lookup helpers for weekly/monthly
- make
/api/rankingweekly/monthly try snapshot lookup first - keep annual on the current annual snapshot loader
- add controlled runtime fallback behind configuration
- return explicit metadata for
ready,missingand fallback cases
Expected runtime behavior in TASK-191:
- weekly/monthly:
- snapshot first
- runtime fallback only when snapshot is missing and fallback is enabled
- controlled missing when snapshot is missing and fallback is disabled
- annual:
- keep current snapshot behavior unchanged
Operational note after TASK-191:
- snapshot tables are initialized automatically on first ranking access
- snapshot rows are not generated automatically yet
- runtime fallback remains enabled by default for transition through
HLL_BACKEND_RANKING_RUNTIME_FALLBACK_ENABLED=true - operators can force controlled missing behavior by setting
HLL_BACKEND_RANKING_RUNTIME_FALLBACK_ENABLED=false
Manual Generation Workflow
Manual generator entrypoint:
python -m app.rcon_historical_leaderboards generate-ranking-snapshot --timeframe weekly --server-key all --metric kills --limit 20
Bulk refresh entrypoint:
python -m app.rcon_historical_leaderboards refresh-ranking-snapshots --limit 30
Operational default:
- when
HLL_BACKEND_DATABASE_URLis configured, the CLI uses PostgreSQL by default - SQLite is no longer the default operational target for snapshot generation
- local SQLite generation remains available only through an explicit override such as:
python -m app.rcon_historical_leaderboards generate-ranking-snapshot --timeframe weekly --server-key all --metric kills --limit 20 --sqlite-path backend/data/hll_vietnam_dev.sqlite3
Docker form:
docker compose exec backend python -m app.rcon_historical_leaderboards generate-ranking-snapshot --timeframe weekly --server-key all --metric kills --limit 20
Bulk Docker form:
docker compose exec backend python -m app.rcon_historical_leaderboards refresh-ranking-snapshots --limit 30
Operational expectation:
- the recommended Docker command should generate weekly/monthly snapshots in PostgreSQL, matching the
/api/rankingproduction read path
Supported manual parameters:
timeframe:weekly,monthlyserver-key:all,all-servers,comunidad-hispana-01,comunidad-hispana-02metric:kills,deaths,teamkills,matches_considered,kd_ratio,kills_per_matchlimit: positive integer, normally20
Current implementation note:
generate-ranking-snapshotremains unitary per command invocation for explicit manual controlrefresh-ranking-snapshots --limit 30generates the full weekly/monthly public matrix in one run- the periodic historical runner in
backend/app/historical_runner.pyinvokes that bulk refresh as part of the normal backend refresh cycle - current cycle order is:
- existing RCON ingestion/materialization cycle
player_search_indexplayer_period_statsranking_snapshots
- per-combination failures should be reported without aborting the entire matrix refresh
Recommended Combinations
Minimum production matrix for parity with the public Ranking filters:
- weekly +
all-servers+ all six supported metrics - weekly +
comunidad-hispana-01+ all six supported metrics - weekly +
comunidad-hispana-02+ all six supported metrics - monthly +
all-servers+ all six supported metrics - monthly +
comunidad-hispana-01+ all six supported metrics - monthly +
comunidad-hispana-02+ all six supported metrics
That matrix requires 36 snapshot generations for each refresh cycle.
Operational default limit:
- generate each combination with
limit=30 - that stored
limit_size=30covers the current UI request limits10,20and30
Suggested Frequency
Suggested operator cadence:
- weekly current window: regenerate every
5to15minutes while the active week is changing - monthly current window: regenerate every
15to30minutes while the active month is changing - previous week and previous month windows: regenerate once after closure or after any historical backfill that changes source coverage
Operational guidance:
- regenerate after materialized RCON/AdminLog data grows
- regenerate after manual backfill
- regenerate after metric SQL changes that affect ranking totals or ordering
- when using the periodic backend runner, keep ranking refresh attached to the same recurring cycle rather than a separate scheduler unless operational load proves otherwise
- the runner inherits its cadence from
HLL_HISTORICAL_REFRESH_INTERVAL_SECONDS - fallback runtime remains preserved for
/api/rankingif a requested snapshot is missing or unavailable
Ready Vs Fallback
Expected API result after successful generation for an exact requested combination:
snapshot_status=readyfallback_used=falsesource.read_model=ranking-snapshot
Expected API result when the requested combination has not been generated yet and fallback remains enabled:
snapshot_status=missingfallback_used=truefreshness=runtime
Expected API result when the requested combination has not been generated and fallback is disabled:
snapshot_status=missingfallback_used=falseitems=[]
Out Of Scope
- backend implementation of the snapshot generator
- migrations
- frontend changes
- annual storage migration from the legacy annual snapshot tables
- Elo/MMR
- public scoreboard as ranking primary source