Skip to content

Commit

Permalink
Merge pull request #164 from starknet-id/ayush/leaderboard-endpoints-…
Browse files Browse the repository at this point in the history
…upgrade

feat: optimise leaderboard endpoints
  • Loading branch information
Th0rgal authored Dec 28, 2023
2 parents 323ee33 + f3aa9ca commit ee2f22c
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 165 deletions.
15 changes: 13 additions & 2 deletions src/endpoints/leaderboard/get_ranking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ use mongodb::Collection;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use axum::http::{header, Response};
use chrono::Utc;

pub async fn get_user_rank(
collection: &Collection<Document>,
Expand Down Expand Up @@ -269,7 +271,7 @@ pub async fn handler(
&start_timestamp,
&end_timestamp,
)
.await;
.await;
let total_users = stats.get("total_users").unwrap().as_i32().unwrap() as i64;
let user_rank = stats.get("user_rank").unwrap().as_i32().unwrap() as i64;

Expand Down Expand Up @@ -366,7 +368,16 @@ pub async fn handler(
"first_elt_position".to_string(),
if lower_range == 0 { 1 } else { lower_range },
);
(StatusCode::OK, Json(res)).into_response()

// Set caching response
let expires = Utc::now() + chrono::Duration::minutes(5);
let caching_response = Response::builder()
.status(StatusCode::OK)
.header(header::CACHE_CONTROL, "public, max-age=300")
.header(header::EXPIRES, expires.to_rfc2822())
.body(Json(res).to_string());

return caching_response.unwrap().into_response();
}
Err(_err) => get_error("Error querying ranks".to_string()),
}
Expand Down
288 changes: 125 additions & 163 deletions src/endpoints/leaderboard/get_static_info.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/*
this endpoint will return static data of leaderboard and position of user address
Steps to get data over different time intervals :
1) iterate over one week timestamps and add total points and get top 3 and get user position
2) iterate over one month timestamps and add total points and get top 3 and get user position
3) iterate over all timestamps and add total points and get top 3 and get user position
*/
this endpoint will return static data of leaderboard and position of user address
Steps to get data over different time intervals :
1) iterate over one week timestamps and add total points and get top 3 and get user position
2) iterate over one month timestamps and add total points and get top 3 and get user position
3) iterate over all timestamps and add total points and get top 3 and get user position
*/

