Files
AstralRinth/src/database/models/collection_item.rs
Geometrically a0aa350a08 Fix cache stampede issues + generalize cache (#884)
* caching changes

* fix cache stampede issues

* Use pub/sub for better DB fetches

* remove pubsub

* remove debugs

* Fix caches not working

* fix search indexing removal
2024-03-26 21:15:50 -07:00

218 lines
6.6 KiB
Rust

use super::ids::*;
use crate::database::models;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::collections::CollectionStatus;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use futures::TryStreamExt;
use serde::{Deserialize, Serialize};
const COLLECTIONS_NAMESPACE: &str = "collections";
#[derive(Clone)]
pub struct CollectionBuilder {
pub collection_id: CollectionId,
pub user_id: UserId,
pub name: String,
pub description: Option<String>,
pub status: CollectionStatus,
pub projects: Vec<ProjectId>,
}
impl CollectionBuilder {
pub async fn insert(
self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<CollectionId, DatabaseError> {
let collection_struct = Collection {
id: self.collection_id,
name: self.name,
user_id: self.user_id,
description: self.description,
created: Utc::now(),
updated: Utc::now(),
icon_url: None,
color: None,
status: self.status,
projects: self.projects,
};
collection_struct.insert(transaction).await?;
Ok(self.collection_id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Collection {
pub id: CollectionId,
pub user_id: UserId,
pub name: String,
pub description: Option<String>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub icon_url: Option<String>,
pub color: Option<u32>,
pub status: CollectionStatus,
pub projects: Vec<ProjectId>,
}
impl Collection {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO collections (
id, user_id, name, description,
created, icon_url, status
)
VALUES (
$1, $2, $3, $4,
$5, $6, $7
)
",
self.id as CollectionId,
self.user_id as UserId,
&self.name,
self.description.as_ref(),
self.created,
self.icon_url.as_ref(),
self.status.to_string(),
)
.execute(&mut **transaction)
.await?;
let (collection_ids, project_ids): (Vec<_>, Vec<_>) =
self.projects.iter().map(|p| (self.id.0, p.0)).unzip();
sqlx::query!(
"
INSERT INTO collections_mods (collection_id, mod_id)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[])
ON CONFLICT DO NOTHING
",
&collection_ids[..],
&project_ids[..],
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn remove(
id: CollectionId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
let collection = Self::get(id, &mut **transaction, redis).await?;
if let Some(collection) = collection {
sqlx::query!(
"
DELETE FROM collections_mods
WHERE collection_id = $1
",
id as CollectionId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM collections
WHERE id = $1
",
id as CollectionId,
)
.execute(&mut **transaction)
.await?;
models::Collection::clear_cache(collection.id, redis).await?;
Ok(Some(()))
} else {
Ok(None)
}
}
pub async fn get<'a, 'b, E>(
id: CollectionId,
executor: E,
redis: &RedisPool,
) -> Result<Option<Collection>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Collection::get_many(&[id], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
collection_ids: &[CollectionId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<Collection>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let val = redis
.get_cached_keys(
COLLECTIONS_NAMESPACE,
&collection_ids.iter().map(|x| x.0).collect::<Vec<_>>(),
|collection_ids| async move {
let collections = sqlx::query!(
"
SELECT c.id id, c.name name, c.description description,
c.icon_url icon_url, c.color color, c.created created, c.user_id user_id,
c.updated updated, c.status status,
ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods
FROM collections c
LEFT JOIN collections_mods cm ON cm.collection_id = c.id
WHERE c.id = ANY($1)
GROUP BY c.id;
",
&collection_ids,
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, m| {
let collection = Collection {
id: CollectionId(m.id),
user_id: UserId(m.user_id),
name: m.name.clone(),
description: m.description.clone(),
icon_url: m.icon_url.clone(),
color: m.color.map(|x| x as u32),
created: m.created,
updated: m.updated,
status: CollectionStatus::from_string(&m.status),
projects: m
.mods
.unwrap_or_default()
.into_iter()
.map(ProjectId)
.collect(),
};
acc.insert(m.id, collection);
async move { Ok(acc) }
})
.await?;
Ok(collections)
},
)
.await?;
Ok(val)
}
pub async fn clear_cache(id: CollectionId, redis: &RedisPool) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis.delete(COLLECTIONS_NAMESPACE, id.0).await?;
Ok(())
}
}