perf: Attribution route perf improvements (#6489)

* Attribution route perf improvements

* fix clippy

* clear exemptions when user attempts to scan their file

* Increase Redis TTL/refresh time
This commit is contained in:
aecsocket
2026-06-24 17:48:35 +02:00
committed by GitHub
parent 29110890e1
commit c392c85917
19 changed files with 333 additions and 127 deletions
+2 -2
View File
@@ -50,7 +50,7 @@ Run these from the **root** folder before opening a pull request - do not run th
- **App frontend:** `pnpm prepr:frontend:app`
- **Frontend libs:** `pnpm prepr:frontend:lib`
- **All frontend (app+web):** `pnpm prepr`
- **Labrinth (backend):** See `apps/labrinth/CLAUDE.md`
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
The website and app `prepr` commands
@@ -62,7 +62,7 @@ The website and app `prepr` commands
## Project-Specific Instructions
Each project may have its own `CLAUDE.md` with detailed instructions:
Each project may have its own file with detailed instructions:
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tinsert into file_scans (file_id)\n\t\tselect f.id\n\t\tfrom files f\n\t\twhere f.version_id = any($1)\n\t\ton conflict (file_id) do nothing\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": []
},
"hash": "1206e354ee5be83bca26b802b1e7a7ebdf9ee4ae9a142ee93e89ddc1536e7284"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tselect distinct f.version_id as \"version_id: DBVersionId\"\n\t\tfrom override_file_sources ofs\n\t\tinner join files f on f.id = ofs.file_id\n\t\tinner join versions v on v.id = f.version_id\n\t\twhere ofs.sha1 = $1\n\t\t\tand v.mod_id = $2\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version_id: DBVersionId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Bytea",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "365d51d5c56e9420618bd23ec2f0624cb261a1aeb1f929277fa063d730c2d99a"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tinsert into file_scans (file_id)\n\t\tselect f.id\n\t\tfrom files f\n\t\tinner join attribution_enforced_versions aev on aev.id = f.version_id\n\t\twhere f.version_id = any($1)\n\t\ton conflict (file_id) do nothing\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": []
},
"hash": "43ce7cbf44ce123c3d71c5dee035022d2e6b537eb53fcb39608e820e77174b81"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tdelete from attributions_exemptions\n\t\twhere version_id = any($1) or project_id = any($2)\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8Array",
"Int8Array"
]
},
"nullable": []
},
"hash": "70d1e6c703a2124e96e60a3fabe90b0b2dcb0c8631fdf5da57b3bc8047acfef2"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n select version_id as \"version_id: DBVersionId\"\n from files\n where id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version_id: DBVersionId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "752eb66451b57e9595b8a1671b1f98465ddb903a57d566de9e472b0b78ad2d39"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tselect distinct f.version_id as \"version_id: DBVersionId\"\n\t\tfrom project_attribution_files paf\n\t\tinner join project_attribution_groups pag on pag.id = paf.group_id\n\t\tinner join override_file_sources ofs on ofs.sha1 = paf.sha1\n\t\tinner join files f on f.id = ofs.file_id\n\t\tinner join versions v on v.id = f.version_id\n\t\twhere paf.group_id = any($1)\n\t\t\tand pag.project_id = v.mod_id\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version_id: DBVersionId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
false
]
},
"hash": "a68a8b6e1fb06fac7021657120e8446067fcc40dc1cf7f75670fdf2486b19f41"
}
+2 -2
View File
@@ -29,6 +29,6 @@
- You can force a search reindex by:
- Running `cd apps/labrinth && cargo run -p labrinth -- --run-background-task index-search` (prefer this if backend is running locally)
- Hitting the force reindex admin endpoint
- To seed the database locally: `psql postgresql://labrinth:labrinth@localhost/labrinth -f apps/labrinth/fixtures/labrinth-seed-data-202508052143.sql
`
- To seed the database locally: `psql postgresql://labrinth:labrinth@localhost/labrinth -f apps/labrinth/fixtures/labrinth-seed-data-202508052143.sql`
- When writing `sqlx` queries, prefer `r#` raw strings over escaping quotes
- When interacting with the Postgres database, prefer using a `ro_pool: ReadOnlyPgPool` when performing read-only operations
-34
View File
@@ -1,34 +0,0 @@
# Labrinth
Labrinth is the backend API service for Modrinth, written in Rust.
## Code style
- When writing `sqlx` queries, NEVER use `query` directly. Always prefer using the `query!`, `query_as!`, `query_scalar!` macros.
## Pre-PR Checks
When the user refers to "perform[ing] pre-PR checks", do the following:
- Run `cargo clippy -p labrinth --all-targets` — there must be ZERO warnings, otherwise CI will fail
- DO NOT run tests unless explicitly requested (they take a long time)
- Prepare the sqlx cache: cd into `apps/labrinth` and run `cargo sqlx prepare -- --tests`
- NEVER run `cargo sqlx prepare --workspace`
## Testing
- Run `cargo test -p labrinth --all-targets` to test your changes — all tests must pass
## Local Services
- Read the root `docker-compose.yml` to see what running services are available while developing
- Use `docker exec` to access these services
### Clickhouse
- Access: `docker exec labrinth-clickhouse clickhouse-client`
- Database: `staging_ariadne`
### Postgres
- Access: `docker exec labrinth-postgres psql -U labrinth -d labrinth -c "<query>"`
+2 -27
View File
@@ -10,34 +10,11 @@ use crate::models::projects::{
MissingAttributionFile, OverrideSource, Version,
};
use crate::models::users::User;
use crate::queue::file_scan::{
get_dependency_attributions, get_files_missing_attribution,
};
use crate::queue::file_scan::get_files_missing_attribution;
use crate::routes::ApiError;
use futures::TryStreamExt;
use itertools::Itertools;
pub async fn enrich_dependency_attributions(
versions: &mut [VersionQueryResult],
pool: &ReadOnlyPgPool,
) {
let version_ids = versions.iter().map(|v| v.inner.id).collect::<Vec<_>>();
let dep_attr = get_dependency_attributions(&**pool, &version_ids)
.await
.unwrap_or_default();
for version in versions {
for dep in &mut version.dependencies {
if let Some(attr) = dep_attr.get(&dep.id)
&& (attr.attribution.flame_project.is_some()
|| attr.attribution.resolution.is_some())
{
dep.attribution = Some(attr.attribution.clone());
}
}
}
}
pub trait ValidateAuthorized {
fn validate_authorized(
&self,
@@ -235,12 +212,10 @@ pub async fn filter_visible_versions(
versions.retain(|x| filtered_version_ids.contains(&x.inner.id));
let version_ids: Vec<_> = versions.iter().map(|v| v.inner.id).collect();
let missing = get_files_missing_attribution(pool, &version_ids)
let missing = get_files_missing_attribution(&**ro_pool, &version_ids)
.await
.unwrap_or_default();
enrich_dependency_attributions(&mut versions, ro_pool).await;
Ok(versions
.into_iter()
.map(|v| {
@@ -808,6 +808,18 @@ impl DBVersion {
}
).await?;
let dependency_attributions =
crate::queue::file_scan::get_dependency_attributions(
&mut exec,
&version_ids
.iter()
.copied()
.map(DBVersionId)
.collect_vec(),
)
.await
.unwrap_or_default();
let res = sqlx::query!(
r#"
SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,
@@ -832,6 +844,19 @@ impl DBVersion {
let hashes = hashes.remove(&version_id).map(|x|x.1).unwrap_or_default();
let version_fields = version_fields.remove(&version_id).map(|x|x.1).unwrap_or_default();
let dependencies = dependencies.remove(&version_id).map(|x|x.1).unwrap_or_default();
let dependencies = dependencies
.into_iter()
.map(|mut dependency| {
if let Some(attr) = dependency_attributions.get(&dependency.id)
&& (attr.attribution.flame_project.is_some()
|| attr.attribution.resolution.is_some())
{
dependency.attribution = Some(attr.attribution.clone());
}
dependency
})
.collect_vec();
let loader_fields = loader_fields.iter()
.filter(|x| loader_loader_field_ids.contains(&x.id))
@@ -1030,6 +1055,22 @@ impl DBVersion {
.await?;
Ok(())
}
pub async fn clear_cache_ids(
version_ids: &[DBVersionId],
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis
.delete_many(
version_ids
.iter()
.map(|id| (VERSIONS_NAMESPACE, Some(id.0.to_string()))),
)
.await?;
Ok(())
}
}
#[derive(Clone, Deserialize, Serialize)]
+15 -3
View File
@@ -25,6 +25,8 @@ pub mod util;
const DEFAULT_EXPIRY: i64 = 60 * 60 * 12; // 12 hours
const ACTUAL_EXPIRY: i64 = 60 * 30; // 30 minutes
const VERSION_DEFAULT_EXPIRY: i64 = 60 * 60 * 48; // 48 hours
const VERSION_ACTUAL_EXPIRY: i64 = 60 * 60 * 24; // 24 hours
// Bound how many commands we send in a single Redis pipeline. The multiplexed
// connection's BytesMut write buffer keeps its peak capacity for the life of
@@ -39,6 +41,15 @@ const MGET_CHUNK_SIZE: usize = 32;
// BytesMut peak capacity that builds up under steady load.
const REDIS_MAX_CONN_AGE: Duration = Duration::from_secs(120);
fn cache_expiries(namespace: &str) -> (i64, i64) {
match namespace {
"versions" | "versions_files" => {
(VERSION_DEFAULT_EXPIRY, VERSION_ACTUAL_EXPIRY)
}
_ => (DEFAULT_EXPIRY, ACTUAL_EXPIRY),
}
}
#[derive(Clone)]
pub struct RedisPool {
pub url: String,
@@ -372,6 +383,7 @@ impl RedisPool {
.instrument(info_span!("get cached values"))
};
let (default_expiry, actual_expiry) = cache_expiries(namespace);
let current_time = Utc::now();
let mut expired_values = HashMap::new();
@@ -379,7 +391,7 @@ impl RedisPool {
let mut cached_values = cached_values_raw
.into_iter()
.filter_map(|(key, val)| {
if Utc.timestamp_opt(val.iat + ACTUAL_EXPIRY, 0).unwrap()
if Utc.timestamp_opt(val.iat + actual_expiry, 0).unwrap()
< current_time
{
expired_values.insert(val.key.to_string(), val);
@@ -481,7 +493,7 @@ impl RedisPool {
self.meta_namespace
),
serde_json::to_string(&value)?,
DEFAULT_EXPIRY as u64,
default_expiry as u64,
);
pipe_cmds += 1;
@@ -501,7 +513,7 @@ impl RedisPool {
self.meta_namespace, actual_slug
),
key.to_string(),
DEFAULT_EXPIRY as u64,
default_expiry as u64,
);
pipe_cmds += 1;
}
+19 -1
View File
@@ -102,6 +102,7 @@ pub async fn scan_all_files(
file_id,
&overrides,
&resolved,
redis,
&mut txn,
)
.await
@@ -164,7 +165,7 @@ pub async fn scan_file(
})?;
persist_attribution_results(
project_id, file_id, &overrides, &resolved, txn,
project_id, file_id, &overrides, &resolved, redis, txn,
)
.await
.wrap_err_with(|| {
@@ -303,6 +304,7 @@ async fn persist_attribution_results(
file_id: DBFileId,
overrides: &[OverrideFile],
resolved: &HashMap<String, OverrideResolution>,
redis: &RedisPool,
txn: &mut PgTransaction<'_>,
) -> Result<()> {
let all_sha1s: Vec<Vec<u8>> = overrides
@@ -552,6 +554,22 @@ async fn persist_attribution_results(
.wrap_err("inserting override file sources")?;
}
let version_id = sqlx::query_scalar!(
r#"
select version_id as "version_id: DBVersionId"
from files
where id = $1
"#,
file_id as DBFileId,
)
.fetch_one(&mut *txn)
.await
.wrap_err("fetching scanned file version")?;
DBVersion::clear_cache_ids(&[version_id], redis)
.await
.wrap_err("clearing version cache after attribution scan")?;
Ok(())
}
+4 -3
View File
@@ -242,7 +242,8 @@ impl AutomatedModerationQueue {
for project in projects {
async {
let project =
database::DBProject::get_id((project).into(), &pool, &redis).await?;
database::DBProject::get_id((project).into(), &*ro_pool, &redis)
.await?;
if let Some(project) = project {
let res = async {
@@ -282,7 +283,7 @@ impl AutomatedModerationQueue {
}
let versions =
database::DBVersion::get_many(&project.versions, &pool, &redis)
database::DBVersion::get_many(&project.versions, &*ro_pool, &redis)
.await?
.into_iter()
// we only support modpacks at this time
@@ -374,7 +375,7 @@ impl AutomatedModerationQueue {
let versions_data = filter_visible_versions(
database::models::DBVersion::get_many(
&version_ids,
&pool,
&*ro_pool,
&redis,
)
.await?,
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::auth::get_user_from_headers;
use crate::database::PgPool;
use crate::database::models::{
DBOrganization, DBTeamMember,
DBOrganization, DBTeamMember, DBVersion,
ids::{
DBAttributionGroupId, DBProjectId, DBVersionId,
generate_attribution_group_id,
@@ -143,31 +143,59 @@ async fn scan(
project_ids.sort_unstable_by_key(|id| id.0);
project_ids.dedup_by_key(|id| id.0);
for project_id in project_ids {
for project_id in &project_ids {
ensure_can_upload_versions_to_project(
pool.as_ref(),
project_id,
*project_id,
&user,
"you do not have permission to upload versions to this project",
)
.await?;
}
let project_ids = project_ids.iter().map(|id| id.0).collect::<Vec<_>>();
let mut transaction = pool
.begin()
.await
.wrap_internal_err("failed to begin attribution scan transaction")?;
sqlx::query!(
r#"
delete from attributions_exemptions
where version_id = any($1) or project_id = any($2)
"#,
&version_ids,
&project_ids,
)
.execute(&mut transaction)
.await
.wrap_internal_err("failed to remove attribution scan exemptions")?;
let result = sqlx::query!(
r#"
insert into file_scans (file_id)
select f.id
from files f
inner join attribution_enforced_versions aev on aev.id = f.version_id
where f.version_id = any($1)
on conflict (file_id) do nothing
"#,
&version_ids,
)
.execute(pool.as_ref())
.execute(&mut transaction)
.await
.wrap_internal_err("failed to queue version files for attribution scan")?;
transaction
.commit()
.await
.wrap_internal_err("failed to commit attribution scan transaction")?;
let version_ids =
version_ids.into_iter().map(DBVersionId).collect::<Vec<_>>();
DBVersion::clear_cache_ids(&version_ids, redis.as_ref())
.await
.wrap_internal_err("failed to clear version cache")?;
Ok(web::Json(ScanResponse {
queued_files: result.rows_affected(),
}))
@@ -491,6 +519,9 @@ async fn update_group(
return Err(ApiError::NotFound);
}
clear_group_version_cache(pool.as_ref(), redis.as_ref(), &[group_id])
.await?;
Ok(())
}
@@ -640,6 +671,14 @@ async fn assign(
.await
.wrap_internal_err("failed to clean up empty attribution groups")?;
clear_project_sha1_version_cache(
pool.as_ref(),
redis.as_ref(),
project_id,
&sha1_bytes,
)
.await?;
Ok(())
}
@@ -745,6 +784,72 @@ async fn split(
.await
.wrap_internal_err("failed to clean up empty attribution groups")?;
clear_project_sha1_version_cache(
pool.as_ref(),
redis.as_ref(),
project_id,
&sha1_bytes,
)
.await?;
Ok(())
}
async fn clear_group_version_cache(
pool: &PgPool,
redis: &RedisPool,
group_ids: &[i64],
) -> Result<(), ApiError> {
let version_ids = sqlx::query_scalar!(
r#"
select distinct f.version_id as "version_id: DBVersionId"
from project_attribution_files paf
inner join project_attribution_groups pag on pag.id = paf.group_id
inner join override_file_sources ofs on ofs.sha1 = paf.sha1
inner join files f on f.id = ofs.file_id
inner join versions v on v.id = f.version_id
where paf.group_id = any($1)
and pag.project_id = v.mod_id
"#,
group_ids,
)
.fetch_all(pool)
.await
.wrap_internal_err("failed to fetch attribution group versions")?;
DBVersion::clear_cache_ids(&version_ids, redis)
.await
.wrap_internal_err("failed to clear version attribution cache")?;
Ok(())
}
async fn clear_project_sha1_version_cache(
pool: &PgPool,
redis: &RedisPool,
project_id: DBProjectId,
sha1: &[u8],
) -> Result<(), ApiError> {
let version_ids = sqlx::query_scalar!(
r#"
select distinct f.version_id as "version_id: DBVersionId"
from override_file_sources ofs
inner join files f on f.id = ofs.file_id
inner join versions v on v.id = f.version_id
where ofs.sha1 = $1
and v.mod_id = $2
"#,
sha1,
project_id as DBProjectId,
)
.fetch_all(pool)
.await
.wrap_internal_err("failed to fetch attribution file versions")?;
DBVersion::clear_cache_ids(&version_ids, redis)
.await
.wrap_internal_err("failed to clear version attribution cache")?;
Ok(())
}
+1 -1
View File
@@ -65,7 +65,7 @@ pub async fn forge_updates(
let versions = database::models::DBVersion::get_many(
&project.versions,
&**pool,
&***ro_pool,
&redis,
)
.await?;
+15 -6
View File
@@ -1307,7 +1307,8 @@ pub async fn dependency_list_internal(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = db_models::DBProject::get(&string, &**pool, &redis).await?;
let result =
db_models::DBProject::get(&string, &***ro_pool, &redis).await?;
let user_option = get_user_from_headers(
&req,
@@ -1329,7 +1330,7 @@ pub async fn dependency_list_internal(
let dependencies = database::DBProject::get_dependencies(
project.inner.id,
&**pool,
&***ro_pool,
&redis,
)
.await?;
@@ -1355,11 +1356,19 @@ pub async fn dependency_list_internal(
.unique()
.collect::<Vec<db_models::DBVersionId>>();
let (projects_result, versions_result) = futures::future::try_join(
database::DBProject::get_many_ids(&project_ids, &**pool, &redis),
database::DBProject::get_many_ids(
&project_ids,
&***ro_pool,
&redis,
),
async {
database::DBVersion::get_many(&dep_version_ids, &**pool, &redis)
.await
.wrap_internal_err("failed to fetch dependency versions")
database::DBVersion::get_many(
&dep_version_ids,
&***ro_pool,
&redis,
)
.await
.wrap_internal_err("failed to fetch dependency versions")
},
)
.await?;
+1 -1
View File
@@ -251,7 +251,7 @@ pub async fn get_versions_from_hashes(
.await?,
&user_option,
&pool,
&pool,
pool.as_ref(),
&redis,
)
.await?;
+25 -28
View File
@@ -2,8 +2,7 @@ use std::collections::HashMap;
use super::ApiError;
use crate::auth::checks::{
enrich_dependency_attributions, filter_visible_versions,
is_visible_project, is_visible_version,
filter_visible_versions, is_visible_project, is_visible_version,
};
use crate::auth::get_user_from_headers;
use crate::database;
@@ -81,7 +80,7 @@ pub async fn version_project_get_helper(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let result =
database::models::DBProject::get(&id.0, &**pool, &redis).await?;
database::models::DBProject::get(&id.0, &***ro_pool, &redis).await?;
let user_option = get_user_from_headers(
&req,
@@ -103,7 +102,7 @@ pub async fn version_project_get_helper(
let versions = database::models::DBVersion::get_many(
&project.versions,
&**pool,
&***ro_pool,
&redis,
)
.await?;
@@ -114,20 +113,16 @@ pub async fn version_project_get_helper(
|| x.inner.version_number == id.1
});
if let Some(mut version) = version
if let Some(version) = version
&& is_visible_version(&version.inner, &user_option, &pool, &redis)
.await?
{
let version_id = version.inner.id;
enrich_dependency_attributions(
std::slice::from_mut(&mut version),
&ro_pool,
)
.await;
let mut v = models::projects::Version::from(version);
let missing = get_files_missing_attribution(&**pool, &[version_id])
.await
.unwrap_or_default();
let missing =
get_files_missing_attribution(&***ro_pool, &[version_id])
.await
.unwrap_or_default();
v.files_missing_attribution = missing
.get(&version_id)
.map(|entries| {
@@ -180,9 +175,12 @@ pub async fn versions_get(
.into_iter()
.map(|x| x.into())
.collect::<Vec<database::models::DBVersionId>>();
let versions_data =
database::models::DBVersion::get_many(&version_ids, &**pool, &redis)
.await?;
let versions_data = database::models::DBVersion::get_many(
&version_ids,
&***ro_pool,
&redis,
)
.await?;
let user_option = get_user_from_headers(
&req,
@@ -234,7 +232,8 @@ pub async fn version_get_helper(
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<models::projects::Version>, ApiError> {
let version_data =
database::models::DBVersion::get(id.into(), &**pool, &redis).await?;
database::models::DBVersion::get(id.into(), &***ro_pool, &redis)
.await?;
let user_option = get_user_from_headers(
&req,
@@ -247,17 +246,12 @@ pub async fn version_get_helper(
.map(|x| x.1)
.ok();
if let Some(mut data) = version_data
if let Some(data) = version_data
&& is_visible_version(&data.inner, &user_option, &pool, &redis).await?
{
let version_id = data.inner.id;
enrich_dependency_attributions(
std::slice::from_mut(&mut data),
&ro_pool,
)
.await;
let mut version = models::projects::Version::from(data);
let missing = get_files_missing_attribution(&**pool, &[version_id])
let missing = get_files_missing_attribution(&***ro_pool, &[version_id])
.await
.unwrap_or_default();
version.files_missing_attribution = missing
@@ -839,7 +833,7 @@ pub async fn version_list_internal(
let string = info.into_inner().0;
let result =
database::models::DBProject::get(&string, &**pool, &redis).await?;
database::models::DBProject::get(&string, &***ro_pool, &redis).await?;
let user_option = get_user_from_headers(
&req,
@@ -868,7 +862,7 @@ pub async fn version_list_internal(
});
let mut versions = database::models::DBVersion::get_many(
&project.versions,
&**pool,
&***ro_pool,
&redis,
)
.await?
@@ -926,11 +920,14 @@ pub async fn version_list_internal(
// TODO: This is a bandaid fix for detecting auto-featured versions.
// In the future, not all versions will have 'game_versions' fields, so this will need to be changed.
let (loaders, game_versions) = futures::future::try_join(
database::models::loader_fields::Loader::list(&**pool, &redis),
database::models::loader_fields::Loader::list(
&***ro_pool,
&redis,
),
database::models::legacy_loader_fields::MinecraftGameVersion::list(
None,
Some(true),
&**pool,
&***ro_pool,
&redis,
),
)