use std::collections::HashMap;
use crate::{models::AppState, utils::get_error};
use axum::{
extract::{Query, State},
Expand All @@ -17,194 +16,157 @@ use axum::{
use futures::TryStreamExt;
use mongodb::bson::{doc, Document};
use reqwest::StatusCode;
use std::sync::Arc;
use chrono::{Duration, Utc};
use mongodb::Collection;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use axum::http::header;
use axum::response::Response;
use chrono::Utc;

#[derive(Debug, Serialize, Deserialize)]
pub struct GetLeaderboardInfoQuery {
/*
user address
*/
addr: String,
/*
start of the timestamp range
-> How many days back you want to start the leaderboard
*/
start_timestamp: i64,

/*
end of the timestamp range
-> When do you want to end it (ideally the moment the frontend makes the request till that timestamp)
*/
end_timestamp: i64,
}

pub async fn get_leaderboard_toppers(
collection: &Collection<Document>,
days: i64,
address: &String,
) -> Document {
let time_gap = if days > 0 {
(Utc::now() - Duration::days(days)).timestamp_millis()
} else {
0
};
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<GetLeaderboardInfoQuery>,
) -> impl IntoResponse {
let addr: String = query.addr.to_string();
let collection = state.db.collection::<Document>("leaderboard_table");
let start_timestamp = query.start_timestamp;
let end_timestamp = query.end_timestamp;


let leaderboard_pipeline = vec![
doc! {
"$match": doc! {
"timestamp": doc! {
"$gte": time_gap
"$sort": doc! {
"experience": -1,
"timestamp": 1,
"_id": 1
}
}
},
},
doc! {
"$sort": doc! {
"experience": -1,
"timestamp": 1,
"_id": 1
}
},
"$match": doc! {
"timestamp": doc! {
"$gte": start_timestamp,
"$lte": end_timestamp
}
}
},
doc! {
"$facet": doc! {
"best_users": [
doc! {
"$limit": 3
},
doc! {
"$lookup": doc! {
"from": "achieved",
"localField": "_id",
"foreignField": "addr",
"as": "associatedAchievement"
}
},
doc! {
"$project": doc! {
"_id": 0,
"address": "$_id",
"xp": "$experience",
"achievements": doc! {
"$size": "$associatedAchievement"
"$facet": doc! {
"best_users": [
doc! {
"$limit": 3
},
doc! {
"$lookup": doc! {
"from": "achieved",
"localField": "_id",
"foreignField": "addr",
"as": "associatedAchievement"
}
}
}
],
"total_users": [
doc! {
"$count": "total"
}
],
"rank": [
doc! {
"$addFields": doc! {
"tempSortField": 1
}
},
doc! {
"$setWindowFields": doc! {
"sortBy": doc! {
"tempSortField": -1
},
"output": doc! {
"rank": doc! {
"$documentNumber": doc! {}
},
doc! {
"$project": doc! {
"_id": 0,
"address": "$_id",
"xp": "$experience",
"achievements": doc! {
"$size": "$associatedAchievement"
}
}
}
},
doc! {
"$match": doc! {
"_id": address
],
"total_users": [
doc! {
"$count": "total"
}
},
doc! {
"$project": doc! {
"_id": 0,
"rank": "$rank"
],
"rank": [
doc! {
"$addFields": doc! {
"tempSortField": 1
}
},
doc! {
"$setWindowFields": doc! {
"sortBy": doc! {
"tempSortField": -1
},
"output": doc! {
"rank": doc! {
"$documentNumber": doc! {}
}
}
}
},
doc! {
"$match": doc! {
"_id": addr
}
},
doc! {
"$project": doc! {
"_id": 0,
"rank": "$rank"
}
},
doc! {
"$unwind": "$rank"
}
]
}
},
doc! {
"$project": doc! {
"best_users": 1,
"total_users": doc! {
"$arrayElemAt": [
"$total_users.total",
0
]
},
doc! {
"$unwind": "$rank"
"position": doc! {
"$arrayElemAt": [
"$rank.rank",
0
]
}
]
}
},
doc! {
"$project": doc! {
"best_users": 1,
"total_users": doc! {
"$arrayElemAt": [
"$total_users.total",
0
]
},
"position": doc! {
"$arrayElemAt": [
"$rank.rank",
0
]
}
}
},
},
];


return match collection.aggregate(leaderboard_pipeline, None).await {
Ok(mut cursor) => {
let mut query_result = Vec::new();
while let Some(result) = cursor.try_next().await.unwrap() {
query_result.push(result)
}
if query_result.is_empty() {
return Document::new();
}
query_result[0].clone()
}
Err(_err) => {
Document::new()
}
};
}

pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<GetLeaderboardInfoQuery>,
) -> impl IntoResponse {
let addr: String = query.addr.to_string();
let mut error_flag = Document::new();
let users_collection = state.db.collection::<Document>("leaderboard_table");

// fetch weekly toppers and check if valid result
let weekly_toppers_result = get_leaderboard_toppers(&users_collection, 7, &addr).await;
let weekly_toppers = match weekly_toppers_result.is_empty() {
true => {
error_flag.insert("status", true);
error_flag.clone()
}
false => weekly_toppers_result.clone(),
};
// Set caching response
let expires = Utc::now() + chrono::Duration::minutes(5);
let caching_response = Response::builder()
.status(StatusCode::OK)
.header(header::CACHE_CONTROL, "public, max-age=300")
.header(header::EXPIRES, expires.to_rfc2822())
.body(Json(result).to_string());

// fetch monthly toppers and check if valid result
let monthly_toppers_result = get_leaderboard_toppers(&users_collection, 30, &addr).await;
let monthly_toppers = match monthly_toppers_result.is_empty() {
true => {
error_flag.insert("status", true);
error_flag.clone()
}
false => monthly_toppers_result.clone(),
};

// fetch all time toppers and check if valid result
let all_time_toppers_result = get_leaderboard_toppers(&users_collection, -1, &addr).await;
let all_time_toppers = match all_time_toppers_result.is_empty() {
true => {
error_flag.insert("status", true);
error_flag.clone()
return caching_response.unwrap().into_response();
}
get_error("Error querying ranks".to_string())
}
false => all_time_toppers_result.clone(),
Err(_err) => get_error("Error querying ranks".to_string()),
};


// check if any error occurred
if error_flag.contains_key("status") {
return get_error("Error querying leaderboard".to_string());
}

let mut res: HashMap<String, Document> = HashMap::new();
res.insert("weekly".to_string(), weekly_toppers);
res.insert("monthly".to_string(), monthly_toppers);
res.insert("all_time".to_string(), all_time_toppers);
(StatusCode::OK, Json(res)).into_response()
}

0 comments on commit ee2f22c

Please sign in to comment.