7.7 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
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