diff --git a/rhombus/migrations/libsql/0001_setup.up.sql b/rhombus/migrations/libsql/0001_setup.up.sql index 9b90d7a..9c9d91d 100644 --- a/rhombus/migrations/libsql/0001_setup.up.sql +++ b/rhombus/migrations/libsql/0001_setup.up.sql @@ -194,12 +194,6 @@ FROM rhombus_solve JOIN rhombus_team ON rhombus_solve.team_id = rhombus_team.id GROUP BY rhombus_solve.challenge_id, rhombus_team.division_id; --- CREATE VIEW IF NOT EXISTS rhombus_team_points AS --- SELECT rhombus_solve.team_id, SUM(COALESCE(rhombus_solve.points, rhombus_challenge.points)) AS points, MAX(rhombus_solve.solved_at) AS last_solved_at --- FROM rhombus_solve --- JOIN rhombus_challenge ON rhombus_solve.challenge_id = rhombus_challenge.id --- GROUP BY rhombus_solve.team_id; - CREATE TABLE IF NOT EXISTS rhombus_track ( id INTEGER PRIMARY KEY NOT NULL, ip BLOB NOT NULL, diff --git a/rhombus/src/internal/database/cache.rs b/rhombus/src/internal/database/cache.rs index 9d659d4..c3fce51 100644 --- a/rhombus/src/internal/database/cache.rs +++ b/rhombus/src/internal/database/cache.rs @@ -443,8 +443,8 @@ impl Database for DbCache { get_scoreboard(&self.inner, division_id).await } - async fn get_leaderboard(&self, division_id: &str, page: Option) -> Result { - get_leaderboard(&self.inner, division_id, page).await + async fn get_leaderboard(&self, division_id: &str) -> Result { + get_leaderboard(&self.inner, division_id).await } async fn get_top10_discord_ids(&self) -> Result> { @@ -699,23 +699,18 @@ pub async fn get_scoreboard(db: &Connection, division_id: &str) -> Result), Leaderboard>> = - LazyLock::new(DashMap::new); +pub static LEADERBOARD_CACHE: LazyLock> = LazyLock::new(DashMap::new); -pub async fn get_leaderboard( - db: &Connection, - division_id: &str, - page: Option, -) -> Result { - if let Some(leaderboard) = LEADERBOARD_CACHE.get(&(division_id.to_owned(), page)) { +pub async fn get_leaderboard(db: &Connection, division_id: &str) -> Result { + if let Some(leaderboard) = LEADERBOARD_CACHE.get(division_id) { return Ok(leaderboard.clone()); } - tracing::trace!(division_id, page, "cache miss: get_leaderboard"); + tracing::trace!(division_id, "cache miss: get_leaderboard"); - let leaderboard = db.get_leaderboard(division_id, page).await; + let leaderboard = db.get_leaderboard(division_id).await; if let Ok(leaderboard) = &leaderboard { - LEADERBOARD_CACHE.insert((division_id.to_owned(), page), leaderboard.clone()); + LEADERBOARD_CACHE.insert(division_id.to_owned(), leaderboard.clone()); } leaderboard } diff --git a/rhombus/src/internal/database/libsql.rs b/rhombus/src/internal/database/libsql.rs index fe3e95f..5cf10b4 100644 --- a/rhombus/src/internal/database/libsql.rs +++ b/rhombus/src/internal/database/libsql.rs @@ -28,10 +28,10 @@ use crate::{ provider::{ Author, Category, Challenge, ChallengeAttachment, ChallengeData, ChallengeDivision, ChallengeSolve, Challenges, Database, DiscordUpsertError, Email, Leaderboard, - LeaderboardEntry, Scoreboard, ScoreboardSeriesPoint, ScoreboardTeam, - SetAccountNameError, SetTeamNameError, SiteStatistics, StatisticsCategory, Team, - TeamInner, TeamMeta, TeamMetaInner, TeamStanding, TeamUser, Ticket, - ToBeClosedTicket, UserTrack, Writeup, + LeaderboardEntry, Scoreboard, ScoreboardInner, ScoreboardSeriesPoint, + ScoreboardTeam, SetAccountNameError, SetTeamNameError, SiteStatistics, + StatisticsCategory, Team, TeamInner, TeamMeta, TeamMetaInner, TeamStanding, + TeamUser, Ticket, ToBeClosedTicket, UserTrack, Writeup, }, }, division::Division, @@ -1902,10 +1902,10 @@ impl Database for T { tx.commit().await?; - Ok(Scoreboard { teams }) + Ok(Arc::new(ScoreboardInner::new(teams))) } - async fn get_leaderboard(&self, division_id: &str, page: Option) -> Result { + async fn get_leaderboard(&self, division_id: &str) -> Result { #[derive(Debug, Deserialize)] struct DbLeaderboard { team_id: i64, @@ -1913,100 +1913,36 @@ impl Database for T { points: f64, } - if let Some(page) = page { - let tx = self.transaction().await?; + let mut rank = 0; - let num_teams = tx - .query( - " - SELECT COUNT(*) - FROM rhombus_team - WHERE rhombus_team.division_id = ?1 - ", - [division_id], - ) - .await? - .next() - .await? - .unwrap() - .get::(0) - .unwrap(); - - const PAGE_SIZE: u64 = 25; - - let num_pages = (num_teams + (PAGE_SIZE - 1)) / PAGE_SIZE; - - let page = page.min(num_pages); - - let mut rank = page * PAGE_SIZE; - - let leaderboard_entries = tx - .query( - " - SELECT id AS team_id, name, points - FROM rhombus_team - WHERE division_id = ?1 - ORDER BY points DESC, last_solved_at ASC - LIMIT ?3 OFFSET ?2 - ", - params!(division_id, page * PAGE_SIZE, PAGE_SIZE), - ) - .await? - .into_stream() - .map(|row| { - let db_leaderboard = de::from_row::(&row.unwrap()).unwrap(); - rank += 1; - LeaderboardEntry { - rank, - team_id: db_leaderboard.team_id, - team_name: db_leaderboard.name, - score: db_leaderboard.points.round() as i64, - } - }) - .collect::>() - .await; - - tx.commit().await?; - - Ok(Leaderboard { - entries: leaderboard_entries, - num_pages, + let leaderboard_entries = self + .connect() + .await? + .query( + " + SELECT id AS team_id, name, points + FROM rhombus_team + WHERE division_id = ?1 + ORDER BY points DESC, last_solved_at ASC + ", + params!(division_id), + ) + .await? + .into_stream() + .map(|row| { + let db_leaderboard = de::from_row::(&row.unwrap()).unwrap(); + rank += 1; + LeaderboardEntry { + rank, + team_id: db_leaderboard.team_id, + team_name: db_leaderboard.name, + score: db_leaderboard.points.round() as i64, + } }) - } else { - let mut rank = 0; - - let leaderboard_entries = self - .connect() - .await? - .query( - " - SELECT id AS team_id, name, points - FROM rhombus_team - WHERE division_id = ?1 - ORDER BY points DESC, last_solved_at ASC - ", - params!(division_id), - ) - .await? - .into_stream() - .map(|row| { - let db_leaderboard = de::from_row::(&row.unwrap()).unwrap(); - rank += 1; - LeaderboardEntry { - rank, - team_id: db_leaderboard.team_id, - team_name: db_leaderboard.name, - score: db_leaderboard.points.round() as i64, - } - }) - .collect::>() - .await; + .collect::>() + .await; - Ok(Leaderboard { - entries: leaderboard_entries, - num_pages: 1, - }) - } + Ok(Arc::new(leaderboard_entries)) } async fn get_top10_discord_ids(&self) -> Result> { diff --git a/rhombus/src/internal/database/postgres.rs b/rhombus/src/internal/database/postgres.rs index e698dd2..51257d2 100644 --- a/rhombus/src/internal/database/postgres.rs +++ b/rhombus/src/internal/database/postgres.rs @@ -323,7 +323,7 @@ impl Database for Postgres { todo!() } - async fn get_leaderboard(&self, _division_id: &str, _page: Option) -> Result { + async fn get_leaderboard(&self, _division_id: &str) -> Result { todo!() } diff --git a/rhombus/src/internal/database/provider.rs b/rhombus/src/internal/database/provider.rs index dfc3163..9a92183 100644 --- a/rhombus/src/internal/database/provider.rs +++ b/rhombus/src/internal/database/provider.rs @@ -132,10 +132,20 @@ pub struct ScoreboardTeam { } #[derive(Debug, Serialize, Clone)] -pub struct Scoreboard { +pub struct ScoreboardInner { pub teams: BTreeMap, + pub cached_json: String, } +impl ScoreboardInner { + pub fn new(teams: BTreeMap) -> Self { + let cached_json = serde_json::to_string(&teams).unwrap(); + Self { teams, cached_json } + } +} + +pub type Scoreboard = Arc; + #[derive(Debug, Serialize, Clone)] pub struct LeaderboardEntry { pub team_id: i64, @@ -144,11 +154,7 @@ pub struct LeaderboardEntry { pub rank: u64, } -#[derive(Debug, Serialize, Clone)] -pub struct Leaderboard { - pub num_pages: u64, - pub entries: Vec, -} +pub type Leaderboard = Arc>; #[derive(Debug, Serialize, Clone)] pub struct Email { @@ -343,7 +349,7 @@ pub trait Database { async fn save_settings(&self, settings: &Settings) -> Result<()>; async fn load_settings(&self, settings: &mut Settings) -> Result<()>; async fn get_scoreboard(&self, division_id: &str) -> Result; - async fn get_leaderboard(&self, division_id: &str, page: Option) -> Result; + async fn get_leaderboard(&self, division_id: &str) -> Result; async fn get_top10_discord_ids(&self) -> Result>; async fn get_emails_for_user_id(&self, user_id: i64) -> Result>; async fn get_team_tracks(&self, team_id: i64) -> Result>; diff --git a/rhombus/src/internal/open_graph.rs b/rhombus/src/internal/open_graph.rs index e1df4ab..c15ab4c 100644 --- a/rhombus/src/internal/open_graph.rs +++ b/rhombus/src/internal/open_graph.rs @@ -190,12 +190,8 @@ pub async fn route_default_og_image(state: State) -> impl IntoRespo let mut division_meta = Vec::with_capacity(state.divisions.len()); for division in state.divisions.iter() { let mut places = Vec::with_capacity(3); - let leaderboard = state - .db - .get_leaderboard(&division.id, Some(0)) - .await - .unwrap(); - leaderboard.entries.iter().take(3).for_each(|entry| { + let leaderboard = state.db.get_leaderboard(&division.id).await.unwrap(); + leaderboard.iter().take(3).for_each(|entry| { places.push(TeamMeta { name: entry.team_name.clone(), score: entry.score as u64, diff --git a/rhombus/src/internal/routes/scoreboard.rs b/rhombus/src/internal/routes/scoreboard.rs index 0e2e7e2..7b87f52 100644 --- a/rhombus/src/internal/routes/scoreboard.rs +++ b/rhombus/src/internal/routes/scoreboard.rs @@ -60,20 +60,32 @@ pub async fn route_scoreboard_division( params: Query, uri: Uri, ) -> impl IntoResponse { - let page_num = params.page.unwrap_or(1).saturating_sub(1); + let page_num = params.page.unwrap_or(1).saturating_sub(1) as usize; let division_id = division_id.strip_suffix(".json").unwrap_or(&division_id); let scoreboard = state.db.get_scoreboard(division_id); let challenge_data = state.db.get_challenges(); - let leaderboard = state.db.get_leaderboard(division_id, Some(page_num)); + let leaderboard = state.db.get_leaderboard(division_id); let (scoreboard, challenge_data, leaderboard) = futures::future::try_join3(scoreboard, challenge_data, leaderboard) .await .unwrap(); + const PAGE_SIZE: usize = 25; + let num_pages = (leaderboard.len() + (PAGE_SIZE - 1)) / PAGE_SIZE; + let page_num = page_num.min(num_pages); + + let start = std::cmp::min(leaderboard.len(), page_num * PAGE_SIZE); + let end = std::cmp::min(leaderboard.len(), start + PAGE_SIZE); + let leaderboard = &leaderboard[start..end]; + if uri.path().ends_with(".json") { - return Json(scoreboard.teams).into_response(); + return ( + [("Content-Type", "application/json")], + scoreboard.cached_json.clone(), + ) + .into_response(); } Html( @@ -92,6 +104,7 @@ pub async fn route_scoreboard_division( leaderboard, selected_division_id => division_id, page_num, + num_pages, }) .unwrap(), ) @@ -104,7 +117,7 @@ pub async fn route_scoreboard_division_ctftime( Path(division_id): Path, ) -> impl IntoResponse { let challenge_data = state.db.get_challenges().await.unwrap(); - let leaderboard = state.db.get_leaderboard(&division_id, None).await.unwrap(); + let leaderboard = state.db.get_leaderboard(&division_id).await.unwrap(); let tasks = challenge_data .challenges @@ -113,7 +126,6 @@ pub async fn route_scoreboard_division_ctftime( .collect::>(); let standings = leaderboard - .entries .iter() .map(|team| { json!({ diff --git a/rhombus/templates/scoreboard.html b/rhombus/templates/scoreboard.html index 42d0e59..2b9500d 100644 --- a/rhombus/templates/scoreboard.html +++ b/rhombus/templates/scoreboard.html @@ -93,10 +93,10 @@ {% endcall %} {% endcall %} {% call card.content() %} - {% if leaderboard.entries | length > 0 %} + {% if leaderboard | length > 0 %}
- {% for entry in leaderboard.entries %} + {% for entry in leaderboard %}
{{ entry.rank }} @@ -114,10 +114,10 @@ {% endfor %}
- {% if leaderboard.num_pages > 1 %} + {% if num_pages > 1 %}
- {% for i in range(leaderboard.num_pages) %} + {% for i in range(num_pages) %} {% if i == 0 and page_num != 0 %}