ai generated solutions to our ai generated problems

This commit is contained in:
Heidi
2026-06-15 08:11:16 +01:00
parent 760e49f401
commit c94a09f46c
2 changed files with 107 additions and 29 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ axum = "0.8"
rusqlite = { version = "0.37", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time"] }
tower-http = { version = "0.6", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+94 -16
View File
@@ -13,7 +13,8 @@ use std::{
env, fs,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::{Path as FsPath, PathBuf},
sync::Arc,
sync::{Arc, Mutex},
time::Duration,
};
use tokio::net::TcpListener;
use tower_http::{
@@ -22,11 +23,16 @@ use tower_http::{
};
const MAX_TEAM_NAME_LENGTH: usize = 80;
const LEADERBOARD_CACHE_TTL: Duration = Duration::from_secs(300);
#[derive(Clone)]
struct AppState {
battles_db: PathBuf,
teams_db: PathBuf,
leaderboard_cache: Mutex<Option<CachedLeaderboard>>,
}
struct CachedLeaderboard {
teams: Vec<TeamLeaderboardRow>,
}
#[derive(Debug)]
@@ -92,7 +98,7 @@ struct HealthResponse {
databases: BTreeMap<&'static str, bool>,
}
#[derive(Serialize)]
#[derive(Clone, Serialize)]
struct LeaderboardResponse {
teams: Vec<TeamLeaderboardRow>,
}
@@ -120,7 +126,7 @@ struct TeamSearchRow {
members: i64,
}
#[derive(Serialize)]
#[derive(Clone, Serialize)]
struct TeamLeaderboardRow {
team_id: i64,
name: String,
@@ -288,7 +294,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let state = Arc::new(AppState {
battles_db: resolve_db_path("TSS_BATTLES_DB", "tss_battles.db"),
teams_db: resolve_db_path("TSS_TEAMS_DB", "tss_teams.db"),
leaderboard_cache: Mutex::new(None),
});
spawn_leaderboard_refresh(state.clone());
let app = Router::new()
.route("/health", get(health))
@@ -373,7 +381,17 @@ async fn leaderboard(
State(state): State<Arc<AppState>>,
Query(query): Query<LimitQuery>,
) -> ApiResult<LeaderboardResponse> {
let limit = i64::from(query.limit.unwrap_or(100).clamp(1, 100));
let limit = usize::try_from(query.limit.unwrap_or(100).clamp(1, 100)).unwrap_or(100);
if let Some(teams) = cached_leaderboard(&state, limit)? {
return Ok(Json(LeaderboardResponse { teams }));
}
let rows = leaderboard_rows(&state, limit)?;
cache_leaderboard(&state, &rows)?;
Ok(Json(LeaderboardResponse { teams: rows }))
}
fn leaderboard_rows(state: &AppState, limit: usize) -> Result<Vec<TeamLeaderboardRow>, ApiError> {
let teams_conn = open_db(&state.teams_db)?;
let battles_conn = open_db(&state.battles_db)?;
// Deduplicate teams by name across tournaments — pick the highest team_id
@@ -389,11 +407,15 @@ async fn leaderboard(
.map_err(db_error)?;
let teams = stmt
.query_map(params![limit], |row| read_team_record(row))
.query_map(params![limit as i64], |row| read_team_record(row))
.map_err(db_error)?
.collect::<Result<Vec<_>, _>>()
.map_err(db_error)?;
let mut summaries = team_summaries_for(&battles_conn)?;
let team_names = teams
.iter()
.map(|team| team.name.as_str())
.collect::<Vec<_>>();
let mut summaries = team_summaries_for(&battles_conn, &team_names)?;
let mut rows = Vec::with_capacity(teams.len());
for team in teams {
@@ -412,7 +434,29 @@ async fn leaderboard(
});
}
Ok(Json(LeaderboardResponse { teams: rows }))
Ok(rows)
}
fn spawn_leaderboard_refresh(state: Arc<AppState>) {
tokio::spawn(async move {
loop {
let refresh_state = state.clone();
match tokio::task::spawn_blocking(move || leaderboard_rows(&refresh_state, 100)).await {
Ok(Ok(rows)) => {
if let Err(error) = cache_leaderboard(&state, &rows) {
tracing::warn!("could not cache leaderboard refresh: {}", error.message);
}
}
Ok(Err(error)) => {
tracing::warn!("could not refresh leaderboard cache: {}", error.message);
}
Err(error) => {
tracing::warn!("leaderboard refresh task failed: {}", error);
}
}
tokio::time::sleep(LEADERBOARD_CACHE_TTL).await;
}
});
}
async fn recent_games(
@@ -655,9 +699,19 @@ fn team_summary_for(conn: &Connection, team_name: &str) -> Result<TeamSummary, A
.map_err(db_error)
}
fn team_summaries_for(conn: &Connection) -> Result<HashMap<String, TeamSummary>, ApiError> {
let mut stmt = conn
.prepare(
fn team_summaries_for(
conn: &Connection,
team_names: &[&str],
) -> Result<HashMap<String, TeamSummary>, ApiError> {
if team_names.is_empty() {
return Ok(HashMap::new());
}
let placeholders = std::iter::repeat("?")
.take(team_names.len())
.collect::<Vec<_>>()
.join(", ");
let sql = format!(
"SELECT
team_name,
COUNT(DISTINCT session_id),
@@ -670,13 +724,13 @@ fn team_summaries_for(conn: &Connection) -> Result<HashMap<String, TeamSummary>,
COALESCE(SUM(score), 0),
COUNT(DISTINCT UID)
FROM player_games_hist
WHERE team_name IS NOT NULL AND team_name != ''
GROUP BY team_name COLLATE NOCASE",
)
.map_err(db_error)?;
WHERE team_name COLLATE NOCASE IN ({placeholders})
GROUP BY team_name COLLATE NOCASE"
);
let mut stmt = conn.prepare(&sql).map_err(db_error)?;
let summaries = stmt
.query_map([], |row| {
.query_map(rusqlite::params_from_iter(team_names.iter()), |row| {
let name: String = row.get(0)?;
let battles: i64 = row.get(1)?;
let wins: i64 = row.get(2)?;
@@ -709,6 +763,30 @@ fn team_summaries_for(conn: &Connection) -> Result<HashMap<String, TeamSummary>,
Ok(summaries)
}
fn cached_leaderboard(
state: &AppState,
limit: usize,
) -> Result<Option<Vec<TeamLeaderboardRow>>, ApiError> {
let cache = state
.leaderboard_cache
.lock()
.map_err(|_| ApiError::internal("Leaderboard cache lock failed"))?;
Ok(cache.as_ref().and_then(|cached| {
(cached.teams.len() >= limit).then(|| cached.teams.iter().take(limit).cloned().collect())
}))
}
fn cache_leaderboard(state: &AppState, teams: &[TeamLeaderboardRow]) -> Result<(), ApiError> {
let mut cache = state
.leaderboard_cache
.lock()
.map_err(|_| ApiError::internal("Leaderboard cache lock failed"))?;
*cache = Some(CachedLeaderboard {
teams: teams.to_vec(),
});
Ok(())
}
fn player_summaries_for(
teams_conn: &Connection,
battles_conn: &Connection,