15 KiB
id, source_task, status, type
| id | source_task | status | type |
|---|---|---|---|
| TASK-db-retention-audit-report | TASK-db-retention-audit | review | report |
Database Retention Audit
Scope
This report audits the current storage model used by HLL Vietnam for:
- live server snapshots;
- public-scoreboard historical matches and player stats;
- RCON AdminLog events and materialized matches;
- displayed historical snapshots;
- player event raw ledger;
- paused Elo/MMR storage surfaces.
Confirmed facts below are based on the current repository code, not on assumptions.
Files Inspected
AGENTS.mdai/repo-context.mdai/architecture-index.mdbackend/app/config.pybackend/app/historical_runner.pybackend/app/historical_storage.pybackend/app/historical_snapshot_storage.pybackend/app/player_event_aggregates.pybackend/app/player_event_storage.pybackend/app/postgres_display_storage.pybackend/app/postgres_rcon_storage.pybackend/app/rcon_admin_log_materialization.pybackend/app/rcon_admin_log_storage.pybackend/app/rcon_historical_backfill.pybackend/app/rcon_historical_leaderboards.pybackend/app/rcon_historical_read_model.pybackend/app/rcon_historical_worker.pybackend/app/writer_lock.py
Confirmed Storage Areas
Live and displayed data
server_snapshots- Present in SQLite live storage and PostgreSQL displayed storage.
- Used for live/current server status history.
displayed_historical_snapshots- PostgreSQL primary key is
(server_key, snapshot_type, metric, snapshot_window). - Persistence uses upsert semantics, so normal operation is already bounded by logical identity.
- PostgreSQL primary key is
Public-scoreboard historical fallback
historical_servershistorical_mapshistorical_matcheshistorical_playershistorical_player_match_statshistorical_ingestion_runshistorical_backfill_progress
These remain part of the displayed historical fallback/read path.
RCON historical primary path
rcon_historical_targetsrcon_historical_capture_runsrcon_historical_samplesrcon_historical_checkpointsrcon_historical_competitive_windowsrcon_admin_log_eventsrcon_player_profile_snapshotsrcon_materialized_matchesrcon_match_player_statsrcon_scoreboard_match_candidates
Player-event ledger
player_event_raw_ledgerplayer_event_ingestion_runsplayer_event_backfill_progress
Elo/MMR paused surfaces
Confirmed in backend documentation and runtime payloads as preserved but paused. This audit did not find cleanup code or explicit retention logic for Elo/MMR tables in the task’s read-first files, so they should stay out of cleanup scope.
Snapshot Boundedness Findings
displayed_historical_snapshots
Confirmed bounded in PostgreSQL by primary key plus upsert:
- same logical snapshot identity overwrites previous payload;
- normal scheduled refresh does not append unlimited rows;
- routine cleanup is not required for healthy records.
Safe policy:
- do not bulk-delete this table in the first maintenance command;
- only report suspicious legacy/corrupt rows if found later.
File-based snapshots
Confirmed bounded by deterministic file paths under backend/data/snapshots/<server>/....
Normal writes replace files in place.
Safe policy:
- do not add file cleanup in the first database-maintenance command.
Retention Windows Confirmed In Runtime Code
Two different historical paths currently exist, and they do not use identical monthly rules.
Public-scoreboard fallback (historical_*)
Confirmed in backend/app/historical_storage.py:
- weekly fallback can use previous week when current-week sample is insufficient;
- monthly fallback uses previous month only when current month has zero closed matches;
is_early_monthis computed withcurrent_time.day <= 3, but selection still depends on zero current-month matches.
RCON materialized leaderboards (rcon_materialized_matches)
Confirmed in backend/app/rcon_historical_leaderboards.py:
- monthly leaderboard selection uses previous month through day 7 inclusive;
- weekly selection falls back to previous week when current week lacks the configured minimum closed matches.
Cleanup Policy Recommendation
Because the new maintenance task explicitly requires:
- latest 100 materialized matches;
- current month;
- previous month during first 7 days of a new month;
- current week;
- previous week when weekly fallback may need it;
the cleanup command should implement the stricter policy directly instead of copying the older historical_storage.py month fallback behavior.
Safe-To-Delete Areas
1. server_snapshots
Safe to delete old rows by captured_at age once outside retention.
Why:
- live UI only needs recent history;
- no dependent child tables were found;
- latest snapshot queries use recent rows only.
Recommended initial retention:
- keep last 14 days by default.
2. Non-critical rcon_admin_log_events
Safe to delete by event age after retention.
Confirmed critical event types:
killmatch_startmatch_end
Everything else should be treated as non-critical for first-pass cleanup:
chatmessageconnecteddisconnectedteam_switchunknownkickban- any other non-materialization event type.
Recommended initial retention:
- non-critical: 30 days.
3. Critical rcon_admin_log_events
Conditionally safe to delete only when both are true:
- older than the critical retention threshold;
- not needed by protected materialized matches/windows.
Recommended initial retention:
- critical: 90 days.
4. Old rcon_materialized_matches
Conditionally safe to delete when all of the following are true:
- not in latest N closed matches;
- not in current month;
- not in previous month during the first 7 days of a new month;
- not in current week;
- not in previous week when weekly fallback may still be needed.
5. Child rows in rcon_match_player_stats
Safe to delete only as a dependent delete for non-protected materialized matches.
Delete these before deleting parent matches because no cascade constraint was found.
Areas That Must Be Protected
RCON materialized read path
Protect:
- latest 100 closed
rcon_materialized_matches; - all
rcon_match_player_statsfor protected matches; kill,match_start,match_endevents that still support protected matches or currently relevant windows.
Live/current match surfaces
Protect recent/current RCON data:
- recent
rcon_admin_log_eventsstill used by current-match kill feed and current-match player stats; - recent
server_snapshots.
Reason:
- current-match pages read recent AdminLog windows directly;
- premature deletion would break kill feed or partial player stats.
Displayed snapshots
Protect:
displayed_historical_snapshotsas a bounded read cache;- file-based snapshots under
backend/data/snapshots/.
Public-scoreboard historical_*
Do not delete in the first cleanup command.
Reason:
- these tables still back visible historical endpoints and fallback behavior;
- the task’s cleanup requirements are expressed around materialized matches and AdminLog, not around legacy scoreboard imports;
- deleting this domain now would widen scope materially.
Player-event ledger
Do not delete in the first cleanup command.
Reason:
- it feeds visible player-event snapshots and monthly MVP V2 support;
- no task requirement currently defines safe retention windows for this ledger;
- deleting it without product retention rules is risky.
Elo/MMR paused data
Do not delete.
Reason:
- task explicitly warns against unrelated Elo/MMR deletions;
- runtime is paused, but persisted surfaces are intentionally preserved.
Foreign Keys, Cascades, and Orphan Risk
Confirmed foreign keys
Public-scoreboard historical tables have foreign keys:
historical_matches.historical_server_id -> historical_servers.idhistorical_matches.historical_map_id -> historical_maps.idhistorical_player_match_stats.historical_match_id -> historical_matches.idhistorical_player_match_stats.historical_player_id -> historical_players.id
RCON/postgres tables also define references for some capture-oriented tables.
Missing cascade behavior relevant to cleanup
No ON DELETE CASCADE behavior was found for:
rcon_materialized_matchesrcon_match_player_stats
The materialization code explicitly deletes/rebuilds child player rows itself, which is another signal that cleanup must manage delete order manually.
Required delete order
For materialized RCON cleanup:
- identify protected match keys;
- identify candidate old matches outside protected windows;
- delete
rcon_match_player_statsfor candidate match keys; - delete candidate rows from
rcon_materialized_matches; - delete eligible
rcon_admin_log_events; - delete eligible
server_snapshots.
This order avoids orphaned player-stat rows.
Risks and Edge Cases
Timestamp parsing risk
Some match rows may have:
- missing
started_at; - missing
ended_at; - only server-time fields;
- malformed or non-ISO timestamps.
Safe rule:
- if a materialized match timestamp cannot be interpreted safely, protect it and skip deletion.
Cross-path monthly rule mismatch
There is a confirmed mismatch today:
- public-scoreboard fallback code does not strictly use previous month through day 7;
- RCON leaderboard code does.
Cleanup should follow the task requirement, not the older fallback implementation.
Current-match dependency on recent AdminLog
Current match pages query recent AdminLog rows directly, not only materialized matches. Deleting recent critical events too aggressively could break live/current-match UI even if historical leaderboards still work.
Optional table presence
Some environments may not have every migrated or optional table yet. Maintenance code should treat absent tables as controlled skips, not hard failures.
SQLite versus PostgreSQL
The repo still supports SQLite fallback when HLL_BACKEND_DATABASE_URL is unset.
Maintenance code should either:
- support SQLite safely for the targeted tables; or
- emit a clear unsupported message.
Given the codebase still keeps local SQLite testability, SQLite support is preferable for the cleanup command and its tests.
Recommended Cleanup Scope For First Implementation
Implement only:
server_snapshots;rcon_admin_log_events;rcon_materialized_matches;rcon_match_player_stats.
Explicitly skip in first implementation:
displayed_historical_snapshots;historical_*;player_event_raw_ledgerand related worker tables;- Elo/MMR tables;
rcon_historical_samples,rcon_historical_competitive_windows,rcon_player_profile_snapshots,rcon_scoreboard_match_candidates.
Reason:
- keeps scope aligned to the task;
- avoids deleting sources still needed for visible history fallback or paused domains;
- stays within the repository’s change-budget expectations.
Implementation Plan For app.database_maintenance
- Add maintenance config accessors for all retention values and batch size.
- Add a CLI with
cleanup --dry-runandcleanup --apply. - Build one plan phase that calculates:
- protected materialized match keys;
- candidate match keys;
- candidate admin-log rows by criticality and age;
- candidate server snapshots by age.
- Log the plan in structured JSON before any deletes.
- On apply:
- acquire the shared backend writer lock;
- delete
rcon_match_player_statsin batches for candidate match keys; - delete
rcon_materialized_matchesin batches; - delete
rcon_admin_log_eventsin batches; - delete
server_snapshotsin batches; - optionally run
VACUUM ANALYZEonly when explicitly requested.
- Treat missing optional tables as logged skips.
- Keep dry-run as default behavior.
Recommended SQL Audit Commands
Table-size audit
select
schemaname,
relname as table_name,
pg_size_pretty(pg_total_relation_size(relid)) as total_size,
pg_size_pretty(pg_relation_size(relid)) as table_size,
pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) as indexes_size,
n_live_tup as estimated_rows,
n_dead_tup as estimated_dead_rows
from pg_stat_user_tables
order by pg_total_relation_size(relid) desc;
AdminLog by type and age
select
event_type,
count(*) as row_count,
min(event_timestamp) as first_event_timestamp,
max(event_timestamp) as last_event_timestamp,
min(server_time) as first_server_time,
max(server_time) as last_server_time
from rcon_admin_log_events
group by event_type
order by row_count desc, event_type asc;
Materialized matches by server and closure time
select
target_key,
source_basis,
count(*) as matches,
min(coalesce(ended_at, started_at)) as first_closed_at,
max(coalesce(ended_at, started_at)) as last_closed_at
from rcon_materialized_matches
group by target_key, source_basis
order by target_key asc, source_basis asc;
Materialized player stats coverage
select
target_key,
count(*) as player_stat_rows,
count(distinct match_key) as match_count
from rcon_match_player_stats
group by target_key
order by target_key asc;
Live server snapshots by age
select
server_id,
min(captured_at) as first_captured_at,
max(captured_at) as last_captured_at,
count(*) as snapshot_rows
from server_snapshots
group by server_id
order by last_captured_at desc;
Displayed snapshot coverage
select
snapshot_type,
metric,
snapshot_window,
count(*) as snapshot_rows,
min(generated_at) as first_generated_at,
max(generated_at) as last_generated_at
from displayed_historical_snapshots
group by snapshot_type, metric, snapshot_window
order by snapshot_type asc, metric asc, snapshot_window asc;
Public-scoreboard fallback historical matches
select
hs.slug as server_slug,
count(*) as matches,
min(coalesce(hm.ended_at, hm.started_at, hm.created_at_source)) as first_match_at,
max(coalesce(hm.ended_at, hm.started_at, hm.created_at_source)) as last_match_at
from historical_matches hm
join historical_servers hs on hs.id = hm.historical_server_id
group by hs.slug
order by hs.slug asc;
Player-event ledger age
select
event_type,
count(*) as row_count,
min(occurred_at) as first_occurred_at,
max(occurred_at) as last_occurred_at
from player_event_raw_ledger
group by event_type
order by row_count desc, event_type asc;
Summary
The first cleanup command can be implemented safely if it stays narrow:
- clean
server_snapshots; - clean aged
rcon_admin_log_events; - clean non-protected
rcon_materialized_matches; - delete
rcon_match_player_statsfirst for those matches; - skip bounded snapshot caches and broader fallback domains for now.
The main correctness risk is not delete syntax. It is retention-window selection and preserving current/live dependencies. The cleanup command should therefore be plan-first, dry-run-first, and lock-protected.