Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

perf: caching on statistics endpoint #949

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions src/routes/v2/statistics.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::routes::{
v2_reroute,
v3::{self, statistics::V3Stats},
ApiError,
use crate::{
database::redis::RedisPool,
routes::{
v2_reroute,
v3::{self, statistics::V3Stats},
ApiError,
},
};
use actix_web::{get, web, HttpResponse};
use sqlx::PgPool;
Expand All @@ -12,15 +15,18 @@ pub fn config(cfg: &mut web::ServiceConfig) {

#[derive(serde::Serialize)]
pub struct V2Stats {
pub projects: Option<i64>,
pub versions: Option<i64>,
pub authors: Option<i64>,
pub files: Option<i64>,
pub projects: i64,
pub versions: i64,
pub authors: i64,
pub files: i64,
}

#[get("statistics")]
pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let response = v3::statistics::get_stats(pool)
pub async fn get_stats(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let response = v3::statistics::get_stats(pool, redis)
.await
.or_else(v2_reroute::flatten_404_error)?;

Expand Down
210 changes: 149 additions & 61 deletions src/routes/v3/statistics.rs
Original file line number Diff line number Diff line change
@@ -1,91 +1,179 @@
use crate::routes::ApiError;
use crate::{database::redis::RedisPool, routes::ApiError};
use actix_web::{web, HttpResponse};
use sqlx::PgPool;

const STATISTICS_NAMESPACE: &str = "statistics";
const STATISTICS_EXPIRY: i64 = 60 * 30; // 30 minutes

pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("statistics", web::get().to(get_stats));
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct V3Stats {
pub projects: Option<i64>,
pub versions: Option<i64>,
pub authors: Option<i64>,
pub files: Option<i64>,
pub projects: i64,
pub versions: i64,
pub authors: i64,
pub files: i64,
}

pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let projects = sqlx::query!(
"
SELECT COUNT(id)
FROM mods
WHERE status = ANY($1)
",
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?;

let versions = sqlx::query!(
"
pub async fn get_stats(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let mut redis = redis.connect().await?;

let projects = if let Some(project_count) = redis
.get_deserialized_from_json::<i64>(STATISTICS_NAMESPACE, "projects")
.await?
{
project_count
} else {
let count = sqlx::query!(
"
SELECT COUNT(id)
FROM mods
WHERE status = ANY($1)
",
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?
.count
.unwrap();

redis
.set_serialized_to_json(
STATISTICS_NAMESPACE,
"projects",
count,
Some(STATISTICS_EXPIRY),
)
.await?;

count
};

let versions = if let Some(version_count) = redis
.get_deserialized_from_json::<i64>(STATISTICS_NAMESPACE, "versions")
.await?
{
version_count
} else {
let count = sqlx::query!(
"
SELECT COUNT(v.id)
FROM versions v
INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1)
WHERE v.status = ANY($2)
",
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?;

let authors = sqlx::query!(
"
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?
.count
.unwrap();

redis
.set_serialized_to_json(
STATISTICS_NAMESPACE,
"versions",
count,
Some(STATISTICS_EXPIRY),
)
.await?;

count
};

let authors = if let Some(author_count) = redis
.get_deserialized_from_json::<i64>(STATISTICS_NAMESPACE, "authors")
.await?
{
author_count
} else {
let count = sqlx::query!(
"
SELECT COUNT(DISTINCT u.id)
FROM users u
INNER JOIN team_members tm on u.id = tm.user_id AND tm.accepted = TRUE
INNER JOIN mods m on tm.team_id = m.team_id AND m.status = ANY($1)
",
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?;

let files = sqlx::query!(
"
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?
.count
.unwrap();

redis
.set_serialized_to_json(
STATISTICS_NAMESPACE,
"authors",
count,
Some(STATISTICS_EXPIRY),
)
.await?;

count
};

let files = if let Some(file_count) = redis
.get_deserialized_from_json::<i64>(STATISTICS_NAMESPACE, "files")
.await?
{
file_count
} else {
let count = sqlx::query!(
"
SELECT COUNT(f.id) FROM files f
INNER JOIN versions v on f.version_id = v.id AND v.status = ANY($2)
INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1)
",
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?;
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_one(&**pool)
.await?
.count
.unwrap();

redis
.set_serialized_to_json(
STATISTICS_NAMESPACE,
"files",
count,
Some(STATISTICS_EXPIRY),
)
.await?;

count
};

let v3_stats = V3Stats {
projects: projects.count,
versions: versions.count,
authors: authors.count,
files: files.count,
projects,
versions,
authors,
files,
};

Ok(HttpResponse::Ok().json(v3_stats))
Expand Down